├── log └── .keep ├── storage └── .keep ├── tmp └── .keep ├── vendor └── .keep ├── .ruby-version ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── vod_test.rb │ ├── user_test.rb │ ├── category_test.rb │ ├── channel_test.rb │ ├── follow_test.rb │ └── message_test.rb ├── system │ └── .keep ├── controllers │ ├── .keep │ ├── api │ │ ├── vods_controller_test.rb │ │ ├── users_controller_test.rb │ │ ├── channels_controller_test.rb │ │ ├── follows_controller_test.rb │ │ ├── sessions_controller_test.rb │ │ ├── streams_controller_test.rb │ │ └── categories_controller_test.rb │ ├── streams_controller_test.rb │ └── static_pages_controller_test.rb ├── fixtures │ ├── .keep │ ├── files │ │ └── .keep │ ├── users.yml │ ├── vods.yml │ ├── categories.yml │ ├── channels.yml │ ├── follows.yml │ └── messages.yml ├── integration │ └── .keep ├── application_system_test_case.rb └── test_helper.rb ├── app ├── assets │ ├── images │ │ ├── .keep │ │ └── favicon.png │ ├── javascripts │ │ ├── channels │ │ │ ├── .keep │ │ │ ├── stream.coffee │ │ │ └── chat_room.coffee │ │ ├── images │ │ │ ├── 8e985dfdad1583685bb235dce1ec4da8-profile2.png │ │ │ ├── 9882be331263ed63cf182224e6c67994-twitchwhite.png │ │ │ └── c9b0c2295ed8af575514fa70aff31345-profile.svg │ │ ├── api │ │ │ ├── users.coffee │ │ │ ├── vods.coffee │ │ │ ├── categories.coffee │ │ │ ├── channels.coffee │ │ │ ├── follows.coffee │ │ │ ├── sessions.coffee │ │ │ └── streams.coffee │ │ ├── streams.coffee │ │ ├── static_pages.coffee │ │ ├── cable.js │ │ ├── frontend │ │ │ └── images │ │ │ │ └── c9b0c2295ed8af575514fa70aff31345-profile.svg │ │ └── application.js │ ├── config │ │ └── manifest.js │ └── stylesheets │ │ ├── api │ │ ├── vods.scss │ │ ├── follows.scss │ │ ├── streams.scss │ │ ├── users.scss │ │ ├── channels.scss │ │ ├── sessions.scss │ │ └── categories.scss │ │ ├── streams.scss │ │ ├── static_pages.scss │ │ └── application.css ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ ├── message.rb │ ├── follow.rb │ ├── category.rb │ ├── channel.rb │ ├── vod.rb │ └── user.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── static_pages_controller.rb │ ├── api │ │ ├── categories_controller.rb │ │ ├── sessions_controller.rb │ │ ├── vods_controller.rb │ │ ├── streams_controller.rb │ │ ├── users_controller.rb │ │ ├── follows_controller.rb │ │ └── channels_controller.rb │ └── application_controller.rb ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── mailer.html.erb │ │ └── application.html.erb │ ├── api │ │ ├── follows │ │ │ ├── follow.json.jbuilder │ │ │ └── show.json.jbuilder │ │ ├── users │ │ │ ├── _user.json.jbuilder │ │ │ ├── user.json.jbuilder │ │ │ └── new_user.json.jbuilder │ │ ├── channels │ │ │ ├── show.json.jbuilder │ │ │ ├── search.json.jbuilder │ │ │ ├── index.json.jbuilder │ │ │ └── index_first_vods.json.jbuilder │ │ ├── categories │ │ │ ├── show.json.jbuilder │ │ │ └── index.json.jbuilder │ │ └── vods │ │ │ ├── show.json.jbuilder │ │ │ └── index.json.jbuilder │ └── static_pages │ │ └── root.html.erb ├── helpers │ ├── streams_helper.rb │ ├── api │ │ ├── users_helper.rb │ │ ├── vods_helper.rb │ │ ├── channels_helper.rb │ │ ├── follows_helper.rb │ │ ├── sessions_helper.rb │ │ ├── streams_helper.rb │ │ └── categories_helper.rb │ ├── application_helper.rb │ └── static_pages_helper.rb ├── jobs │ └── application_job.rb ├── channels │ ├── application_cable │ │ ├── channel.rb │ │ └── connection.rb │ ├── stream_channel.rb │ └── chat_room_channel.rb └── mailers │ └── application_mailer.rb ├── public ├── apple-touch-icon.png ├── apple-touch-icon-precomposed.png ├── favicon.png ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── frontend ├── components │ ├── Session │ │ ├── SignupForm │ │ │ ├── SignupForm.module.css │ │ │ └── signup_container_component.js │ │ ├── ErrorBox │ │ │ ├── ErrorBox.module.css │ │ │ └── ErrorBox.jsx │ │ ├── TabNavs │ │ │ ├── TabNavs.module.css │ │ │ └── TabsNav.jsx │ │ ├── login_container_component.js │ │ ├── DemoForm │ │ │ └── DemoForm.jsx │ │ └── SessionForm.jsx │ ├── MainPage │ │ ├── MainPage.module.css │ │ └── MainPage.jsx │ ├── twitchwhite.png │ ├── Stream │ │ └── Stream.module.css │ ├── Vods │ │ ├── VodShow │ │ │ ├── VodShow.module.css │ │ │ └── VodShow.jsx │ │ └── VodsIndex │ │ │ ├── VodIndexItem.jsx │ │ │ ├── ChannelVideosIndex.jsx │ │ │ └── ChannelVideosIndex.module.css │ ├── Modal │ │ ├── Modal.module.css │ │ └── Modal.jsx │ ├── Root.jsx │ ├── App.jsx │ ├── ChatRoom │ │ ├── MessageForm │ │ │ ├── EmojiMenu.module.css │ │ │ ├── EmojiMenu.jsx │ │ │ └── MessageForm.module.css │ │ ├── ChatRoom.module.css │ │ └── ChatRoom.jsx │ ├── Dashboard │ │ ├── SuccessMessage.module.css │ │ ├── SuccessMessage.jsx │ │ ├── VideoForm.module.css │ │ └── Dashboard.module.css │ ├── Channels │ │ ├── ChannelShow │ │ │ ├── ChannelNavs.module.css │ │ │ ├── ChannelShow.module.css │ │ │ ├── FollowButton.jsx │ │ │ └── ChannelNavs.jsx │ │ ├── ChannelIndexItem │ │ │ ├── ChannelIndexItem.module.css │ │ │ └── ChannelIndexItem.jsx │ │ ├── ChannelIndex.module.css │ │ └── ChannelIndex.jsx │ ├── HomePageIndex │ │ ├── HomePageIndex.jsx │ │ └── HomePageIndex.module.css │ ├── ChannelFollowers │ │ ├── ChannelFollowers.module.css │ │ └── ChannelFollowers.jsx │ ├── SessionControls │ │ ├── session_controls_container.js │ │ ├── DropDownMenu │ │ │ ├── DropDownMenu.module.css │ │ │ └── DropDownMenu.jsx │ │ └── SessionControls.module.css │ ├── SideBar │ │ ├── SideBarItem.jsx │ │ ├── SideBar.module.css │ │ └── SideBar.jsx │ ├── About │ │ ├── AboutPage.module.css │ │ └── AboutPage.jsx │ ├── NavBar │ │ ├── NavBar.jsx │ │ ├── NavBar.module.css │ │ ├── SearchBar.module.css │ │ └── SearchBar.jsx │ ├── App.module.css │ ├── MainNav │ │ ├── MainNav.module.css │ │ └── MainNav.jsx │ ├── Categories │ │ ├── CategoryShow.module.css │ │ ├── Categories.jsx │ │ └── CategoryShow.jsx │ ├── Carousel │ │ └── Carousel.module.css │ └── ChannelHome │ │ └── ChannelHome.module.css ├── reducers │ ├── demo_reducer.js │ ├── errors_reducer.js │ ├── ui_reducer.js │ ├── modal_reducer.js │ ├── root_reducer.js │ ├── entities_reducer.js │ ├── categories_reducer.js │ ├── session_errors_reducer.js │ ├── sessions_reducer.js │ ├── vods_reducer.js │ ├── users_reducer.js │ └── channels_reducer.js ├── util │ ├── categories_api_util.js │ ├── selectors.js │ ├── follows_api_util.js │ ├── session_api_util.js │ ├── vod_api_util.js │ ├── stream_util.js │ ├── channels_api_util.js │ └── route_util.jsx ├── actions │ ├── modal_actions.js │ ├── category_actions.js │ ├── follow_actions.js │ ├── vod_actions.js │ ├── channel_actions.js │ └── session_actions.js ├── store │ └── store.js └── index.jsx ├── UIHere.png ├── bin ├── bundle ├── rake ├── rails ├── yarn ├── spring ├── update └── setup ├── config ├── spring.rb ├── environment.rb ├── initializers │ ├── mime_types.rb │ ├── filter_parameter_logging.rb │ ├── application_controller_renderer.rb │ ├── cookies_serializer.rb │ ├── backtrace_silencers.rb │ ├── wrap_parameters.rb │ ├── assets.rb │ ├── inflections.rb │ └── content_security_policy.rb ├── boot.rb ├── cable.yml ├── routes.rb ├── credentials.yml.enc ├── application.rb ├── locales │ └── en.yml ├── storage.yml ├── puma.rb └── environments │ ├── test.rb │ └── development.rb ├── config.ru ├── db └── migrate │ ├── 20200508012904_add_channel_name_to_channels.rb │ ├── 20200430204938_add_email_to_users.rb │ ├── 20200428003032_create_channels.rb │ ├── 20200608021328_create_categories.rb │ ├── 20200506052708_create_follows.rb │ ├── 20200502024743_create_vods.rb │ ├── 20200428213901_create_messages.rb │ ├── 20200427180156_create_users.rb │ └── 20200501081013_create_active_storage_tables.active_storage.rb ├── Rakefile ├── rtmpserver └── package.json ├── .eslintrc.js ├── .gitignore ├── package.json ├── webpack.config.js └── Gemfile /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.5.1 -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/system/.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 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/helpers/streams_helper.rb: -------------------------------------------------------------------------------- 1 | module StreamsHelper 2 | end 3 | -------------------------------------------------------------------------------- /frontend/components/Session/SignupForm/SignupForm.module.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/helpers/api/users_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/vods_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::VodsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/channels_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::ChannelsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/follows_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::FollowsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/sessions_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::SessionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/streams_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::StreamsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/static_pages_helper.rb: -------------------------------------------------------------------------------- 1 | module StaticPagesHelper 2 | end 3 | -------------------------------------------------------------------------------- /UIHere.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccoombsesmail/Fidget/HEAD/UIHere.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccoombsesmail/Fidget/HEAD/public/favicon.png -------------------------------------------------------------------------------- /app/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccoombsesmail/Fidget/HEAD/app/assets/images/favicon.png -------------------------------------------------------------------------------- /app/views/api/follows/follow.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | json.channelId @follow.channel_id 3 | json.userId @follow.user_id 4 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /frontend/components/MainPage/MainPage.module.css: -------------------------------------------------------------------------------- 1 | .mainContainer { 2 | display: flex; 3 | height: 100%; 4 | 5 | } -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /frontend/components/twitchwhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccoombsesmail/Fidget/HEAD/frontend/components/twitchwhite.png -------------------------------------------------------------------------------- /frontend/components/Stream/Stream.module.css: -------------------------------------------------------------------------------- 1 | .videoPlayer { 2 | width: 100%; 3 | height: 800px; 4 | padding-bottom: 400px; 5 | } -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w[ 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ].each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /config.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 | -------------------------------------------------------------------------------- /test/models/vod_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class VodTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/channel_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ChannelTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/follow_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class FollowTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/message_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MessageTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/message.rb: -------------------------------------------------------------------------------- 1 | class Message < ApplicationRecord 2 | validates :user_id, presence: true 3 | validates :body, presence: true 4 | belongs_to :user 5 | 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/static_pages_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticPagesController < ApplicationController 2 | def root 3 | @demoUser = User.find_by(username: 'FidgetDemoUser') 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/javascripts/images/8e985dfdad1583685bb235dce1ec4da8-profile2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccoombsesmail/Fidget/HEAD/app/assets/javascripts/images/8e985dfdad1583685bb235dce1ec4da8-profile2.png -------------------------------------------------------------------------------- /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/javascripts/images/9882be331263ed63cf182224e6c67994-twitchwhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccoombsesmail/Fidget/HEAD/app/assets/javascripts/images/9882be331263ed63cf182224e6c67994-twitchwhite.png -------------------------------------------------------------------------------- /app/models/follow.rb: -------------------------------------------------------------------------------- 1 | class Follow < ApplicationRecord 2 | validates :channel_id, :user_id, presence: true 3 | 4 | belongs_to :user 5 | 6 | belongs_to :channel 7 | 8 | 9 | 10 | end 11 | -------------------------------------------------------------------------------- /test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 4 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400] 5 | end 6 | -------------------------------------------------------------------------------- /test/controllers/api/vods_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::VodsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/streams_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class StreamsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::UsersControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/category.rb: -------------------------------------------------------------------------------- 1 | class Category < ApplicationRecord 2 | validates :name, :description, presence: true 3 | validates :name, uniqueness: true 4 | 5 | has_one_attached :imgUrl 6 | 7 | 8 | end 9 | -------------------------------------------------------------------------------- /app/views/api/users/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | 3 | json.id user.id 4 | json.username user.username 5 | json.channelId user.channel.id 6 | json.follows user.followed_channels.map{|channel| channel.id } 7 | -------------------------------------------------------------------------------- /test/controllers/api/channels_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::ChannelsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/follows_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::FollowsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/sessions_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::SessionsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/streams_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::StreamsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/static_pages_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class StaticPagesControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/vods.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/vods 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/streams.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the streams controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/views/api/users/user.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | json.id @user.id 5 | json.username @user.username 6 | json.channelId @user.channel.id 7 | json.follows @user.followed_channels.map{|channel| channel.id } 8 | -------------------------------------------------------------------------------- /db/migrate/20200508012904_add_channel_name_to_channels.rb: -------------------------------------------------------------------------------- 1 | class AddChannelNameToChannels < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :channels, :channel_name, :string, null: false 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 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/follows.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/follows controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/streams.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/streams 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 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/channels.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/channels controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/sessions.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/sessions controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/static_pages.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the StaticPages controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/views/api/channels/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.set! @channel.id do 2 | json.id @channel.id 3 | json.ownerId @channel.owner_id 4 | json.channelName @channel.channel_name 5 | json.logoUrl url_for(@channel.logoUrl) 6 | end -------------------------------------------------------------------------------- /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 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /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/views/api/categories/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.set! @category.id do 2 | json.id @category.id 3 | json.name @category.name 4 | json.description @category.description 5 | json.imgUrl url_for(@category.imgUrl) 6 | end -------------------------------------------------------------------------------- /app/views/api/vods/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.set! @vod.id do 2 | json.id @vod.id 3 | json.channel_id @vod.channel_id 4 | json.title @vod.title 5 | json.category @vod.category 6 | json.videoUrl url_for(@vod.videoUrl) 7 | 8 | end -------------------------------------------------------------------------------- /db/migrate/20200430204938_add_email_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddEmailToUsers < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :users, :email, :string, null: false 4 | add_column :users, :dob, :date, null: false 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /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/vods.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/streams.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/channels.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/follows.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/sessions.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/streams.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/static_pages.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/channels/stream_channel.rb: -------------------------------------------------------------------------------- 1 | class StreamChannel < ApplicationCable::Channel 2 | def subscribed 3 | stream_from "stream_channel" 4 | end 5 | 6 | def unsubscribed 7 | # Any cleanup needed when channel is unsubscribed 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://redistogo:84d2638644ea10a558714035c1e341ce@pike.redistogo.com:10840/ 10 | channel_prefix: Fidget_production 11 | -------------------------------------------------------------------------------- /frontend/reducers/demo_reducer.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const demoReducer = (state = {}, action) => { 4 | Object.freeze(state) 5 | 6 | switch (action.type) { 7 | default: 8 | return state 9 | } 10 | } 11 | 12 | 13 | 14 | export default demoReducer; -------------------------------------------------------------------------------- /frontend/components/Vods/VodShow/VodShow.module.css: -------------------------------------------------------------------------------- 1 | .videoPlayer { 2 | width: 100%; 3 | height: 800px; 4 | padding-bottom: 500px; 5 | outline: none; 6 | 7 | } 8 | 9 | 10 | .vodWrapper { 11 | min-height: 200vh; 12 | overflow-y: scroll; 13 | padding-bottom: 500px; 14 | } -------------------------------------------------------------------------------- /frontend/reducers/errors_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import sessionsErrorsReducer from './session_errors_reducer' 3 | 4 | 5 | const errorsReducer = combineReducers({ 6 | session: sessionsErrorsReducer, 7 | }) 8 | 9 | 10 | export default errorsReducer 11 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /rtmpserver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rtmpserver", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /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/20200428003032_create_channels.rb: -------------------------------------------------------------------------------- 1 | class CreateChannels < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :channels do |t| 4 | t.integer :owner_id, null: false 5 | t.timestamps 6 | end 7 | 8 | add_index :channels, :owner_id, unique: true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/views/api/categories/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @categories.each do |category| 2 | json.set! category.id do 3 | json.id category.id 4 | json.name category.name 5 | json.description category.description 6 | json.imgUrl url_for(category.imgUrl) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /frontend/util/categories_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchCategories = () => { 2 | return $.ajax({ 3 | method: 'GET', 4 | url: 'api/categories', 5 | }) 6 | } 7 | 8 | export const fetchCategory = (name) => { 9 | return $.ajax({ 10 | method: 'GET', 11 | url: `api/categories/${name}`, 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /frontend/reducers/ui_reducer.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux' 2 | import modalReducer from './modal_reducer' 3 | import demoReducer from './demo_reducer' 4 | 5 | 6 | const uiReducer = combineReducers({ 7 | modal: modalReducer, 8 | demoUserId: demoReducer 9 | }) 10 | 11 | 12 | 13 | export default uiReducer -------------------------------------------------------------------------------- /app/views/api/follows/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | @followers.each do |follow| 2 | channel = follow.user.channel 3 | json.set! channel.id do 4 | json.id channel.id 5 | json.ownerId channel.owner_id 6 | json.channelName channel.channel_name 7 | json.logoUrl url_for(channel.logoUrl) 8 | end 9 | end 10 | 11 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require_relative '../config/environment' 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 | -------------------------------------------------------------------------------- /app/views/api/channels/search.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.channels do 2 | @channels.each do |channel| 3 | json.set! channel.id do 4 | json.id channel.id 5 | json.ownerId channel.owner_id 6 | json.channelName channel.channel_name 7 | json.logoUrl url_for(channel.logoUrl) 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | begin 5 | exec "yarnpkg", *ARGV 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20200608021328_create_categories.rb: -------------------------------------------------------------------------------- 1 | class CreateCategories < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :categories do |t| 4 | t.string :name, null: false 5 | t.string :description, null: false 6 | t.timestamps 7 | end 8 | 9 | add_index :categories, :name, unique: true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /frontend/components/Modal/Modal.module.css: -------------------------------------------------------------------------------- 1 | .modalBg { 2 | position: fixed; 3 | top: 0; 4 | bottom: 0; 5 | right: 0; 6 | left: 0; 7 | background: rgba(0, 0, 0, 0.7); 8 | z-index: 100000; 9 | } 10 | 11 | .modalChild { 12 | position: absolute; 13 | top: 50%; 14 | left: 50%; 15 | transform: translate(-50%, -50%); 16 | z-index: 10000; 17 | 18 | } -------------------------------------------------------------------------------- /db/migrate/20200506052708_create_follows.rb: -------------------------------------------------------------------------------- 1 | class CreateFollows < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :follows do |t| 4 | t.integer :channel_id, null: false 5 | t.integer :user_id, null: false 6 | t.timestamps 7 | end 8 | 9 | add_index :follows, :channel_id 10 | add_index :follows, :user_id 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /frontend/actions/modal_actions.js: -------------------------------------------------------------------------------- 1 | export const OPEN_MODAL = "OPEN_MODAL" 2 | export const CLOSE_MODAL = "CLOSE_MODAL" 3 | 4 | 5 | 6 | 7 | export const openModal = (component) => { 8 | return { 9 | type: OPEN_MODAL, 10 | component 11 | 12 | } 13 | } 14 | 15 | export const closeModal = () => { 16 | return { 17 | type: CLOSE_MODAL 18 | } 19 | } -------------------------------------------------------------------------------- /db/migrate/20200502024743_create_vods.rb: -------------------------------------------------------------------------------- 1 | class CreateVods < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :vods do |t| 4 | t.integer :channel_id, null: false 5 | t.string :title, null: false 6 | t.string :category, null: false 7 | t.timestamps 8 | end 9 | add_index :vods, :channel_id 10 | add_index :vods, :category 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /frontend/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | // import logger from 'redux-logger' 3 | import thunk from 'redux-thunk' 4 | import RootReducer from '../reducers/root_reducer' 5 | 6 | 7 | const configureStore = (preloadedState = {}) => { 8 | return createStore(RootReducer, preloadedState, applyMiddleware(thunk)) 9 | } 10 | 11 | 12 | export default configureStore 13 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/stream.coffee: -------------------------------------------------------------------------------- 1 | App.stream = App.cable.subscriptions.create "StreamChannel", 2 | connected: -> 3 | # Called when the subscription is ready for use on the server 4 | 5 | disconnected: -> 6 | # Called when the subscription has been terminated by the server 7 | 8 | received: (data) -> 9 | # Called when there's incoming data on the websocket for this channel 10 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/chat_room.coffee: -------------------------------------------------------------------------------- 1 | App.chat_room = App.cable.subscriptions.create "ChatRoomChannel", 2 | connected: -> 3 | # Called when the subscription is ready for use on the server 4 | 5 | disconnected: -> 6 | # Called when the subscription has been terminated by the server 7 | 8 | received: (data) -> 9 | # Called when there's incoming data on the websocket for this channel 10 | -------------------------------------------------------------------------------- /db/migrate/20200428213901_create_messages.rb: -------------------------------------------------------------------------------- 1 | class CreateMessages < ActiveRecord::Migration[5.2] 2 | 3 | def change 4 | create_table :messages do |t| 5 | t.integer :user_id, null: false 6 | t.integer :vod_id 7 | t.string :body, null: false 8 | t.timestamps 9 | end 10 | 11 | add_index :messages, :user_id 12 | add_index :messages, :vod_id 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/fixtures/vods.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/fixtures/categories.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/fixtures/channels.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/fixtures/follows.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/fixtures/messages.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /app/assets/javascripts/cable.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command. 3 | // 4 | //= require action_cable 5 | //= require_self 6 | //= require_tree ./channels 7 | 8 | (function() { 9 | this.App || (this.App = {}); 10 | 11 | App.cable = ActionCable.createConsumer(); 12 | 13 | }).call(this); 14 | -------------------------------------------------------------------------------- /app/assets/javascripts/images/c9b0c2295ed8af575514fa70aff31345-profile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/components/Root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Provider } from 'react-redux' 3 | import { HashRouter } from 'react-router-dom' 4 | import App from './App' 5 | /* eslint-disable */ 6 | 7 | const Root = ({ store }) => { 8 | 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | } 18 | 19 | 20 | export default Root 21 | -------------------------------------------------------------------------------- /app/assets/javascripts/frontend/images/c9b0c2295ed8af575514fa70aff31345-profile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db/migrate/20200427180156_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :users do |t| 4 | t.string :username, null: false 5 | t.string :session_token, null: false 6 | t.string :password_digest, null: false 7 | t.timestamps 8 | end 9 | 10 | add_index :users, :username, unique: true 11 | add_index :users, :session_token, unique: true 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /frontend/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classes from './App.module.css' 3 | 4 | import Modal from './Modal/Modal' 5 | import MainPage from './MainPage/MainPage' 6 | import NavBar from './NavBar/NavBar' 7 | 8 | const App = () => { 9 | return ( 10 |
11 | 12 | 13 | 14 |
15 | 16 | ) 17 | } 18 | 19 | 20 | export default App 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2020: true, 5 | }, 6 | extends: [ 7 | 'plugin:react/recommended', 8 | 'airbnb', 9 | ], 10 | parserOptions: { 11 | ecmaFeatures: { 12 | jsx: true, 13 | }, 14 | ecmaVersion: 11, 15 | sourceType: 'module', 16 | }, 17 | plugins: [ 18 | 'react', 19 | ], 20 | rules: { 21 | 'react/prop-types': 0, 22 | semi: [2, 'never'], 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /app/models/channel.rb: -------------------------------------------------------------------------------- 1 | class Channel < ApplicationRecord 2 | validates :owner_id, presence: true 3 | 4 | belongs_to :owner, 5 | foreign_key: :owner_id, 6 | class_name: :User 7 | 8 | has_many :vods, 9 | dependent: :destroy 10 | 11 | has_many :follows 12 | 13 | 14 | 15 | has_many :users, 16 | through: :follows, 17 | source: :user 18 | 19 | has_one_attached :logoUrl 20 | 21 | 22 | 23 | end 24 | -------------------------------------------------------------------------------- /frontend/util/selectors.js: -------------------------------------------------------------------------------- 1 | export const getFollowedChannels = (channels, currentUser) => { 2 | let followedChannels = [] 3 | let follows = []; 4 | if (currentUser) { 5 | follows = currentUser.follows 6 | for (const key in channels) { 7 | if (follows.indexOf(Number(key)) !== -1){ 8 | followedChannels.push(channels[key]) 9 | } 10 | } 11 | } 12 | 13 | return followedChannels 14 | 15 | } -------------------------------------------------------------------------------- /frontend/reducers/modal_reducer.js: -------------------------------------------------------------------------------- 1 | import {OPEN_MODAL, CLOSE_MODAL} from '../actions/modal_actions.js' 2 | 3 | 4 | 5 | 6 | const modalReducer = (state = null, action) => { 7 | Object.freeze(state) 8 | 9 | switch (action.type) { 10 | case OPEN_MODAL: 11 | return action.component 12 | case CLOSE_MODAL: 13 | return null 14 | default: 15 | return state 16 | } 17 | 18 | } 19 | 20 | 21 | 22 | export default modalReducer; -------------------------------------------------------------------------------- /frontend/reducers/root_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import entitiesReducer from './entities_reducer' 3 | import sessionsReducer from './sessions_reducer' 4 | import uiReducer from './ui_reducer' 5 | import errorsReducer from './errors_reducer' 6 | 7 | 8 | const rootReducer = combineReducers({ 9 | entities: entitiesReducer, 10 | session: sessionsReducer, 11 | ui: uiReducer, 12 | errors: errorsReducer, 13 | }) 14 | 15 | 16 | export default rootReducer 17 | -------------------------------------------------------------------------------- /app/controllers/api/categories_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::CategoriesController < ApplicationController 2 | 3 | def index 4 | @categories = Category.all 5 | render :index 6 | end 7 | 8 | def show 9 | @category = Category.find_by(name: params[:id]) 10 | 11 | if @category 12 | render :show 13 | else 14 | render json: {:error => "Category Does Not Exist"}, status: 422 15 | end 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | # identified_by :current_user 4 | 5 | # def connect 6 | # self.current_user = get_current_user 7 | # end 8 | 9 | # private 10 | # def get_current_user 11 | # user = User.find_by(session_token: cookies.signed[:session_token]) 12 | # reject_unauthorized_connection if user.nil? 13 | # user 14 | # end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /frontend/reducers/entities_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import usersReducer from './users_reducer' 3 | import channelsReducer from './channels_reducer'; 4 | import vodsReducer from './vods_reducer'; 5 | import categoriesReducer from './categories_reducer'; 6 | 7 | 8 | const entitiesReducer = combineReducers({ 9 | users: usersReducer, 10 | channels: channelsReducer, 11 | vods: vodsReducer, 12 | categories: categoriesReducer, 13 | }) 14 | 15 | 16 | export default entitiesReducer 17 | -------------------------------------------------------------------------------- /frontend/reducers/categories_reducer.js: -------------------------------------------------------------------------------- 1 | import {RECEIVE_CATEGORIES, RECEIVE_CATEGORY} from '../actions/category_actions' 2 | 3 | 4 | const categoriesReducer = (state = {}, action) => { 5 | switch (action.type) { 6 | case RECEIVE_CATEGORIES: 7 | return Object.assign({}, action.categories) 8 | break 9 | case RECEIVE_CATEGORY: 10 | return Object.assign({}, action.category) 11 | default: 12 | return state 13 | } 14 | } 15 | 16 | 17 | export default categoriesReducer -------------------------------------------------------------------------------- /app/views/api/users/new_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.user do 2 | json.set! @user.id do 3 | json.id @user.id 4 | json.username @user.username 5 | json.channelId @user.channel.id 6 | json.follows @user.followed_channels.map{|channel| channel.id } 7 | end 8 | end 9 | 10 | 11 | json.channel do 12 | json.set! @channel.id do 13 | json.id @channel.id 14 | json.ownerId @channel.owner_id 15 | json.channelName @channel.channel_name 16 | json.logoUrl url_for(@channel.logoUrl) 17 | end 18 | end -------------------------------------------------------------------------------- /app/views/static_pages/root.html.erb: -------------------------------------------------------------------------------- 1 | <% if logged_in? %> 2 | 3 | 10 | 11 | <% end %> 12 | 13 | 20 | 21 | 22 |
React is not working
-------------------------------------------------------------------------------- /frontend/components/Session/ErrorBox/ErrorBox.module.css: -------------------------------------------------------------------------------- 1 | .errorBox { 2 | display: flex; 3 | align-items: center; 4 | width: calc(90% - 3px); 5 | height: 50px; 6 | background-color: #E6E6EA; 7 | border: 1.2px solid red; 8 | border-radius: 7px; 9 | box-shadow: -3px 0 0px 0px red; 10 | margin-bottom: 10px; 11 | margin-left: 5px; 12 | 13 | } 14 | 15 | .errorBoxContent { 16 | font-size: 13px; 17 | } 18 | 19 | .errorsContentWrapper { 20 | display: flex; 21 | flex-direction: column; 22 | margin-left: 20px; 23 | 24 | } -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /frontend/components/Session/TabNavs/TabNavs.module.css: -------------------------------------------------------------------------------- 1 | .navContainer { 2 | display: flex; 3 | width: 87%; 4 | border-bottom: 1px solid #E5E5E5; 5 | margin-bottom: 30px; 6 | margin-top: 10px; 7 | } 8 | 9 | .navTab { 10 | list-style: none; 11 | height: 100%; 12 | width: 55px; 13 | margin-right: 20px; 14 | cursor: pointer; 15 | 16 | 17 | } 18 | 19 | .navTab > span { 20 | margin-bottom: 10px; 21 | font-size: 15px; 22 | } 23 | 24 | .selected { 25 | border-bottom: 2px solid #9147FF; 26 | color: #9147FF 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads Spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == 'spring' } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /frontend/util/follows_api_util.js: -------------------------------------------------------------------------------- 1 | export const postFollow = (follow) => { 2 | 3 | return $.ajax({ 4 | method: 'POST', 5 | url: '/api/follows', 6 | data: {follow} 7 | }) 8 | 9 | } 10 | 11 | 12 | export const deleteFollow = (channelId) => { 13 | 14 | return $.ajax({ 15 | method: 'DELETE', 16 | url: `/api/follows/${channelId}`, 17 | }) 18 | 19 | } 20 | 21 | 22 | export const getChannelFollowers = (id) => { 23 | 24 | return $.ajax({ 25 | method: 'GET', 26 | url: `/api/follows/${id}`, 27 | }) 28 | 29 | } -------------------------------------------------------------------------------- /frontend/util/session_api_util.js: -------------------------------------------------------------------------------- 1 | export const createUser = (user) => { 2 | 3 | return $.ajax({ 4 | method: 'POST', 5 | url: 'api/users', 6 | data: {user} 7 | }) 8 | } 9 | 10 | 11 | export const createSession = (user) => { 12 | 13 | return $.ajax({ 14 | method: 'POST', 15 | url: 'api/session', 16 | data: { user } 17 | }) 18 | } 19 | 20 | 21 | 22 | export const deleteSession = () => { 23 | 24 | return $.ajax({ 25 | method: 'DELETE', 26 | url: 'api/session', 27 | 28 | }) 29 | } 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /frontend/components/ChatRoom/MessageForm/EmojiMenu.module.css: -------------------------------------------------------------------------------- 1 | .menu { 2 | position: absolute; 3 | display: flex; 4 | background-color: #18181B; 5 | height: 200px; 6 | width: 320px; 7 | bottom: 90px; 8 | border: 1px solid black; 9 | border-radius: 7px; 10 | -webkit-box-shadow: 4px 3px 5px 0px rgba(0,0,0,1); 11 | -moz-box-shadow: 4px 3px 5px 0px rgba(0,0,0,1); 12 | box-shadow: 4px 3px 5px 0px rgba(0,0,0,1); 13 | } 14 | 15 | .menu > li { 16 | list-style: none; 17 | margin: 10px; 18 | 19 | } 20 | 21 | 22 | .emojiImg { 23 | width: 30px; 24 | height: 30px; 25 | } -------------------------------------------------------------------------------- /frontend/util/vod_api_util.js: -------------------------------------------------------------------------------- 1 | 2 | export const fetchVod = (vodId) => { 3 | return $.ajax({ 4 | method: 'GET', 5 | url: `/api/vods/${vodId}` 6 | }) 7 | } 8 | 9 | 10 | export const fetchVods = (filter) => { 11 | return $.ajax({ 12 | method: 'GET', 13 | url: '/api/vods', 14 | data: {filter} 15 | }) 16 | } 17 | 18 | 19 | export const postVod = (formData) => { 20 | 21 | return $.ajax({ 22 | method: 'POST', 23 | url: '/api/vods', 24 | data: formData, 25 | contentType: false, 26 | processData: false 27 | }) 28 | 29 | } -------------------------------------------------------------------------------- /app/channels/chat_room_channel.rb: -------------------------------------------------------------------------------- 1 | class ChatRoomsChannel < ApplicationCable::Channel 2 | 3 | def subscribed 4 | stream_for "chat_rooms_channel_#{params[:id]}" 5 | end 6 | 7 | def speak(data) 8 | message = Message.create({:user_id => data['user_id'], :body => data['message']}) 9 | broadcastMessage = {message: message.body, username: message.user.username, color: data['color']} 10 | ChatRoomsChannel.broadcast_to("chat_rooms_channel_#{data['channelName']}", broadcastMessage) 11 | end 12 | 13 | 14 | 15 | def unsubscribed 16 | # Any cleanup needed when channel is unsubscribed 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /frontend/reducers/session_errors_reducer.js: -------------------------------------------------------------------------------- 1 | import {RECEIVE_SESSION_ERRORS, CLEAR_SESSION_ERRORS} from '../actions/session_actions' 2 | import { OPEN_MODAL } from '../actions/modal_actions' 3 | 4 | 5 | 6 | const sessionsErrorsReducer = (state = {}, action) => { 7 | Object.freeze(state) 8 | 9 | switch (action.type) { 10 | case RECEIVE_SESSION_ERRORS: 11 | return Object.assign({}, action.errors.responseJSON ) 12 | case OPEN_MODAL: 13 | return {} 14 | default: 15 | return state 16 | } 17 | 18 | } 19 | 20 | 21 | 22 | export default sessionsErrorsReducer; -------------------------------------------------------------------------------- /frontend/components/Dashboard/SuccessMessage.module.css: -------------------------------------------------------------------------------- 1 | .messageWrapper { 2 | width: 500px; 3 | height: 600px; 4 | background-color: #18181B; 5 | border: 1px solid #303032; 6 | border-radius: 5px; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | 12 | 13 | .closeBtn { 14 | position: absolute; 15 | right: 20px; 16 | bottom: 20px; 17 | padding: 10px; 18 | background-color: #3A3A3D; 19 | border: none; 20 | color: whitesmoke; 21 | text-decoration: none; 22 | border-radius: 5px; 23 | font-weight: 800; 24 | font-size: 14px; 25 | font-family: 'Ubuntu', sans-serif; 26 | } -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html 3 | 4 | 5 | namespace :api, defaults: {format: :json} do 6 | resources :users, only: [:create] 7 | resource :session, only: [:create, :destroy] 8 | resources :channels, only: [:index, :show, :update] 9 | resources :vods, only: [:show, :index, :create] 10 | resources :follows, only: [:create, :destroy, :show] 11 | resources :categories, only: [:index, :show] 12 | resources :streams, only: [:create] 13 | end 14 | 15 | root to: 'static_pages#root' 16 | mount ActionCable.server, at: '/cable' 17 | end 18 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join('node_modules') 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /app/views/api/vods/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.vods do 2 | @vods.each do |vod| 3 | json.set! vod.id do 4 | json.id vod.id 5 | json.channelId vod.channel_id 6 | json.title vod.title 7 | json.category vod.category 8 | json.videoUrl url_for(vod.videoUrl) 9 | end 10 | end 11 | end 12 | 13 | 14 | json.channels do 15 | @vods.each do |vod| 16 | channel = vod.channel 17 | json.set! channel.id do 18 | json.id channel.id 19 | json.ownerId channel.owner_id 20 | json.channelName channel.channel_name 21 | json.logoUrl url_for(channel.logoUrl) 22 | end 23 | end 24 | end 25 | 26 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | 7qeNOSXaDmyuKl2YKIVX0qh+OayQdZ1nbXFBjvSs4IblAux+uak5Bsme4/eGZxEveMHBdQyMa9DpJtEjF4g3Jl6xxFlwy6CtQYtHnUi6TXw/Rvt17usf+3V/h27lkksyRJtv2RFVV/w2CGpBRHN2f2TcSxNGeqcd4Euh+Tx1LUnj0g13FL5IghNjR4lqd+AwnRa1HGCyweF1jnyYsbMXUlso33QfViqi2LIflcZgCfBLI2C1jqf3FFm5klf7J43jFQp/dCIrDJyLtPItaAwjs4mkp3xHrTgpsweGa6vPG1w1CwBw5OQuOG3Sn3hoq3j3XVuSmeQQMTw20Lc+HHkkZ5VXFVNKbPyt21rvOpRrsOcEpjy7P+PcBwvosFqkMD14AHjVdHNr989D4sPEt6RhXtvcxbbJzhsCIMwBLQPr/O0F2Y6X+LLAU4vlTv/kqoSHn+vVc1rov9MvIXMwpyB2sl8bVFKvebnFQcis49jSeTfxgcI0QI6YM3i1qHpQQcaq4XgZOOOkayakys+LgUlrADMZkOIdqL4pEe2zpYk2CM3iy/HQBuqp7GHUgU2rVC0N7CFdY9jiloiI+gq7sr/Qgh2L0dOogBp5GjFKrOS4aEdRkqYUUBb7ZAvmXXnIjSY7a3EKTPDzXVkk34wPWAdRixa9t0yalQqTviI=--dTIcjpoRl+XTFzIQ--WmHk5R2dHUaNTbVCh2NX3w== -------------------------------------------------------------------------------- /app/views/api/channels/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | json.channels do 3 | @channels.each do |channel| 4 | json.set! channel.id do 5 | json.id channel.id 6 | json.ownerId channel.owner_id 7 | json.channelName channel.channel_name 8 | json.logoUrl url_for(channel.logoUrl) 9 | end 10 | end 11 | end 12 | 13 | 14 | json.users do 15 | @channels.each do |channel| 16 | owner = channel.owner 17 | json.set! owner.id do 18 | json.id owner.id 19 | json.username owner.username 20 | json.channelId channel.id 21 | json.follows owner.followed_channels.map{|channel| channel.id } 22 | 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /frontend/components/Channels/ChannelShow/ChannelNavs.module.css: -------------------------------------------------------------------------------- 1 | .tabsWrapper { 2 | display: flex; 3 | width: 300px; 4 | justify-content: center; 5 | align-items: center; 6 | box-sizing: border-box; 7 | } 8 | 9 | .tabsWrapper > li { 10 | display: flex; 11 | color: white; 12 | list-style: none; 13 | margin-right: 20px; 14 | box-sizing: border-box; 15 | cursor: pointer; 16 | border-bottom: 2px solid #18181B; 17 | 18 | } 19 | 20 | .tabSelected { 21 | box-sizing: border-box; 22 | border-bottom: 2px solid #9147FF !important; 23 | } 24 | .tabSelected > p { 25 | color: #9147FF 26 | } 27 | 28 | 29 | .tabTitle { 30 | box-sizing: border-box; 31 | margin-bottom: 5px; 32 | } 33 | 34 | -------------------------------------------------------------------------------- /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 Fidget 10 | class Application < Rails::Application 11 | # Initialize configuration defaults for originally generated Rails version. 12 | config.load_defaults 5.2 13 | 14 | # Settings in config/environments/* take precedence over those specified here. 15 | # Application configuration can go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded after loading 17 | # the framework and any gems in your application. 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /frontend/util/stream_util.js: -------------------------------------------------------------------------------- 1 | export const JOIN_CALL = 'JOIN_CALL' 2 | export const BROADCAST = 'BROADCAST' 3 | export const OFFER = 'OFFER' 4 | export const ANSWER = 'ANSWER' 5 | export const CANDIDATE = 'CANDIDATE' 6 | export const WATCHER = 'WATCHER' 7 | export const PEER_DISCONNECT = 'PEER_DISCONNECT' 8 | export const EXCHANGE = 'EXCHANGE' 9 | export const LEAVE_CALL = 'LEAVE_CALL' 10 | 11 | 12 | export const ice = { 13 | iceServers: [ 14 | { 15 | urls: 'stun:stun2.l.google.com:19302', 16 | }, 17 | ], 18 | } 19 | 20 | export const broadcastData = (data) => { 21 | fetch('api/streams', { 22 | method: 'POST', 23 | body: JSON.stringify(data), 24 | headers: { 'content-type': 'application/json' }, 25 | }, 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /frontend/components/Dashboard/SuccessMessage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { closeModal } from '../../actions/modal_actions' 4 | import styles from './SuccessMessage.module.css' 5 | /* eslint-disable */ 6 | 7 | const SuccessMessage = ({ closeModalOnClick }) => { 8 | 9 | return ( 10 |
11 |

Successful Upload

12 | 13 |
14 | ) 15 | } 16 | 17 | 18 | const mDTP = (dispatch) => { 19 | return { 20 | closeModalOnClick: (comp) => dispatch(closeModal(comp)), 21 | } 22 | } 23 | 24 | 25 | export default connect(null, mDTP)(SuccessMessage) 26 | -------------------------------------------------------------------------------- /app/controllers/api/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SessionsController < ApplicationController 2 | 3 | before_action :require_logged_in, only: [:destroy] 4 | 5 | def create 6 | 7 | # returns [user(possibly nil), usernameError or passwordError] 8 | queryResult = User.find_by_credentials( 9 | params[:user][:username], 10 | params[:user][:password] 11 | ) 12 | 13 | @user = queryResult[0] 14 | 15 | if @user 16 | login!(@user) 17 | render 'api/users/user' 18 | else 19 | render json: queryResult[1], status: 401 20 | end 21 | end 22 | 23 | 24 | def destroy 25 | logout! 26 | render json: {} 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/controllers/api/vods_controller.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | 3 | class Api::VodsController < ApplicationController 4 | 5 | def index 6 | @vods = Vod.all_filter(params[:filter]) 7 | render :index 8 | end 9 | 10 | def show 11 | 12 | @vod = Vod.find_by(id: params[:id]) 13 | 14 | render :show 15 | end 16 | 17 | def create 18 | 19 | @vod = Vod.new(vod_params) 20 | if @vod.save 21 | render :show 22 | else 23 | render json: @vod.errors.full_messages, status: 422 24 | end 25 | 26 | end 27 | 28 | 29 | private 30 | 31 | def vod_params 32 | params.require(:vod).permit(:channel_id, :title, :category, :videoUrl) 33 | 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /frontend/components/ChatRoom/MessageForm/EmojiMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 3 | import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' 4 | import classes from './EmojiMenu.module.css' 5 | 6 | 7 | class EmojiMenu extends React.Component { 8 | render() { 9 | 10 | return ( 11 | <> 12 | { 13 | this.props.show ? ( 14 | 19 | ) : null 20 | 21 | } 22 | 23 | ) 24 | } 25 | 26 | 27 | } 28 | 29 | 30 | export default EmojiMenu 31 | -------------------------------------------------------------------------------- /frontend/reducers/sessions_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_CURRENT_USER, LOGOUT_CURRENT_USER, RECEIVE_NEW_USER} from '../actions/session_actions' 2 | 3 | const _nullSession = { 4 | currentUserId: null 5 | } 6 | 7 | 8 | const sessionsReducer = (state = _nullSession, action) => { 9 | Object.freeze(state) 10 | 11 | switch (action.type) { 12 | case RECEIVE_CURRENT_USER: 13 | return Object.assign({}, {currentUserId: action.user.id}); 14 | case RECEIVE_NEW_USER: 15 | return Object.assign({}, { currentUserId: Object.keys(action.payload.user)[0] }); 16 | case LOGOUT_CURRENT_USER: 17 | return _nullSession 18 | default: 19 | return state 20 | } 21 | 22 | 23 | } 24 | 25 | 26 | export default sessionsReducer; -------------------------------------------------------------------------------- /frontend/util/channels_api_util.js: -------------------------------------------------------------------------------- 1 | 2 | export const fetchChannels = (filter) => { 3 | 4 | return $.ajax({ 5 | method: 'GET', 6 | url: 'api/channels', 7 | data: {filter} 8 | }) 9 | } 10 | 11 | export const fetchChannel = (channelId) => { 12 | return $.ajax({ 13 | method: 'GET', 14 | url: `api/channels/${channelId}` 15 | }) 16 | 17 | } 18 | 19 | // Will find by channelOwnerId instead of channelId since channelOwnerId is more easily acsesible 20 | export const updateChannel = (channelOwnerId, formData) => { 21 | 22 | return $.ajax({ 23 | method: 'PATCH', 24 | url: `/api/channels/${channelOwnerId}`, 25 | data: formData, 26 | contentType: false, 27 | processData: false 28 | }) 29 | 30 | } 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's 5 | // vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require rails-ujs 15 | //= require activestorage 16 | //= require_tree . 17 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Fidget 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | 10 | 11 | <%= favicon_link_tag asset_path('favicon.png') %> 12 | <%= stylesheet_link_tag 'application', media: 'all' %> 13 | <%= stylesheet_link_tag 'application', 'https://fonts.googleapis.com/css2?family=Ubuntu:wght@700&display=swap', media: 'all' %> 14 | <%= javascript_include_tag 'application' %> 15 | 16 | 17 | 18 | <%= yield %> 19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend/components/Session/login_container_component.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { login, clearErrors } from '../../actions/session_actions' 3 | import SessionForm from './SessionForm' 4 | import { openModal, closeModal } from '../../actions/modal_actions' 5 | /* eslint-disable */ 6 | 7 | const mSTP = (state) => { 8 | return { 9 | formType: 'Login', 10 | otherForm: 'Sign Up', 11 | errors: state.errors.session, 12 | } 13 | } 14 | 15 | 16 | const mDTP = (dispatch) => { 17 | 18 | return { 19 | processForm: (user) => dispatch(login(user)), 20 | navToOtherForm: () => dispatch(openModal('signup')), 21 | closeModal: () => dispatch(closeModal()), 22 | clearErrors: () => dispatch(clearErrors()), 23 | } 24 | } 25 | 26 | 27 | export default connect(mSTP, mDTP)(SessionForm) 28 | -------------------------------------------------------------------------------- /frontend/components/HomePageIndex/HomePageIndex.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classes from './HomePageIndex.module.css' 3 | 4 | import Categories from '../Categories/Categories' 5 | import ChannelIndex from '../Channels/ChannelIndex' 6 | import Carousel from '../Carousel/Carousel' 7 | 8 | const HomePageIndex = () => { 9 | 10 | return ( 11 |
12 | 13 |

Top Channels

14 |
15 |
16 | 17 |
18 |

19 | Categories 20 | You May Like 21 |

22 | 23 |
24 |
25 | ) 26 | } 27 | 28 | 29 | export default HomePageIndex 30 | -------------------------------------------------------------------------------- /frontend/components/Session/SignupForm/signup_container_component.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { signup, clearErrors } from '../../../actions/session_actions' 3 | import { openModal, closeModal } from '../../../actions/modal_actions' 4 | import SignupForm from './SignupForm' 5 | /* eslint-disable */ 6 | 7 | const mSTP = (state) => { 8 | 9 | return { 10 | formType: 'Sign Up', 11 | otherForm: 'Login', 12 | errors: state.errors.session, 13 | } 14 | } 15 | 16 | 17 | const mDTP = (dispatch) => { 18 | 19 | return { 20 | processForm: (user) => dispatch(signup(user)), 21 | navToOtherForm: () => dispatch(openModal('login')), 22 | closeModal: () => dispatch(closeModal()), 23 | clearErrors: () => dispatch(clearErrors()), 24 | } 25 | } 26 | 27 | 28 | export default connect(mSTP, mDTP)(SignupForm) 29 | -------------------------------------------------------------------------------- /.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 uploaded files in development 17 | /storage/* 18 | !/storage/.keep 19 | 20 | /node_modules 21 | /yarn-error.log 22 | 23 | /public/assets 24 | .byebug_history 25 | 26 | # Ignore master key for decrypting credentials and more. 27 | /config/master.key 28 | 29 | 30 | 31 | 32 | node_modules/ 33 | bundle.js* 34 | /public/assets 35 | .byebug_history 36 | .DS_Store 37 | npm-debug.log -------------------------------------------------------------------------------- /app/controllers/api/streams_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::StreamsController < ApplicationController 2 | 3 | def create 4 | head :no_content 5 | ActionCable.server.broadcast("stream_channel", call_params) 6 | end 7 | 8 | private 9 | def call_params 10 | permittedParams = params.permit(:call, :type, :id, :to, :stream, :sdp) 11 | if (params[:candidate]) 12 | permittedParams['candidate'] = params[:candidate] 13 | end 14 | if (params[:description]) 15 | permittedParams['description'] = params[:description] 16 | end 17 | return permittedParams 18 | end 19 | 20 | end 21 | 22 | 23 | # candidate: [ 24 | # :candidate, 25 | # :sdpMid, 26 | # :sdpMLineIndex, 27 | # :foundation, 28 | # :component, 29 | # :address, 30 | # :protocol, 31 | # :type, 32 | # :usernameFragment 33 | # ], -------------------------------------------------------------------------------- /frontend/actions/category_actions.js: -------------------------------------------------------------------------------- 1 | import * as CategoryAPIUtil from '../util/categories_api_util' 2 | 3 | export const RECEIVE_CATEGORIES = 'RECEIVE_CATEGORIES' 4 | export const RECEIVE_CATEGORY = 'RECEIVE_CATEGORY' 5 | 6 | 7 | const receiveCategory = (category) => { 8 | return { 9 | type: RECEIVE_CATEGORY, 10 | category, 11 | } 12 | } 13 | 14 | 15 | const receiveCategories = (categories) => { 16 | return { 17 | type: RECEIVE_CATEGORIES, 18 | categories, 19 | } 20 | } 21 | 22 | 23 | export const requestCategories = () => (dispatch) => { 24 | return CategoryAPIUtil.fetchCategories() 25 | .then((categories) => dispatch(receiveCategories(categories))) 26 | } 27 | 28 | export const requestCategory = (name) => (dispatch) => { 29 | return CategoryAPIUtil.fetchCategory(name) 30 | .then((category) => dispatch(receiveCategory(category))) 31 | } 32 | -------------------------------------------------------------------------------- /frontend/components/Vods/VodsIndex/VodIndexItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withRouter } from 'react-router-dom' 3 | import classes from './ChannelVideosIndex.module.css' 4 | /* eslint-disable */ 5 | 6 | const VodIndexItem = ({ vod, match, history, channel }) => { 7 | 8 | const navToVodShow = () => { 9 | const channelName = match.params.channelName || channel.channelName 10 | history.push(`/channels/${vod.channelId}/${channelName}/videos/${vod.id}`) 11 | } 12 | 13 | return ( 14 |
15 | 18 |

{vod.title}

19 |
{match.params.channelName}
20 |
{vod.category}
21 |
22 | ) 23 | } 24 | 25 | 26 | export default withRouter(VodIndexItem) 27 | -------------------------------------------------------------------------------- /frontend/util/route_util.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import {Route, Redirect, withRouter} from 'react-router-dom' 4 | 5 | 6 | 7 | 8 | const Auth = ({component: Component, path, loggedIn}) => { 9 | 10 | 11 | 12 | return ( 13 | <> 14 | { 15 | loggedIn ? ( 16 | } /> 19 | ) : ( 20 | 21 | ) 22 | } 23 | 24 | 25 | ) 26 | } 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | const mSTP = state => { 36 | return { 37 | loggedIn: Boolean(state.session.currentUserId) 38 | } 39 | } 40 | 41 | 42 | export default withRouter(connect(mSTP, null)(Auth)) -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | 3 | class Api::UsersController < ApplicationController 4 | 5 | def create 6 | 7 | 8 | newParams = user_params 9 | newParams["dob"] = params[:user][:dob].to_i 10 | @user = User.new(newParams) 11 | 12 | if @user.save 13 | login!(@user) 14 | @channel = Channel.create({:owner_id => @user.id, :channel_name => @user.username}) 15 | file = open('https://fidget-seeds.s3-us-west-1.amazonaws.com/defaultlogo2.png') 16 | @channel.logoUrl.attach(io: file, filename: 'defaultlogo2.png') 17 | render :new_user 18 | else 19 | render json: @user.errors.full_messages, status: 422 20 | end 21 | 22 | end 23 | 24 | 25 | private 26 | def user_params 27 | params.require(:user).permit(:username, :password, :email, :dob) 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a way to update your development environment automatically. 14 | # Add necessary update steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | puts "\n== Updating database ==" 24 | system! 'bin/rails db:migrate' 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! 'bin/rails log:clear tmp:clear' 28 | 29 | puts "\n== Restarting application server ==" 30 | system! 'bin/rails restart' 31 | end 32 | -------------------------------------------------------------------------------- /frontend/components/ChannelFollowers/ChannelFollowers.module.css: -------------------------------------------------------------------------------- 1 | 2 | .followersWrapper { 3 | display: flex; 4 | flex-wrap: wrap; 5 | justify-content: center; 6 | margin-left: 10px; 7 | padding-top: 50px; 8 | } 9 | .channelWrapper { 10 | width: 20vw; 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | transform-style: preserve-3d; 15 | -webkit-transition: transform .2s linear; 16 | -moz-transition: transform .2s linear; 17 | -o-transition: transform .2s linear; 18 | transition: transform .2s linear; 19 | 20 | } 21 | 22 | .channelWrapper > img { 23 | border-radius: 5px; 24 | width: 80%; 25 | 26 | } 27 | 28 | .channelWrapper:hover { 29 | -webkit-transform: scale(1.2); 30 | -moz-transform: scale(1.2); 31 | -o-transform: scale(1.2); 32 | transform: scale(1.2); 33 | } 34 | 35 | .channelWrapper > span { 36 | padding-top: 5px; 37 | color: white; 38 | font-size: 25px; 39 | } -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at http://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /frontend/components/SessionControls/session_controls_container.js: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux' 2 | import SessionControls from './SessionControls' 3 | import { logout } from '../../actions/session_actions' 4 | import { openModal } from '../../actions/modal_actions' 5 | import { requestChannel} from '../../actions/channel_actions' 6 | 7 | 8 | const mSTP = (state) => { 9 | const currentUser = state.entities.users[state.session.currentUserId] 10 | let currentChannel; 11 | currentUser ? currentChannel = state.entities.channels[currentUser.channelId] : currentChannel = null 12 | 13 | return { 14 | currentUser: currentUser, 15 | currentChannel: currentChannel 16 | 17 | } 18 | } 19 | 20 | 21 | 22 | const mDTP = dispatch => { 23 | 24 | return { 25 | logout: () => dispatch(logout()), 26 | openModal: (form) => dispatch(openModal(form)), 27 | requestChannel: (channelId) => dispatch(requestChannel(channelId)) 28 | } 29 | } 30 | 31 | 32 | export default connect(mSTP, mDTP)(SessionControls) -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | 3 | protect_from_forgery unless: -> { request.format.json? } 4 | helper_method :current_user, :logged_in?, :demo_user 5 | 6 | def current_user 7 | @current_user ||= User.find_by(session_token: session[:session_token]) 8 | end 9 | 10 | def demo_user 11 | @demo_user = User.find_by(username: 'FidgetDemoUser') 12 | end 13 | 14 | def logged_in? 15 | !!current_user 16 | end 17 | 18 | def login!(user) 19 | # token = user.reset_session_token! 20 | # cookies.signed[:session_token] = token 21 | # session[:session_token] = cookies.signed[:session_token] 22 | 23 | session[:session_token] = user.reset_session_token! 24 | end 25 | 26 | def logout! 27 | current_user.reset_session_token! 28 | session[:session_token] = nil 29 | end 30 | 31 | 32 | def require_logged_in 33 | render json: ['Must Logged In'] unless logged_in? 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /frontend/reducers/vods_reducer.js: -------------------------------------------------------------------------------- 1 | import {RECEIVE_VOD, RECEIVE_VODS, RECEIVE_RANDOM_VODS, CLEAR_VODS} from '../actions/vod_actions' 2 | import { RECEIVE_CHANNELS } from '../actions/channel_actions' 3 | 4 | 5 | 6 | const vodsReducer = (state = {}, action) => { 7 | Object.freeze(state) 8 | 9 | switch (action.type) { 10 | case RECEIVE_VOD: 11 | return Object.assign({}, state, action.vod) 12 | case RECEIVE_VODS: 13 | return Object.assign({}, action.payload.vods ) 14 | case RECEIVE_RANDOM_VODS: 15 | let newState = Object.assign({}, state) 16 | newState['randomVods'] = action.payload.vods 17 | return newState 18 | case RECEIVE_CHANNELS: 19 | if (action.payload.vods) { 20 | return Object.assign({}, state, action.payload.vods) 21 | }else { 22 | return state 23 | } 24 | case CLEAR_VODS: 25 | return {} 26 | default: 27 | return state 28 | } 29 | } 30 | 31 | 32 | 33 | export default vodsReducer; -------------------------------------------------------------------------------- /app/controllers/api/follows_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::FollowsController < ApplicationController 2 | before_action :require_logged_in, only: [:destroy, :create] 3 | 4 | 5 | def show 6 | @followers = Follow.where("channel_id = ?", params[:id]).includes(:user).includes(:channel).references(:user).references(:channel) 7 | render :show 8 | end 9 | 10 | 11 | def create 12 | @follow = Follow.new(follow_params) 13 | 14 | if @follow.save 15 | render :follow 16 | else 17 | render json: @follow.errors.full_messages, status: 422 18 | end 19 | end 20 | 21 | 22 | 23 | def destroy 24 | @follow = Follow.where(channel_id: params[:id], user_id: current_user.id)[0] 25 | if @follow 26 | Follow.destroy(@follow.id) 27 | render :follow 28 | else 29 | render json: ["Follow does not exist"] 30 | end 31 | end 32 | 33 | 34 | private 35 | def follow_params 36 | params.require(:follow).permit(:channel_id, :user_id) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /frontend/components/SideBar/SideBarItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withRouter } from 'react-router-dom' 3 | import classes from './SideBar.module.css' 4 | /* eslint-disable */ 5 | 6 | const SideBarItem = ({ channel, users, history }) => { 7 | 8 | const navToChannel = () => { 9 | if (users && channel) { 10 | history.push(`/channels/${channel.id}/${users[channel.ownerId].username}/home`) 11 | } 12 | } 13 | 14 | return ( 15 |
16 | 17 |
18 | { 19 | // First request returns specific channel info and user info. Need to wait for second request to get followed channels/users for sidebar 20 | Object.keys(users).length > 1 && users[channel.ownerId] ? ( 21 |

{users[channel.ownerId].username}

) : null 22 | } 23 |
24 |
Offline
25 |
26 | ) 27 | } 28 | 29 | 30 | export default withRouter(SideBarItem) 31 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a starting point to setup your application. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | # puts "\n== Copying sample files ==" 24 | # unless File.exist?('config/database.yml') 25 | # cp 'config/database.yml.sample', 'config/database.yml' 26 | # end 27 | 28 | puts "\n== Preparing database ==" 29 | system! 'bin/rails db:setup' 30 | 31 | puts "\n== Removing old logs and tempfiles ==" 32 | system! 'bin/rails log:clear tmp:clear' 33 | 34 | puts "\n== Restarting application server ==" 35 | system! 'bin/rails restart' 36 | end 37 | -------------------------------------------------------------------------------- /frontend/components/Vods/VodShow/VodShow.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { withRouter } from 'react-router-dom' 4 | import { requestVod } from '../../../actions/vod_actions' 5 | import classes from './VodShow.module.css' 6 | /* eslint-disable */ 7 | 8 | class VodShow extends React.Component { 9 | 10 | componentDidMount() { 11 | this.props.requestVod(this.props.match.params.vodId) 12 | } 13 | 14 | render() { 15 | return ( 16 | <> 17 | { 18 | this.props.vod ? ( 19 | 22 | ) : null 23 | } 24 | 25 | ) 26 | } 27 | } 28 | 29 | 30 | const mSTP = (state, ownProps) => { 31 | return { 32 | vod: state.entities.vods[ownProps.match.params.vodId], 33 | } 34 | } 35 | 36 | 37 | const mDTP = (dispatch) => { 38 | return { 39 | requestVod: (vodId) => dispatch(requestVod(vodId)), 40 | } 41 | } 42 | 43 | 44 | export default withRouter(connect(mSTP, mDTP)(VodShow)) 45 | -------------------------------------------------------------------------------- /db/migrate/20200501081013_create_active_storage_tables.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20170806125915) 2 | class CreateActiveStorageTables < ActiveRecord::Migration[5.2] 3 | def change 4 | create_table :active_storage_blobs do |t| 5 | t.string :key, null: false 6 | t.string :filename, null: false 7 | t.string :content_type 8 | t.text :metadata 9 | t.bigint :byte_size, null: false 10 | t.string :checksum, null: false 11 | t.datetime :created_at, null: false 12 | 13 | t.index [ :key ], unique: true 14 | end 15 | 16 | create_table :active_storage_attachments do |t| 17 | t.string :name, null: false 18 | t.references :record, null: false, polymorphic: true, index: false 19 | t.references :blob, null: false 20 | 21 | t.datetime :created_at, null: false 22 | 23 | t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true 24 | t.foreign_key :active_storage_blobs, column: :blob_id 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /frontend/components/HomePageIndex/HomePageIndex.module.css: -------------------------------------------------------------------------------- 1 | .indexWrapper { 2 | flex-grow: 6.0; 3 | display: flex; 4 | flex-direction: column; 5 | background-color: #0E0E10; 6 | height: 100vh; 7 | overflow-y: scroll; 8 | 9 | } 10 | 11 | 12 | .indexWrapper > h2 { 13 | padding-top: 20px; 14 | margin-left: 40px; 15 | color: white; 16 | 17 | } 18 | 19 | .indexWrapper > hr { 20 | width: 90%; 21 | margin-top: 30px; 22 | margin-bottom: 40px; 23 | margin-left: 40px; 24 | background-color: rgb(48,48,50); 25 | border: .5px solid rgb(48,48,50) ; 26 | } 27 | 28 | .innerContainer{ 29 | display: flex; 30 | flex-direction: column; 31 | align-items: flex-start; 32 | width: 100%; 33 | padding-bottom: 200px; 34 | margin-left: 40px; 35 | } 36 | 37 | .innerContainer > hr { 38 | width: 90%; 39 | margin-top: 30px; 40 | margin-bottom: 30px; 41 | background-color: rgb(48,48,50); 42 | border: .5px solid rgb(48,48,50) ; 43 | } 44 | 45 | .categoriesHeader { 46 | color: white; 47 | margin-bottom: 5px; 48 | } 49 | 50 | 51 | .categoriesHeader > b { 52 | color: #0CD6F8; 53 | } 54 | -------------------------------------------------------------------------------- /app/models/vod.rb: -------------------------------------------------------------------------------- 1 | class Vod < ApplicationRecord 2 | validates :channel_id, :title, :category, presence: true 3 | 4 | belongs_to :channel 5 | 6 | has_one_attached :videoUrl 7 | 8 | # filter --> {:filterType => value} 9 | def self.all_filter(filter) 10 | 11 | if !filter[:channel_id].nil? 12 | newFilter = {:channel_id => filter[:channel_id].to_i} 13 | return Vod.where("channel_id = :channel_id", newFilter) 14 | elsif !filter[:category].nil? 15 | newFilter = {:category => filter[:category]} 16 | return Vod.where("category = :category", newFilter) 17 | elsif !filter[:random].nil? 18 | count = Vod.count + 1 19 | ids = [] 20 | vods = [] 21 | while ids.length < 5 22 | randomId = rand(count) 23 | if randomId != 0 && !ids.include?(randomId) 24 | ids.push(randomId) 25 | end 26 | end 27 | ids.each do |id| 28 | vods.push(Vod.where("id = ?", "#{id}")[0]) 29 | end 30 | return vods 31 | end 32 | 33 | return Vod.all 34 | 35 | end 36 | 37 | end 38 | 39 | 40 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | 19 | # If you are using UJS then enable automatic nonce generation 20 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 21 | 22 | # Report CSP violations to a specified URI 23 | # For further information see the following documentation: 24 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 25 | # Rails.application.config.content_security_policy_report_only = true 26 | -------------------------------------------------------------------------------- /frontend/components/About/AboutPage.module.css: -------------------------------------------------------------------------------- 1 | .aboutWrapper { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | flex-grow: 6; 6 | background-color: #18181B; 7 | height: 100vh; 8 | overflow-y: scroll; 9 | } 10 | 11 | .mostOuter { 12 | padding-bottom: 500px; 13 | } 14 | 15 | .outer { 16 | margin-top: 450px ; 17 | display: flex; 18 | flex-direction: column; 19 | width: 600px; 20 | height: 700px; 21 | background-color: #DADCE0; 22 | align-items: center; 23 | border-radius: 5px; 24 | -webkit-box-shadow: 8px 6px 10px 0px rgba(0,0,0,1); 25 | -moz-box-shadow: 8px 6px 10px 0px rgba(0,0,0,1); 26 | box-shadow: 8px 6px 10px 0px rgba(0,0,0,1); 27 | 28 | } 29 | 30 | .outer > h1 { 31 | margin-top: 15px; 32 | margin-bottom: 5px; 33 | color: black; 34 | } 35 | 36 | .outer > h2 { 37 | margin-bottom: 15px; 38 | } 39 | 40 | 41 | .linkWrapper { 42 | display: flex; 43 | width: 80%; 44 | justify-content: space-evenly; 45 | 46 | } 47 | .linkWrapper > li { 48 | list-style: none; 49 | } 50 | 51 | .icon { 52 | width: 60px; 53 | height: 60px; 54 | cursor: pointer; 55 | } 56 | 57 | .picture { 58 | width: 80%; 59 | margin-top: 40px; 60 | border-radius: 8px; 61 | } -------------------------------------------------------------------------------- /frontend/components/NavBar/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import classes from './NavBar.module.css' 4 | import SessionControlsContainer from '../SessionControls/session_controls_container' 5 | import SearchBar from './SearchBar' 6 | 7 | /* eslint-disable */ 8 | 9 | const NavBar = () => { 10 | 11 | return ( 12 | 34 | ) 35 | } 36 | 37 | 38 | export default NavBar 39 | -------------------------------------------------------------------------------- /frontend/components/Channels/ChannelIndexItem/ChannelIndexItem.module.css: -------------------------------------------------------------------------------- 1 | .channelItem { 2 | list-style: none; 3 | width: 350px; 4 | height: 195px; 5 | background-color: #27262C; 6 | margin-right: 20px; 7 | color: white; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | /* border-radius: 7px; */ 12 | align-items: center; 13 | /* -webkit-box-shadow: 4px 3px 5px 0px rgba(145,71,255,1); 14 | -moz-box-shadow: 4px 3px 5px 0px rgba(145,71,255,1); 15 | box-shadow: 4px 3px 5px 0px rgba(145,71,255,1); */ 16 | } 17 | 18 | .icon { 19 | width: 50px; 20 | height: 50px; 21 | margin-top: 10px; 22 | border-radius: 50%; 23 | } 24 | 25 | .videoPlayer { 26 | 27 | width: 350px; 28 | cursor: pointer; 29 | margin: 0; 30 | padding: 0; 31 | } 32 | 33 | .videoPlayer:hover { 34 | border: 2px solid #0CD6F8; 35 | } 36 | 37 | .channelDetailsWrapper { 38 | display: flex; 39 | align-items: center; 40 | margin-bottom: 40px; 41 | 42 | } 43 | 44 | .details > h4 { 45 | color: white; 46 | } 47 | 48 | .details > h5 { 49 | color: #8F8F98; 50 | } 51 | 52 | .details { 53 | margin-top: 10px; 54 | margin-left: 10px; 55 | } -------------------------------------------------------------------------------- /frontend/components/Channels/ChannelIndexItem/ChannelIndexItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import classes from './ChannelIndexItem.module.css' 4 | /* eslint-disable */ 5 | 6 | const ChannelIndexItem = ({ user, vod, channel }) => { 7 | return ( 8 |
9 | {user ? ( 10 | 11 |
12 | { 13 | vod.videoUrl ? ( 14 | ) : ( ) 17 | } 18 |
19 |
20 | 21 |
22 |

{vod.title}

23 |
{user.username}
24 |
{vod.category}
25 |
26 |
27 | ) : null 28 | } 29 |
30 | ) 31 | } 32 | 33 | 34 | 35 | 36 | 37 | export default ChannelIndexItem -------------------------------------------------------------------------------- /frontend/components/App.module.css: -------------------------------------------------------------------------------- 1 | .mainNav { 2 | background-color: #18181B; 3 | /* background-color: whitesmoke; */ 4 | height: 55px; 5 | display: flex; 6 | justify-content: space-between; 7 | align-items: center; 8 | box-shadow: 3px 3px 8px #888888; 9 | border-bottom: 2px solid #0E0E10; 10 | width: 100%; 11 | flex-shrink: 0 12 | } 13 | 14 | 15 | .leftNav { 16 | margin-left: 20px; 17 | display: flex; 18 | align-items: center; 19 | 20 | } 21 | 22 | .rightNav { 23 | margin-right: 20px; 24 | } 25 | 26 | .appTitle { 27 | color: whitesmoke; 28 | } 29 | 30 | .mainContainer { 31 | display: flex; 32 | height: 100%; 33 | /* overflow-y: visible; */ 34 | } 35 | .mainContainer2 { 36 | height: 100%; 37 | /* overflow-y: visible; */ 38 | } 39 | 40 | 41 | .browseContainer { 42 | display: flex; 43 | height: 55px; 44 | margin-left: 20px; 45 | align-items: center; 46 | justify-content: center; 47 | border-bottom: 2px solid #A970FF; 48 | cursor: pointer; 49 | } 50 | 51 | .browseContainer > span { 52 | color: #A970FF; 53 | font-size: 19px; 54 | font-weight: 540; 55 | 56 | } 57 | .line{ 58 | height: 40px; 59 | border-left: .5px solid rgb(48,48,50); 60 | margin-left: 20px; 61 | 62 | } 63 | -------------------------------------------------------------------------------- /frontend/components/MainNav/MainNav.module.css: -------------------------------------------------------------------------------- 1 | .directoryWrapper { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: flex-start; 5 | padding-left: 30px; 6 | padding-top: 40px; 7 | background-color: #0E0E10; 8 | flex-grow: 6; 9 | height: 100vh; 10 | overflow-y: scroll; 11 | } 12 | 13 | .outer { 14 | padding-bottom: 500px; 15 | } 16 | 17 | .directoryWrapper > h1 { 18 | margin-bottom: 30px; 19 | font-size: 50px; 20 | } 21 | 22 | 23 | .tabsWrapper { 24 | display: flex; 25 | width: 300px; 26 | justify-content: flex-start; 27 | align-items: center; 28 | box-sizing: border-box; 29 | margin-bottom: 100px; 30 | margin-top: 20px; 31 | } 32 | 33 | .tabsWrapper > li { 34 | display: flex; 35 | color: white; 36 | list-style: none; 37 | margin-right: 20px; 38 | font-weight: 700; 39 | font-size: 19px; 40 | box-sizing: border-box; 41 | cursor: pointer; 42 | border-bottom: 2px solid #18181B; 43 | 44 | } 45 | 46 | .tabSelected { 47 | box-sizing: border-box; 48 | border-bottom: 2px solid #9147FF !important; 49 | } 50 | .tabSelected > p { 51 | color: #9147FF 52 | } 53 | 54 | 55 | .tabTitle { 56 | box-sizing: border-box; 57 | margin-bottom: 5px; 58 | } 59 | 60 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | 17 | * { 18 | margin: 0; 19 | } 20 | 21 | h4, h2, li, ul, nav, div { 22 | margin: 0; 23 | padding: 0; 24 | } 25 | h1{ 26 | color: white; 27 | } 28 | 29 | a { 30 | text-decoration: none; 31 | 32 | 33 | } 34 | html { 35 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 36 | /* font-family: 'Ubuntu', sans-serif; */ 37 | -webkit-font-smoothing: antialiased; 38 | } 39 | 40 | body { 41 | height: 200%; 42 | overflow-y: hidden; 43 | } 44 | 45 | -------------------------------------------------------------------------------- /frontend/components/Channels/ChannelIndex.module.css: -------------------------------------------------------------------------------- 1 | .indexWrapper { 2 | flex-grow: 6.0; 3 | display: flex; 4 | flex-direction: column; 5 | box-sizing: border-box; 6 | 7 | /* flex-wrap: wrap; */ 8 | background-color: #0E0E10; 9 | /* justify-content: center; */ 10 | overflow-y: scroll; 11 | height: 100vh; 12 | padding-bottom: 800px; 13 | } 14 | 15 | .indexWrapper > h1 { 16 | padding-top: 200px; 17 | margin-left: 40px; 18 | 19 | } 20 | 21 | .indexWrapper > hr { 22 | width: 90%; 23 | margin-top: 30px; 24 | margin-bottom: 40px; 25 | margin-left: 40px; 26 | background-color: rgb(48,48,50); 27 | height: .5px; 28 | border: none; 29 | } 30 | 31 | .innerContainer{ 32 | display: flex; 33 | flex-direction: column; 34 | align-items: flex-start; 35 | width: 100%; 36 | overflow-y: scroll; 37 | /* padding-bottom: 200px; */ 38 | margin-left: 41px; 39 | } 40 | 41 | .innerContainer > hr { 42 | width: 90%; 43 | margin-top: 30px; 44 | margin-bottom: 30px; 45 | background-color: rgb(48,48,50); 46 | border: .5px solid rgb(48,48,50) ; 47 | } 48 | 49 | 50 | .channelsContainer { 51 | display: flex; 52 | flex-wrap: wrap; 53 | /* height: 100vh; 54 | overflow-y: scroll; */ 55 | } 56 | 57 | -------------------------------------------------------------------------------- /frontend/components/MainPage/MainPage.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import React from 'react' 4 | import { Route, Switch } from 'react-router-dom' 5 | import classes from './MainPage.module.css' 6 | 7 | import AuthRoute from '../../util/route_util' 8 | import ChannelShow from '../Channels/ChannelShow/ChannelShow' 9 | import CategoryShow from '../Categories/CategoryShow' 10 | import Dashboard from '../Dashboard/Dashboard' 11 | import SideBar from '../SideBar/SideBar' 12 | import HomePageIndex from '../HomePageIndex/HomePageIndex' 13 | import MainNav from '../MainNav/MainNav' 14 | import AboutPage from '../About/AboutPage' 15 | 16 | const MainPage = () => { 17 | 18 | 19 | return ( 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 | 33 | ) 34 | } 35 | 36 | 37 | export default MainPage 38 | -------------------------------------------------------------------------------- /frontend/components/Categories/CategoryShow.module.css: -------------------------------------------------------------------------------- 1 | .categoryWrapper { 2 | display: flex; 3 | flex-direction: column; 4 | background-color: #0E0E10; 5 | flex-grow: 6; 6 | overflow-x: hidden; 7 | overflow-y: scroll; 8 | height: 100vh; 9 | 10 | } 11 | 12 | .banner { 13 | display: flex; 14 | flex-direction: column; 15 | } 16 | 17 | .infoArea { 18 | display: flex; 19 | margin: 40px; 20 | } 21 | 22 | .description { 23 | display: flex; 24 | flex-direction: column; 25 | margin-left: 30px; 26 | } 27 | 28 | .description > h5 { 29 | margin-top: 15px; 30 | color:#75757C; 31 | font-size: 14px; 32 | } 33 | 34 | .videoWrapper { 35 | margin-top: 60px; 36 | display: flex; 37 | flex-wrap: wrap; 38 | padding-bottom: 300px; 39 | } 40 | 41 | 42 | .categoryWrapper > hr { 43 | width: 90%; 44 | margin-left: 20px; 45 | margin-top: 30px; 46 | background-color: rgb(48,48,50); 47 | height: .5px; 48 | border: none; 49 | } 50 | 51 | 52 | .videoCountWrapper{ 53 | display: flex; 54 | margin: 30px; 55 | } 56 | 57 | .videoCountWrapper h1:nth-child(1) { 58 | margin-right: 10px; 59 | color: #EFEFF1; 60 | } 61 | 62 | 63 | .videoCountWrapper h1:nth-child(2) { 64 | margin-right: 10px; 65 | color: #A4A4AF; 66 | } 67 | -------------------------------------------------------------------------------- /frontend/components/NavBar/NavBar.module.css: -------------------------------------------------------------------------------- 1 | .mainNav { 2 | background-color: #18181B; 3 | /* background-color: whitesmoke; */ 4 | height: 55px; 5 | display: flex; 6 | justify-content: space-between; 7 | align-items: center; 8 | box-shadow: 3px 3px 8px #888888; 9 | border-bottom: 2px solid #0E0E10; 10 | width: 100%; 11 | flex-shrink: 0 12 | } 13 | 14 | 15 | .leftNav { 16 | margin-left: 20px; 17 | display: flex; 18 | align-items: center; 19 | 20 | } 21 | 22 | .rightNav { 23 | margin-right: 20px; 24 | } 25 | 26 | .appTitle { 27 | color: whitesmoke; 28 | } 29 | 30 | .mainContainer { 31 | display: flex; 32 | overflow-y: hidden; 33 | } 34 | .mainContainer2 { 35 | overflow-y: hidden; 36 | } 37 | 38 | 39 | .browseContainer { 40 | display: flex; 41 | box-sizing: border-box; 42 | height: 55px; 43 | margin-left: 20px; 44 | align-items: center; 45 | justify-content: center; 46 | cursor: pointer; 47 | } 48 | 49 | 50 | 51 | .browseContainer > span { 52 | color: white; 53 | font-size: 19.5px; 54 | font-weight: 540; 55 | 56 | } 57 | 58 | .browseContainer > span:hover { 59 | color: #A970FF; 60 | 61 | 62 | } 63 | .line{ 64 | height: 40px; 65 | border-left: .5px solid rgb(48,48,50); 66 | margin-left: 20px; 67 | 68 | } -------------------------------------------------------------------------------- /frontend/components/SideBar/SideBar.module.css: -------------------------------------------------------------------------------- 1 | .sideBar { 2 | background-color: #1F1F23; 3 | height: calc(100vh - 55px); 4 | flex-grow: 1.1; 5 | min-width: 250px; 6 | max-width: 250px; 7 | display: flex; 8 | flex-direction: column; 9 | overflow-y: hidden; 10 | 11 | } 12 | 13 | .sideBarItemContainer { 14 | padding-left: 15px; 15 | padding-top: 3px; 16 | padding-bottom: 3px; 17 | margin-top: 15px; 18 | display: flex; 19 | justify-content: flex-start; 20 | align-items: center; 21 | cursor: pointer; 22 | position: relative; 23 | } 24 | .sideBarItemContainer:hover { 25 | background-color: #3A3A3D; 26 | } 27 | 28 | 29 | 30 | 31 | .sideBarItem { 32 | /* font-size: 25px; */ 33 | } 34 | 35 | .logo { 36 | width: 40px; 37 | height: 40px; 38 | border-radius: 50%; 39 | margin-right: 10px; 40 | } 41 | 42 | 43 | .channelDetails { 44 | display: flex; 45 | align-items: center; 46 | 47 | } 48 | .channelDetails > h4 { 49 | color: rgb(231, 231, 240); 50 | font-size: 12px; 51 | } 52 | 53 | .sideBarItemContainer > h5 { 54 | position: absolute; 55 | right: 10px; 56 | color: #C0C0C5; 57 | } 58 | 59 | .sideBar > h3 { 60 | color: white; 61 | margin-top: 20px; 62 | margin-left: 15px; 63 | font-size: 14px; 64 | } -------------------------------------------------------------------------------- /frontend/actions/follow_actions.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_FOLLOW = "RECEIVE_FOLLOW" 2 | export const REMOVE_FOLLOW = "REMOVE_FOLLOW" 3 | export const RECEIVE_FOLLOWED_CHANNELS = "RECEIVE_FOLLOWED_CHANNELS" 4 | import * as FollowsAPIUtil from '../util/follows_api_util' 5 | 6 | 7 | const receiveFollow = (follow) => { 8 | 9 | return { 10 | type: RECEIVE_FOLLOW, 11 | follow 12 | } 13 | 14 | } 15 | 16 | 17 | const removeFollow = (follow) => { 18 | 19 | return { 20 | type: REMOVE_FOLLOW, 21 | follow 22 | } 23 | 24 | } 25 | 26 | const receiveFollowedChannels = (channels) => { 27 | 28 | return { 29 | type: RECEIVE_FOLLOWED_CHANNELS, 30 | channels 31 | } 32 | 33 | } 34 | 35 | 36 | export const requestFollowedChannels = (channelId) => (dispatch) => { 37 | 38 | return FollowsAPIUtil.getChannelFollowers(channelId) 39 | .then((channels) => dispatch(receiveFollowedChannels(channels))) 40 | } 41 | 42 | 43 | 44 | export const createFollow = (follow) => dispatch => { 45 | 46 | return FollowsAPIUtil.postFollow(follow) 47 | .then((follow) => dispatch(receiveFollow(follow))) 48 | } 49 | 50 | 51 | export const deleteFollow = (channelId) => dispatch => { 52 | 53 | return FollowsAPIUtil.deleteFollow(channelId) 54 | .then((follow) => dispatch(removeFollow(follow))) 55 | } -------------------------------------------------------------------------------- /frontend/components/ChannelFollowers/ChannelFollowers.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react' 3 | import { connect } from 'react-redux' 4 | import { withRouter } from 'react-router-dom' 5 | import classes from './ChannelFollowers.module.css' 6 | import { requestFollowedChannels } from '../../actions/follow_actions' 7 | 8 | class ChannelFollowers extends React.Component { 9 | 10 | componentDidMount() { 11 | this.props.requestFollowedChannels(this.props.match.params.channelId) 12 | } 13 | 14 | render() { 15 | return ( 16 |
17 | { 18 | this.props.channels.map((channel) => { 19 | return ( 20 |
21 | 22 | {channel.channelName} 23 |
24 | ) 25 | }) 26 | } 27 |
28 | ) 29 | } 30 | } 31 | 32 | 33 | const mSTP = (state) => { 34 | const channels = state.entities.channels.followedChannels ? Object.values(state.entities.channels.followedChannels) : [] 35 | return { 36 | channels, 37 | } 38 | } 39 | 40 | const mDTP = (dispatch) => { 41 | return { 42 | requestFollowedChannels: (channelId) => dispatch(requestFollowedChannels(channelId)), 43 | } 44 | } 45 | 46 | export default withRouter(connect(mSTP, mDTP)(ChannelFollowers)) 47 | -------------------------------------------------------------------------------- /frontend/components/About/AboutPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classes from './AboutPage.module.css' 3 | /* eslint-disable */ 4 | 5 | 6 | const AboutPage = () => { 7 | 8 | return ( 9 |
10 |
11 |
12 | 13 |

Charles Coombs-Esmail

14 |

Website Creator

15 | 32 |
33 |
34 |
35 | 36 | ) 37 | } 38 | 39 | 40 | export default AboutPage 41 | -------------------------------------------------------------------------------- /app/views/api/channels/index_first_vods.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.channels do 2 | @channels.each do |channel| 3 | json.set! channel.id do 4 | json.id channel.id 5 | json.ownerId channel.owner_id 6 | json.channelName channel.channel_name 7 | json.logoUrl url_for(channel.logoUrl) 8 | end 9 | end 10 | end 11 | 12 | 13 | json.vods do 14 | @channels.each do |channel| 15 | vod = channel.vods.first 16 | if !vod.nil? 17 | json.set! vod.id do 18 | json.id vod.id 19 | json.channelId vod.channel_id 20 | json.title vod.title 21 | json.category vod.category 22 | json.videoUrl url_for(vod.videoUrl) 23 | end 24 | else 25 | json.set! SecureRandom.base64(64) do 26 | json.id nil 27 | json.channelId channel.id 28 | json.title nil 29 | json.category nil 30 | json.videoUrl nil 31 | end 32 | end 33 | end 34 | end 35 | 36 | 37 | json.users do 38 | @channels.each do |channel| 39 | owner = channel.owner 40 | json.set! owner.id do 41 | json.id owner.id 42 | json.username owner.username 43 | json.channelId channel.id 44 | json.follows owner.followed_channels.map{|channel| channel.id } 45 | end 46 | end 47 | end -------------------------------------------------------------------------------- /frontend/actions/vod_actions.js: -------------------------------------------------------------------------------- 1 | import * as VodAPIUtil from '../util/vod_api_util' 2 | export const RECEIVE_VOD = "RECEIVE_VOD" 3 | export const RECEIVE_VODS = "RECEIVE_VODS" 4 | export const CLEAR_VODS = "CLEAR_VODS" 5 | export const RECEIVE_RANDOM_VODS = "RECEIVE_RANDOM_VODS" 6 | 7 | 8 | const receiveVod = (vod) => { 9 | return { 10 | type: RECEIVE_VOD, 11 | vod, 12 | } 13 | } 14 | 15 | 16 | const receiveVods = (payload) => { 17 | return { 18 | type: RECEIVE_VODS, 19 | payload, 20 | } 21 | } 22 | 23 | const receiveRandomVods = (payload) => { 24 | return { 25 | type: RECEIVE_RANDOM_VODS, 26 | payload, 27 | } 28 | } 29 | 30 | export const clearVods = () => { 31 | return { 32 | type: CLEAR_VODS, 33 | } 34 | } 35 | 36 | 37 | export const requestVod = (vodId) => (dispatch) => { 38 | return VodAPIUtil.fetchVod(vodId) 39 | .then((vod) => dispatch(receiveVod(vod))) 40 | } 41 | 42 | 43 | export const requestVods = (filter) => (dispatch) => { 44 | return VodAPIUtil.fetchVods(filter) 45 | .then((vods) => dispatch(receiveVods(vods))) 46 | } 47 | 48 | export const requestRandomVods = (filter) => (dispatch) => { 49 | return VodAPIUtil.fetchVods(filter) 50 | .then((vods) => dispatch(receiveRandomVods(vods))) 51 | } 52 | 53 | export const createVod = (formData) => (dispatch) => { 54 | return VodAPIUtil.postVod(formData) 55 | .then((vod) => dispatch(receiveVod(vod))) 56 | .fail((err) => console.log(err)) 57 | } 58 | -------------------------------------------------------------------------------- /frontend/components/Session/TabNavs/TabsNav.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classes from './TabNavs.module.css' 3 | /* eslint-disable */ 4 | 5 | 6 | class TabNavs extends React.Component { 7 | 8 | constructor(props) { 9 | super(props) 10 | this.state = { 11 | currentForm: '', 12 | } 13 | this.changeTab = this.changeTab.bind(this) 14 | } 15 | 16 | 17 | componentDidMount() { 18 | this.setState({ currentForm: this.props.currentForm }) 19 | } 20 | 21 | changeTab() { 22 | this.props.clearErrors() 23 | this.props.navToOtherForm() 24 | let nextForm 25 | this.state.currentForm === 'Login' ? nextForm = 'Sign Up' : nextForm = 'Login' 26 | this.setState({ currentForm: nextForm }) 27 | } 28 | 29 | render() { 30 | const loginBtnClasses = [classes.navTab] 31 | const signupBtnClasses = [classes.navTab] 32 | 33 | if (this.state.currentForm === 'Sign Up') { 34 | signupBtnClasses.push(classes.selected) 35 | } else { 36 | loginBtnClasses.push(classes.selected) 37 | } 38 | 39 | return ( 40 |
    41 |
  • 42 | Log In 43 |
  • 44 |
  • 45 | Sign Up 46 |
  • 47 |
48 | 49 | 50 | ) 51 | } 52 | } 53 | 54 | 55 | export default TabNavs 56 | -------------------------------------------------------------------------------- /frontend/components/ChatRoom/ChatRoom.module.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* .chatroomWrapper{ 4 | position: absolute; 5 | right: 0; 6 | height: 90vh; */ 7 | /* margin-top: 100px; */ 8 | /* min-width: 200px; */ 9 | /* border: solid gray 1px; 10 | background-color: #18181B; 11 | flex-grow: 1; 12 | 13 | } */ 14 | 15 | .chatroomContainer { 16 | position: absolute; 17 | right: 0; 18 | /* height: 90vh; */ 19 | height: calc(100vh - 55px); 20 | 21 | /* margin-top: 100px; */ 22 | min-width: 340px; 23 | max-width: 340px; 24 | border: solid #303032 1px; 25 | background-color: #18181B; 26 | flex-grow: 1; 27 | display: flex; 28 | flex-direction: column; 29 | align-items: center; 30 | /* overflow-y: hidden; */ 31 | overflow-x: hidden; 32 | 33 | 34 | } 35 | 36 | .messageList{ 37 | overflow-y: scroll; 38 | height: 83%; 39 | width: 100%; 40 | /* width: 300px; */ 41 | border: solid #303032 1px; 42 | background-color: #18181B; 43 | color: #EFEFF1; 44 | font-weight: 700; 45 | border-bottom: none; 46 | } 47 | 48 | .messageLi { 49 | list-style: none; 50 | margin: 10px; 51 | } 52 | 53 | .username { 54 | color:#A86DFF; 55 | } 56 | 57 | 58 | .messageBody{ 59 | color: white; 60 | } 61 | 62 | .chatTitle { 63 | display: flex; 64 | align-items: center; 65 | color: whitesmoke; 66 | height: 50px; 67 | } 68 | 69 | 70 | ::-webkit-scrollbar { 71 | width: 0px; 72 | background: transparent; 73 | } 74 | -------------------------------------------------------------------------------- /app/controllers/api/channels_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::ChannelsController < ApplicationController 2 | 3 | def index 4 | 5 | if params[:filter] == nil 6 | @channels = Channel.all.limit(8) 7 | render :index 8 | elsif params[:filter]['firstVods'] # Get first 8 channels along with their first vods to use as thumbnails (home page) 9 | @channels = Channel.order(:created_at).limit(8) 10 | render :index_first_vods 11 | elsif params[:filter]['allChannels'] # Get all channels along with their first vods to use as thumbnails (directory/browse) 12 | @channels = Channel.all 13 | render :index_first_vods 14 | elsif params[:filter]['searchChannels'] 15 | @channels = Channel.where("channel_name LIKE ?", "%#{params[:filter]['searchInput']}%") 16 | render :index 17 | end 18 | 19 | end 20 | 21 | 22 | def show 23 | @channel = Channel.find_by(id: params[:id]) 24 | render :show 25 | end 26 | 27 | 28 | def update 29 | 30 | @channel = Channel.find_by(owner_id: params[:id]) 31 | 32 | if @channel.update(channel_params) 33 | 34 | render :show 35 | else 36 | render @channel.errors.full_messages, status: 401 37 | end 38 | end 39 | 40 | private 41 | def channel_params 42 | params.require(:channel).permit(:owner_id, :logoUrl) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | 10 | amazon_dev: 11 | service: S3 12 | access_key_id: <%= Rails.application.credentials.aws[:access_key_id] %> 13 | secret_access_key: <%= Rails.application.credentials.aws[:secret_access_key] %> 14 | region: <%= Rails.application.credentials.aws[:region] %> 15 | bucket: <%= Rails.application.credentials.aws[:dev][:bucket] %> 16 | 17 | amazon_prod: 18 | service: S3 19 | access_key_id: <%= Rails.application.credentials.aws[:access_key_id] %> 20 | secret_access_key: <%= Rails.application.credentials.aws[:secret_access_key] %> 21 | region: <%= Rails.application.credentials.aws[:region] %> 22 | bucket: <%= Rails.application.credentials.aws[:prod][:bucket] %> 23 | 24 | # Remember not to checkin your GCS keyfile to a repository 25 | # google: 26 | # service: GCS 27 | # project: your_project 28 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 29 | # bucket: your_own_bucket 30 | 31 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 32 | # microsoft: 33 | # service: AzureStorage 34 | # storage_account_name: your_account_name 35 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 36 | # container: your_container_name 37 | 38 | # mirror: 39 | # service: Mirror 40 | # primary: local 41 | # mirrors: [ amazon, google, microsoft ] 42 | -------------------------------------------------------------------------------- /frontend/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import configureStore from './store/store' 4 | import Root from './components/Root' 5 | 6 | 7 | 8 | document.addEventListener('DOMContentLoaded', () => { 9 | let store; 10 | 11 | if (window.currentUser != null) { 12 | const preloadedState = { 13 | entities: { 14 | users: { 15 | [window.currentUser.id] : window.currentUser, 16 | [window.demoUser.id]: window.demoUser 17 | } 18 | }, 19 | session: { 20 | currentUserId: window.currentUser.id, 21 | }, 22 | ui: { 23 | demoUserId: window.demoUser.id 24 | } 25 | } 26 | 27 | store = configureStore( preloadedState) 28 | delete window.currentUser 29 | delete window.demoUser 30 | 31 | 32 | }else { 33 | const preloadedState = { 34 | entities: { 35 | users: { 36 | [window.demoUser.id]: window.demoUser 37 | } 38 | }, 39 | ui: { 40 | demoUserId: window.demoUser.id 41 | } 42 | } 43 | 44 | store = configureStore(preloadedState) 45 | delete window.demoUser 46 | } 47 | 48 | 49 | 50 | 51 | let rootEl = document.getElementById('root') 52 | window.getState = store.getState 53 | ReactDOM.render(, rootEl ) 54 | 55 | }) -------------------------------------------------------------------------------- /frontend/reducers/users_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_CURRENT_USER, RECEIVE_NEW_USER } from '../actions/session_actions' 2 | import { RECEIVE_CHANNELS } from '../actions/channel_actions' 3 | import {RECEIVE_FOLLOW, REMOVE_FOLLOW} from '../actions/follow_actions' 4 | 5 | const userReducer = (state = {}, action) => { 6 | Object.freeze(state) 7 | 8 | switch (action.type) { 9 | case RECEIVE_CURRENT_USER: 10 | return Object.assign({}, state, {[action.user.id]: action.user}) 11 | case RECEIVE_NEW_USER: 12 | return Object.assign({}, state, action.payload.user) 13 | case RECEIVE_CHANNELS: 14 | return Object.assign({}, state, action.payload.users) 15 | case RECEIVE_FOLLOW: 16 | let newState = Object.assign({}, state) 17 | let addFollow = [...newState[action.follow.userId].follows] 18 | addFollow.push(action.follow.channelId) 19 | newState[action.follow.userId].follows = addFollow 20 | return newState 21 | case REMOVE_FOLLOW: 22 | let newState2 = Object.assign({}, state) 23 | let indexOfChannel = newState2[action.follow.userId].follows.indexOf(action.follow.channelId) 24 | let removeFollow = newState2[action.follow.userId].follows.filter((follow, idx) => idx !== indexOfChannel) 25 | newState2[action.follow.userId].follows = removeFollow 26 | return newState2 27 | default: 28 | return state 29 | } 30 | 31 | } 32 | 33 | 34 | export default userReducer; -------------------------------------------------------------------------------- /frontend/components/Vods/VodsIndex/ChannelVideosIndex.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { withRouter } from 'react-router-dom' 4 | import classes from './ChannelVideosIndex.module.css' 5 | import { requestVods, clearVods } from '../../../actions/vod_actions' 6 | import VodIndexItem from './VodIndexItem' 7 | /* eslint-disable */ 8 | 9 | class ChannelVideosIndex extends React.Component { 10 | 11 | 12 | componentDidMount() { 13 | this.props.requestVods({ 14 | channel_id: this.props.match.params.channelId, 15 | }) 16 | 17 | } 18 | 19 | componentWillUnmount() { 20 | this.props.clearVods() 21 | } 22 | 23 | render() { 24 | return ( 25 |
26 |
27 |

Videos

28 |

{this.props.vods.length}

29 |
30 |
31 |
32 | { 33 | this.props.vods.map((vod, idx) => { 34 | return 35 | }) 36 | } 37 |
38 |
39 | ) 40 | } 41 | } 42 | 43 | 44 | const mSTP = (state) => { 45 | return { 46 | vods: Object.values(state.entities.vods), 47 | } 48 | } 49 | 50 | const mDTP = (dispatch) => { 51 | return { 52 | requestVods: (filter) => dispatch(requestVods(filter)), 53 | clearVods: () => dispatch(clearVods()), 54 | } 55 | } 56 | 57 | 58 | export default withRouter(connect(mSTP, mDTP)(ChannelVideosIndex)) 59 | -------------------------------------------------------------------------------- /frontend/components/Modal/Modal.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react' 3 | import { connect } from 'react-redux' 4 | import { closeModal } from '../../actions/modal_actions' 5 | import LoginContainerComponent from '../Session/login_container_component' 6 | import SignupContainerComponent from '../Session/SignupForm/signup_container_component' 7 | 8 | import classes from './Modal.module.css' 9 | import DemoForm from '../Session/DemoForm/DemoForm' 10 | import SuccessMessage from '../Dashboard/SuccessMessage' 11 | 12 | 13 | const Modal = ({ modal, closeModal }) => { 14 | if (!modal) { 15 | return null 16 | } 17 | 18 | let component 19 | switch (modal) { 20 | case 'login': 21 | component = 22 | break 23 | case 'signup': 24 | component = 25 | break 26 | case 'demo': 27 | component = 28 | break; 29 | case 'success': 30 | component = 31 | break 32 | default: 33 | return null 34 | } 35 | 36 | return ( 37 |
38 |
e.stopPropagation()}> 39 | {component} 40 |
41 |
42 | ) 43 | 44 | } 45 | 46 | 47 | const mSTP = (state) => { 48 | return { 49 | modal: state.ui.modal, 50 | } 51 | } 52 | 53 | 54 | const mDTP = (dispatch) => { 55 | return { 56 | closeModal: () => dispatch(closeModal()), 57 | } 58 | } 59 | 60 | 61 | export default connect(mSTP, mDTP)(Modal) -------------------------------------------------------------------------------- /frontend/components/Vods/VodsIndex/ChannelVideosIndex.module.css: -------------------------------------------------------------------------------- 1 | .indexWrapper { 2 | display: flex; 3 | flex-direction: column; 4 | flex-wrap: wrap; 5 | } 6 | 7 | .vodsWrapper { 8 | display: flex; 9 | flex-wrap: wrap; 10 | } 11 | 12 | 13 | .vodItem { 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: flex-start; 18 | margin: 30px; 19 | box-sizing: border-box; 20 | 21 | } 22 | 23 | .vodItem > h4 { 24 | color: white; 25 | margin-top: 10px; 26 | 27 | } 28 | 29 | .vodItem > h6 { 30 | margin-top: 5px; 31 | color: rgb(169, 169, 179); 32 | font-size: 12px; 33 | 34 | } 35 | 36 | .vodThumb { 37 | width: 100%; 38 | height: 160px; 39 | background-color: white; 40 | display: flex; 41 | justify-content: center; 42 | align-items: center; 43 | } 44 | 45 | .videoPlayer { 46 | width: 300px; 47 | height: 160px; 48 | box-sizing: border-box; 49 | cursor: pointer; 50 | border: 1px solid transparent; 51 | } 52 | 53 | .videoPlayer:hover { 54 | border: 1px solid blue; 55 | 56 | } 57 | 58 | 59 | .videoCountWrapper{ 60 | display: flex; 61 | margin: 30px; 62 | } 63 | 64 | .videoCountWrapper h1:nth-child(1) { 65 | margin-right: 10px; 66 | color: #EFEFF1; 67 | } 68 | 69 | 70 | .videoCountWrapper h1:nth-child(2) { 71 | margin-right: 10px; 72 | color: #A4A4AF; 73 | } 74 | 75 | 76 | 77 | .indexWrapper > hr { 78 | width: 90%; 79 | margin-left: 20px; 80 | margin-top: 30px; 81 | margin-bottom: 30px; 82 | background-color: rgb(48,48,50); 83 | height: .5px; 84 | border: none; 85 | } -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the `pidfile` that Puma will use. 19 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 20 | 21 | # Specifies the number of `workers` to boot in clustered mode. 22 | # Workers are forked webserver processes. If using threads and workers together 23 | # the concurrency of the application would be max `threads` * `workers`. 24 | # Workers do not work on JRuby or Windows (both of which do not support 25 | # processes). 26 | # 27 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 28 | 29 | # Use the `preload_app!` method when specifying a `workers` number. 30 | # This directive tells Puma to first boot the application and load code 31 | # before forking the application. This takes advantage of Copy On Write 32 | # process behavior so workers use less memory. 33 | # 34 | # preload_app! 35 | 36 | # Allow puma to be restarted by `rails restart` command. 37 | plugin :tmp_restart 38 | -------------------------------------------------------------------------------- /frontend/components/Dashboard/VideoForm.module.css: -------------------------------------------------------------------------------- 1 | .videoUploadWrapper { 2 | 3 | margin-top: 80px; 4 | margin-left: 20px; 5 | padding-bottom: 700px; 6 | 7 | /* display: flex; */ 8 | 9 | } 10 | 11 | 12 | .videoUploadWrapper > h2 { 13 | color: white; 14 | margin-bottom: 15px; 15 | 16 | } 17 | 18 | .videoForm { 19 | display: flex; 20 | align-items: center; 21 | width: 900px; 22 | height: 380px; 23 | background-color: #18181B; 24 | border: 1px solid #303032; 25 | border-radius: 7px; 26 | } 27 | 28 | .videoPlayer { 29 | padding: 30px; 30 | width: 400px; 31 | 32 | } 33 | 34 | 35 | .formInput { 36 | width: calc(100% - 10px); 37 | background-color: #3A3A3D; 38 | color: white; 39 | border-radius: 5px; 40 | box-sizing: border-box; 41 | height: 27px; 42 | margin-top: 7px; 43 | padding-left: 10px; 44 | border: none; 45 | 46 | } 47 | 48 | .formInput:focus { 49 | border: solid #9147FF 2.2px; 50 | background-color: rgb(0, 0, 0); 51 | border-radius: 5px; 52 | outline: none; 53 | 54 | } 55 | 56 | .formLabel { 57 | width: 90%; 58 | margin-bottom: 25px; 59 | font-size: 12px; 60 | } 61 | 62 | 63 | .categoryInput { 64 | width: 48%; 65 | background-color: #3A3A3D; 66 | color: white; 67 | box-sizing: border-box; 68 | border-radius: 5px; 69 | margin-top: 5px; 70 | height: 32px; 71 | margin-right: 7px; 72 | cursor: pointer; 73 | 74 | } 75 | 76 | 77 | .categoryInput:focus { 78 | border: solid #9147FF 2.2px; 79 | background-color: rgb(0, 0, 0); 80 | border-radius: 5px; 81 | outline: none; 82 | } 83 | -------------------------------------------------------------------------------- /frontend/components/Channels/ChannelShow/ChannelShow.module.css: -------------------------------------------------------------------------------- 1 | .channelWrapper { 2 | flex-grow: 6.0; 3 | background-color: #0E0E10; 4 | display: flex; 5 | height: 100vh; 6 | overflow-y: scroll !important; 7 | 8 | } 9 | 10 | 11 | .channelContents { 12 | display: flex; 13 | flex-direction: column; 14 | background-color: #0E0E10; 15 | width: 100px; 16 | flex-grow: 3; 17 | 18 | } 19 | 20 | nav { 21 | background-color: #18181B; 22 | height: 48px; 23 | border-bottom: 2px solid #18181B ; 24 | display: flex; 25 | justify-content: space-between; 26 | align-items: center; 27 | 28 | } 29 | 30 | .leftNav { 31 | width: 100px; 32 | } 33 | 34 | .rightNav { 35 | width: 500px; 36 | } 37 | 38 | 39 | .right { 40 | min-width: 342px; 41 | max-width: 342px; 42 | flex: 1; 43 | height: calc(100vh - 55px); 44 | } 45 | 46 | .followWrapper { 47 | display: flex; 48 | justify-content: center; 49 | align-items: center; 50 | width: 100px; 51 | height: 30px; 52 | border-radius: 5px; 53 | background-color: #9147FF; 54 | color: white; 55 | margin-right: 20px; 56 | cursor: pointer; 57 | } 58 | 59 | .followWrapper:hover { 60 | opacity: .7; 61 | 62 | } 63 | 64 | 65 | .followWrapper > h5 { 66 | color: white; 67 | margin-left: 5px; 68 | 69 | } 70 | 71 | .followIcon { 72 | color: #EFEFF1; 73 | } 74 | 75 | 76 | .userIconWrapper { 77 | display: flex; 78 | justify-content: center; 79 | align-self: center; 80 | border-radius: 50%; 81 | z-index: 6000; 82 | 83 | } 84 | 85 | .userIcon{ 86 | border-radius: 50%; 87 | width: 40px; 88 | height: 40px; 89 | cursor: pointer; 90 | } 91 | 92 | 93 | -------------------------------------------------------------------------------- /frontend/reducers/channels_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_CHANNELS, UPDATE_CHANNEL, RECEIVE_CHANNEL, CLEAR_CHANNELS, RECEIVE_SEARCHED_CHANNELS } from '../actions/channel_actions' 2 | import { RECEIVE_NEW_USER } from '../actions/session_actions' 3 | import {RECEIVE_VODS, RECEIVE_RANDOM_VODS } from '../actions/vod_actions' 4 | import { RECEIVE_FOLLOWED_CHANNELS } from '../actions/follow_actions' 5 | 6 | 7 | const channelsReducer = (state = {}, action) => { 8 | Object.freeze(state); 9 | let newState = Object.assign({}, state); 10 | 11 | switch (action.type) { 12 | case RECEIVE_CHANNELS: 13 | return Object.assign({}, state, action.payload.channels) 14 | case RECEIVE_CHANNEL: 15 | return Object.assign({}, state, action.channel) 16 | case UPDATE_CHANNEL: 17 | newState = Object.assign({}, state) 18 | const channel = Object.values(action.channel)[0] 19 | newState[channel.id] = channel 20 | return newState 21 | case RECEIVE_NEW_USER: 22 | return Object.assign({}, state, action.payload.channel) 23 | case RECEIVE_VODS: 24 | return Object.assign({}, state, action.payload.channels) 25 | case RECEIVE_RANDOM_VODS: 26 | newState = Object.assign({}, state) 27 | newState['randomVodChannels'] = action.payload.channels 28 | return newState 29 | case RECEIVE_SEARCHED_CHANNELS: 30 | newState = Object.assign({}, state) 31 | newState['searched'] = action.payload.channels 32 | return newState 33 | case RECEIVE_FOLLOWED_CHANNELS: 34 | newState = Object.assign({}, state) 35 | newState['followedChannels'] = action.channels 36 | return newState 37 | case CLEAR_CHANNELS: 38 | return {} 39 | default: 40 | return state 41 | } 42 | } 43 | 44 | 45 | 46 | export default channelsReducer; 47 | 48 | 49 | -------------------------------------------------------------------------------- /frontend/components/Channels/ChannelIndex.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react' 3 | import { connect } from 'react-redux' 4 | import { withRouter } from 'react-router-dom' 5 | import { requestChannels } from '../../actions/channel_actions' 6 | import { requestVods, clearVods } from '../../actions/vod_actions' 7 | import classes from './ChannelIndex.module.css' 8 | import ChannelIndexItem from './ChannelIndexItem/ChannelIndexItem' 9 | 10 | class ChannelIndex extends React.Component { 11 | 12 | 13 | componentDidMount() { 14 | const { match, requestChannels } = this.props 15 | if (match.path === '/') { 16 | requestChannels({ firstVods: true }) 17 | } else { 18 | requestChannels({ allChannels: true }) 19 | } 20 | } 21 | 22 | componentWillUnmount() { 23 | this.props.clearVods() 24 | } 25 | 26 | 27 | render() { 28 | return ( 29 |
30 | { 31 | this.props.vods.map((vod, idx) => { 32 | const channel = this.props.channels[vod.channelId] 33 | let user 34 | channel ? user = this.props.users[channel.ownerId] : user = null 35 | return 36 | }, this) 37 | } 38 |
39 | ) 40 | } 41 | } 42 | 43 | const mSTP = (state) => { 44 | return { 45 | channels: state.entities.channels, 46 | users: state.entities.users, 47 | vods: Object.values(state.entities.vods), 48 | } 49 | } 50 | 51 | 52 | const mDTP = (dispatch) => { 53 | return { 54 | requestChannels: (filter) => dispatch(requestChannels(filter)), 55 | requestVods: () => dispatch(requestVods()), 56 | clearVods: () => dispatch(clearVods()), 57 | } 58 | } 59 | 60 | 61 | export default withRouter(connect(mSTP, mDTP)(ChannelIndex)) 62 | -------------------------------------------------------------------------------- /frontend/components/Carousel/Carousel.module.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | [type=radio] { 4 | display: none; 5 | } 6 | 7 | #slider { 8 | height: 35vw; 9 | position: relative; 10 | perspective: 1000px; 11 | transform-style: preserve-3d; 12 | } 13 | 14 | #slider label { 15 | margin: auto; 16 | width: 60%; 17 | height: 100%; 18 | border-radius: 4px; 19 | position: absolute; 20 | left: 0; right: 0; 21 | cursor: pointer; 22 | transition: transform 0.4s ease; 23 | } 24 | 25 | #s1:checked ~ #slide4, #s2:checked ~ #slide5, 26 | #s3:checked ~ #slide1, #s4:checked ~ #slide2, 27 | #s5:checked ~ #slide3 { 28 | box-shadow: 0 1px 4px 0 rgba(0,0,0,.37); 29 | transform: translate3d(-30%,0,-200px); 30 | } 31 | 32 | #s1:checked ~ #slide5, #s2:checked ~ #slide1, 33 | #s3:checked ~ #slide2, #s4:checked ~ #slide3, 34 | #s5:checked ~ #slide4 { 35 | box-shadow: 0 6px 10px 0 rgba(0,0,0,.3), 0 2px 2px 0 rgba(0,0,0,.2); 36 | transform: translate3d(-15%,0,-100px); 37 | } 38 | 39 | #s1:checked ~ #slide1, #s2:checked ~ #slide2, 40 | #s3:checked ~ #slide3, #s4:checked ~ #slide4, 41 | #s5:checked ~ #slide5 { 42 | box-shadow: 0 13px 25px 0 rgba(0,0,0,.3), 0 11px 7px 0 rgba(0,0,0,.19); 43 | transform: translate3d(0,0,0); 44 | } 45 | 46 | #s1:checked ~ #slide2, #s2:checked ~ #slide3, 47 | #s3:checked ~ #slide4, #s4:checked ~ #slide5, 48 | #s5:checked ~ #slide1 { 49 | box-shadow: 0 6px 10px 0 rgba(0,0,0,.3), 0 2px 2px 0 rgba(0,0,0,.2); 50 | transform: translate3d(15%,0,-100px); 51 | } 52 | 53 | #s1:checked ~ #slide3, #s2:checked ~ #slide4, 54 | #s3:checked ~ #slide5, #s4:checked ~ #slide1, 55 | #s5:checked ~ #slide2 { 56 | box-shadow: 0 1px 4px 0 rgba(0,0,0,.37); 57 | transform: translate3d(30%,0,-200px); 58 | } 59 | 60 | #slide1 { background: #00BCD4 } 61 | #slide2 { background: #4CAF50 } 62 | #slide3 { background: #CDDC39 } 63 | #slide4 { background: #FFC107 } 64 | #slide5 { background: #FF5722 } -------------------------------------------------------------------------------- /frontend/components/Categories/Categories.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import React from 'react' 4 | import { connect } from 'react-redux' 5 | import { withRouter, Link } from 'react-router-dom' 6 | import { clearVods } from '../../actions/vod_actions' 7 | import { requestCategories } from '../../actions/category_actions' 8 | import classes from './Categories.module.css' 9 | 10 | class Categories extends React.Component { 11 | 12 | componentDidMount() { 13 | this.props.requestCategories() 14 | } 15 | 16 | componentWillUnmount() { 17 | this.props.clearVods() 18 | } 19 | 20 | render() { 21 | return ( 22 |
23 | { 24 | this.props.categories.map((category, idx) => { 25 | const key = category.id || idx 26 | return ( 27 | 28 |
29 |
30 |
31 | 32 |
33 |
{category.name}
34 |
35 |
36 | 37 | ) 38 | }) 39 | } 40 |
41 | ) 42 | } 43 | } 44 | 45 | 46 | const mSTP = (state) => { 47 | return { 48 | categories: Object.values(state.entities.categories), 49 | } 50 | } 51 | 52 | const mDTP = (dispatch) => { 53 | return { 54 | clearVods: () => dispatch(clearVods()), 55 | requestCategories: () => dispatch(requestCategories()), 56 | } 57 | } 58 | 59 | export default withRouter(connect(mSTP, mDTP)(Categories)) 60 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

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

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /frontend/components/SessionControls/DropDownMenu/DropDownMenu.module.css: -------------------------------------------------------------------------------- 1 | .dropDown{ 2 | position: absolute; 3 | width: 170px; 4 | height: 300px; 5 | background-color: #18181B; 6 | right: 5px; 7 | top: 50px; 8 | color: white; 9 | border-radius: 7px; 10 | z-index: 99999; 11 | -webkit-box-shadow: 4px 3px 5px 0px rgba(0,0,0,1); 12 | -moz-box-shadow: 4px 3px 5px 0px rgba(0,0,0,1); 13 | box-shadow: 4px 3px 5px 0px rgba(0,0,0,1); 14 | word-wrap: break-word; 15 | 16 | } 17 | 18 | .dropDown > li { 19 | font-size: 13px; 20 | list-style: none; 21 | color:white; 22 | margin: 5px; 23 | padding: 10px; 24 | cursor: pointer; 25 | } 26 | 27 | .dropDown > li:hover { 28 | background-color: rgb(58,58,61); 29 | 30 | } 31 | 32 | .hide { 33 | display: none; 34 | } 35 | 36 | .show { 37 | display: block; 38 | } 39 | 40 | 41 | .topDiv { 42 | display: flex; 43 | /* border-bottom: 1px solid lightgrey; */ 44 | padding: 8px; 45 | margin-bottom: 20px; 46 | 47 | 48 | 49 | } 50 | 51 | .userIconMain { 52 | color: white; 53 | font-size: large; 54 | /* margin-left: 10px; */ 55 | font-size: 23px; 56 | padding: 10px; 57 | border-radius: 50%; 58 | background-color: #00BAA3; 59 | cursor: pointer; 60 | } 61 | 62 | 63 | .dropDown > hr { 64 | width: 85%; 65 | margin: auto; 66 | background-color: rgb(48,48,50); 67 | height: .5px; 68 | border: none; 69 | } 70 | 71 | .topDiv > h5 { 72 | margin-left: 8px; 73 | margin-top: 4px; 74 | font-size: 11.5px; 75 | font-weight: 700; 76 | 77 | } 78 | 79 | .userIconWrapper { 80 | display: flex; 81 | justify-content: center; 82 | align-self: center; 83 | border-radius: 50%; 84 | z-index: 6000; 85 | 86 | } 87 | 88 | .userIcon{ 89 | border-radius: 50%; 90 | width: 45px; 91 | height: 45px; 92 | cursor: pointer; 93 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Fidget", 3 | "private": true, 4 | "dependencies": { 5 | "@babel/core": "^7.9.0", 6 | "@babel/preset-env": "^7.9.5", 7 | "@babel/preset-react": "^7.9.4", 8 | "@fortawesome/fontawesome-svg-core": "^1.2.28", 9 | "@fortawesome/free-solid-svg-icons": "^5.13.0", 10 | "@fortawesome/react-fontawesome": "^0.1.9", 11 | "babel-loader": "^8.1.0", 12 | "file-loader": "^6.0.0", 13 | "react": "^16.13.1", 14 | "react-dom": "^16.13.1", 15 | "react-redux": "^7.2.0", 16 | "react-router-dom": "^5.1.2", 17 | "react-typist": "^2.0.5", 18 | "redux": "^4.0.5", 19 | "redux-logger": "^3.0.6", 20 | "redux-thunk": "^2.3.0", 21 | "webpack": "^4.43.0", 22 | "webpack-cli": "^3.3.11" 23 | }, 24 | "description": "This README would normally document whatever steps are necessary to get the application up and running.", 25 | "version": "1.0.0", 26 | "main": "index.js", 27 | "directories": { 28 | "lib": "lib", 29 | "test": "test" 30 | }, 31 | "scripts": { 32 | "test": "echo \"Error: no test specified\" && exit 1", 33 | "webpack": "webpack --mode=development --watch", 34 | "postinstall": "webpack" 35 | }, 36 | "engines": { 37 | "node": "10.13.0", 38 | "npm": "6.4.1" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/ccoombsesmail/Fidget.git" 43 | }, 44 | "keywords": [], 45 | "author": "", 46 | "license": "ISC", 47 | "bugs": { 48 | "url": "https://github.com/ccoombsesmail/Fidget/issues" 49 | }, 50 | "homepage": "https://github.com/ccoombsesmail/Fidget#readme", 51 | "devDependencies": { 52 | "css-loader": "^3.5.3", 53 | "eslint": "^6.8.0", 54 | "eslint-config-airbnb": "^18.1.0", 55 | "eslint-plugin-import": "^2.20.2", 56 | "eslint-plugin-jsx-a11y": "^6.2.3", 57 | "eslint-plugin-react": "^7.20.0", 58 | "eslint-plugin-react-hooks": "^2.5.1", 59 | "style-loader": "^1.1.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/actions/channel_actions.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_CHANNELS = 'RECEIVE_CHANNELS' 2 | export const RECEIVE_CHANNEL = 'RECEIVE_CHANNEL' 3 | export const UPDATE_CHANNEL = "UPDATE_CHANNEL" 4 | export const CLEAR_CHANNELS = "CLEAR_CHANNELS" 5 | export const RECEIVE_SEARCHED_CHANNELS = "RECEIVE_SEARCHED_CHANNELS" 6 | import * as ChannelAPIUtil from '../util/channels_api_util' 7 | 8 | 9 | 10 | const receiveChannel = (channel) => { 11 | 12 | return { 13 | type: RECEIVE_CHANNEL, 14 | channel 15 | } 16 | } 17 | 18 | 19 | const receiveChannels = (payload) => { 20 | 21 | return{ 22 | type: RECEIVE_CHANNELS, 23 | payload 24 | } 25 | 26 | } 27 | 28 | const receiveSearchedChannels = (payload) => { 29 | 30 | return{ 31 | type: RECEIVE_SEARCHED_CHANNELS, 32 | payload 33 | } 34 | } 35 | 36 | 37 | 38 | const receiveUpdatedChannel = (channel) => { 39 | 40 | return { 41 | type: UPDATE_CHANNEL, 42 | channel 43 | } 44 | } 45 | 46 | 47 | export const clearChannels = () => { 48 | 49 | return { 50 | type: CLEAR_CHANNELS, 51 | } 52 | } 53 | 54 | 55 | 56 | export const requestChannel = (channelId) => dispatch => { 57 | 58 | return ChannelAPIUtil.fetchChannel(channelId) 59 | .then((channel) => dispatch(receiveChannel(channel))) 60 | } 61 | 62 | 63 | 64 | export const requestChannels = (filter) => dispatch => { 65 | 66 | return ChannelAPIUtil.fetchChannels(filter) 67 | .then((payload) => dispatch(receiveChannels(payload))) 68 | } 69 | 70 | 71 | export const requestSearchedChannels = (filter) => dispatch => { 72 | 73 | return ChannelAPIUtil.fetchChannels(filter) 74 | .then((payload) => dispatch(receiveSearchedChannels(payload))) 75 | } 76 | 77 | 78 | export const updateChannel = (channelOwnerId, formData) => dispatch => { 79 | 80 | return ChannelAPIUtil.updateChannel(channelOwnerId, formData) 81 | .then(channel => dispatch(receiveUpdatedChannel(channel))) 82 | } 83 | 84 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | validates :username, :session_token, presence: true, uniqueness: true 3 | validates :password_digest, presence: true 4 | validates :password, length: {minimum: 6, allow_nil: true} 5 | validates_email_format_of :email, :message => 'Please enter a valid email' 6 | validate :dob_is_valid_date 7 | 8 | 9 | attr_reader :password 10 | after_initialize :ensure_session_token 11 | 12 | has_one :channel, 13 | foreign_key: :owner_id, 14 | class_name: :Channel, 15 | dependent: :destroy 16 | 17 | has_many :follows 18 | 19 | 20 | has_many :followed_channels, 21 | through: :follows, 22 | source: :channel 23 | 24 | 25 | def self.find_by_credentials(username, password) 26 | queryResult = [] 27 | user = User.find_by(username: username) 28 | # return nil if user.nil? 29 | 30 | queryResult.push(user) 31 | return queryResult.push({usernameError: "This username does not exist"}) if user.nil? 32 | 33 | user.verify_password(password) ? queryResult : [nil, { passwordError: "That password was incorrect. Please try again."}] 34 | 35 | end 36 | 37 | 38 | def verify_password(password) 39 | BCrypt::Password.new(self.password_digest).is_password?(password) 40 | end 41 | 42 | 43 | 44 | def password=(password) 45 | @password = password 46 | self.password_digest = BCrypt::Password.create(password) 47 | end 48 | 49 | 50 | def reset_session_token! 51 | self.session_token = SecureRandom.base64(64) 52 | self.save 53 | self.session_token 54 | end 55 | 56 | 57 | private 58 | 59 | def ensure_session_token 60 | self.session_token ||= SecureRandom.base64(64) 61 | end 62 | 63 | def dob_is_valid_date 64 | errors.add(:dob, 'Please Enter A Valid Date Of Birth') if ((Date.parse(dob.to_s) rescue ArgumentError) == ArgumentError) 65 | 66 | end 67 | 68 | 69 | end 70 | -------------------------------------------------------------------------------- /frontend/components/ChannelHome/ChannelHome.module.css: -------------------------------------------------------------------------------- 1 | .videoPlayer { 2 | width: 100%; 3 | height: 700px; 4 | padding-bottom: 600px; 5 | 6 | } 7 | 8 | .btnWrapper { 9 | height: 800px; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | } 14 | 15 | .btnWrapper > button { 16 | margin-top: -100px; 17 | padding: 10px; 18 | width: 300px; 19 | background-color: #9147FF; 20 | border: none; 21 | color: whitesmoke; 22 | text-decoration: none; 23 | border-radius: 5px; 24 | font-weight: 800; 25 | letter-spacing: 1px; 26 | text-transform: uppercase; 27 | font-size: 17px; 28 | margin-right: 12px; 29 | font-family: 'Ubuntu', sans-serif; 30 | cursor: pointer; 31 | -webkit-animation: pulse 2.3s ease-in-out infinite alternate; /* Safari 4+ */ 32 | -moz-animation: pulse 2.3s ease-in-out infinite alternate; /* Fx 5+ */ 33 | -o-animation: pulse 2.3s ease-in-out infinite alternate; /* Opera 12+ */ 34 | animation: pulse 2.3s ease-in-out infinite alternate; /* IE 10+, Fx 29+ */ 35 | 36 | } 37 | 38 | .peerVideoWrapper { 39 | display: flex; 40 | } 41 | 42 | 43 | 44 | @keyframes pulse { 45 | from { 46 | box-shadow: 0px 0px 4px #9147FF; 47 | 48 | } 49 | to { 50 | box-shadow: 0px 0px 40px #9b5bfa; 51 | 52 | } 53 | } 54 | 55 | 56 | @-webkit-keyframes pulse { 57 | from { 58 | box-shadow: 0px 0px 4px #9147FF; 59 | 60 | } 61 | to { 62 | box-shadow: 0px 0px 40px #9b5bfa; 63 | 64 | } 65 | } 66 | @-moz-keyframes pulse { 67 | from { 68 | box-shadow: 0px 0px 4px #9147FF; 69 | 70 | } 71 | to { 72 | box-shadow: 0px 0px 40px #9b5bfa; 73 | 74 | } 75 | } 76 | @-o-keyframes pulse { 77 | from { 78 | box-shadow: 0px 0px 4px #9147FF; 79 | 80 | } 81 | to { 82 | box-shadow: 0px 0px 40px #9b5bfa; 83 | 84 | } 85 | } 86 | @keyframes pulse { 87 | from { 88 | box-shadow: 0px 0px 4px #9147FF; 89 | 90 | } 91 | to { 92 | box-shadow: 0px 0px 40px #9b5bfa; 93 | 94 | } 95 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | entry: path.join(__dirname, "frontend", "index.jsx"), 5 | output: { 6 | path: path.join(__dirname, "app", "assets", "javascripts"), 7 | filename: "bundle.js" 8 | }, 9 | resolve: { 10 | extensions: [".js", ".jsx", "*"] 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | exclude: /(node_modules)/, 16 | use: { 17 | loader: "babel-loader", 18 | query: { 19 | presets: ["@babel/env", "@babel/react"] 20 | } 21 | } 22 | 23 | }, 24 | { 25 | test: /\.css$/, 26 | use: [ 27 | 'style-loader', 28 | { 29 | loader: 'css-loader', 30 | options: { 31 | importLoaders: 1, 32 | modules: { 33 | mode: 'local', 34 | localIdentName: '[name]__[local]--[hash:base64:5]' 35 | } 36 | } 37 | } 38 | ], 39 | include: /\.module\.css$/ 40 | }, 41 | { 42 | test: /\.css$/, 43 | use: [ 44 | 'style-loader', 45 | 'css-loader', 46 | 47 | ], 48 | exclude: /\.module\.css$/ 49 | }, 50 | { 51 | test: /\.(png|jp(e*)g|svg|gif)$/, 52 | use: [ 53 | { 54 | loader: 'file-loader', 55 | options: { 56 | name: '/images/[hash]-[name].[ext]', 57 | }, 58 | }, 59 | ], 60 | } 61 | ] 62 | }, 63 | devtool: "source-map" 64 | }; -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Store uploaded files on the local file system in a temporary directory 32 | config.active_storage.service = :test 33 | 34 | config.action_mailer.perform_caching = false 35 | 36 | # Tell Action Mailer not to deliver emails to the real world. 37 | # The :test delivery method accumulates sent emails in the 38 | # ActionMailer::Base.deliveries array. 39 | config.action_mailer.delivery_method = :test 40 | 41 | # Print deprecation notices to the stderr. 42 | config.active_support.deprecation = :stderr 43 | 44 | # Raises error for missing translations 45 | # config.action_view.raise_on_missing_translations = true 46 | end 47 | -------------------------------------------------------------------------------- /frontend/components/SideBar/SideBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { withRouter } from 'react-router-dom' 4 | import classes from './SideBar.module.css' 5 | import { requestChannels } from '../../actions/channel_actions' 6 | import { getFollowedChannels } from '../../util/selectors' 7 | import SideBarItem from './SideBarItem' 8 | /* eslint-disable */ 9 | 10 | 11 | class SideBar extends React.Component { 12 | 13 | componentDidMount() { 14 | this.props.requestChannels() 15 | } 16 | 17 | 18 | render() { 19 | return ( 20 | 43 | ) 44 | } 45 | } 46 | 47 | 48 | const mSTP = (state) => { 49 | const currentUser = state.entities.users[state.session.currentUserId] 50 | let followedChannels = [] 51 | if (currentUser) { 52 | followedChannels = getFollowedChannels(state.entities.channels, currentUser) 53 | } 54 | return { 55 | channels: Object.values(state.entities.channels).slice(0,7), 56 | users: state.entities.users, 57 | followedChannels: followedChannels.slice(0, 7), 58 | } 59 | } 60 | 61 | 62 | const mDTP = (dispatch) => { 63 | return { 64 | requestChannels: () => dispatch(requestChannels()), 65 | } 66 | } 67 | 68 | 69 | export default withRouter(connect(mSTP,mDTP)(SideBar)) 70 | -------------------------------------------------------------------------------- /frontend/components/Session/ErrorBox/ErrorBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classes from './ErrorBox.module.css' 3 | /* eslint-disable */ 4 | 5 | 6 | const ErrorBox = ({ errors, navToOtherForm, passwordMatch}) => { 7 | 8 | let errorMessage = null 9 | 10 | if (errors['usernameError']) { 11 | errorMessage = ( 12 | <> 13 | 14 | {errors['usernameError']} 15 | 16 |

17 | Want to create a new account? 18 |

19 | 20 | ) 21 | } else if (errors['passwordError']) { 22 | errorMessage = ( 23 | <> 24 | 25 | {errors['passwordError']} 26 | 27 |

28 | Forgot your password? 29 |

30 | 31 | ) 32 | } else if (!passwordMatch) { 33 | errorMessage = ( 34 | <> 35 | 36 | Passwords do not match 37 | 38 |

39 | {/* Forgot your password? */} 40 |

41 | 42 | ) 43 | } else if (Object.values(errors).length === 0) { 44 | errorMessage = null 45 | } else{ 46 | errorMessage = ( 47 | <> 48 | 49 | {errors[0]} 50 | 51 |

52 | {/* Forgot your password? */} 53 |

54 | 55 | ) 56 | } 57 | 58 | return ( 59 | <> 60 | { 61 | errorMessage === null ? null : ( 62 |
63 |
64 | {errorMessage} 65 |
66 |
67 | ) 68 | } 69 | 70 | ) 71 | } 72 | 73 | 74 | export default ErrorBox 75 | -------------------------------------------------------------------------------- /frontend/components/ChatRoom/MessageForm/MessageForm.module.css: -------------------------------------------------------------------------------- 1 | .messageFormWrapper{ 2 | position: absolute; 3 | bottom: 0; 4 | margin-bottom: 10px; 5 | width: 310px; 6 | left: 50%; 7 | transform: translateX(-50%); 8 | margin-left: -5px; 9 | 10 | } 11 | 12 | .emojiInputWrapper{ 13 | 14 | display: flex ; 15 | align-items: center; 16 | margin-right: -10px; 17 | } 18 | 19 | 20 | .messageSubmit { 21 | width: 100%; 22 | background-color: #3A3A3D; 23 | border: solid #3A3A3D 1px; 24 | border-radius: 6px; 25 | padding: 5px; 26 | height: 30px; 27 | font-weight: 700; 28 | color: white; 29 | 30 | } 31 | 32 | 33 | .messageSubmit:focus { 34 | background-color: black; 35 | border: solid #9147FF 3px; 36 | border-radius: 8px; 37 | outline: none; 38 | } 39 | 40 | 41 | .submitButtonWrapper { 42 | display: flex; 43 | padding-top: 10px; 44 | justify-content: space-between; 45 | align-items: center; 46 | background-color: #18181B; 47 | 48 | 49 | } 50 | 51 | .chatButton { 52 | 53 | width: 50px; 54 | height: 30px; 55 | color: white; 56 | font-weight: 600; 57 | background-color: #9147FF; 58 | border: none; 59 | border-radius: 7px; 60 | margin-right: -12px; 61 | 62 | } 63 | .chatButton:hover { 64 | opacity: .7; 65 | } 66 | 67 | .pointIconWrapper{ 68 | width: 30px; 69 | height: 30px; 70 | display: flex; 71 | justify-content: center; 72 | align-items: center; 73 | margin-left: 5px; 74 | border-radius: 5px; 75 | } 76 | 77 | .pointIconWrapper:hover { 78 | background-color: #3A3A3D; 79 | 80 | } 81 | 82 | .pointIcon{ 83 | font-size: large; 84 | color: #C2C2C4; 85 | 86 | } 87 | 88 | 89 | .emojiBtn { 90 | position: absolute; 91 | right: -5px; 92 | font-size: 15px; 93 | width: 30px; 94 | height: 30px; 95 | display: flex; 96 | justify-content: center; 97 | align-items: center; 98 | border-radius: 5px; 99 | color: #C2C2C4; 100 | /* top: 50%; */ 101 | /* transform: translateY(50%) */ 102 | } 103 | 104 | .emojiBtn:hover { 105 | background-color: grey; 106 | } -------------------------------------------------------------------------------- /frontend/components/Channels/ChannelShow/FollowButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 3 | import { faHeart, faHeartBroken } from '@fortawesome/free-solid-svg-icons' 4 | import classes from './ChannelShow.module.css' 5 | 6 | 7 | 8 | const FollowButton = ({ currentUserId, followChannel, unfollowChannel, channel, follows }) => { 9 | 10 | 11 | 12 | 13 | const handleFollowClick = () => { 14 | if (currentUserId) { 15 | followChannel() 16 | } 17 | } 18 | 19 | const handleUnfollowClick = () => { 20 | if (currentUserId) { 21 | unfollowChannel(channel.id) 22 | } 23 | } 24 | 25 | 26 | return ( 27 | <> 28 | { 29 | isChannelOwner(currentUserId, channel) ?
: 30 | currentlyFollowing(currentUserId, channel, follows) ? ( 31 |
32 | 33 |
Unfollow
34 | 35 |
) : ( 36 | 37 |
38 | 39 |
Follow
40 | 41 |
42 | 43 | ) 44 | } 45 | 46 | ) 47 | 48 | } 49 | 50 | 51 | function isChannelOwner(currentUserId, channel) { 52 | if (currentUserId && channel && currentUserId === channel.id) { 53 | return true 54 | } 55 | return false 56 | } 57 | 58 | 59 | function currentlyFollowing(currentUserId, channel, follows) { 60 | if (!currentUserId) { 61 | return false 62 | } else { 63 | if (channel && follows.indexOf(channel.id) !== -1) { 64 | return true 65 | } else { 66 | return false 67 | } 68 | } 69 | } 70 | 71 | 72 | export default FollowButton; -------------------------------------------------------------------------------- /frontend/actions/session_actions.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER' 2 | export const RECEIVE_NEW_USER = 'RECEIVE_NEW_USER' 3 | export const LOGOUT_CURRENT_USER = 'LOGOUT_CURRENT_USER' 4 | export const RECEIVE_SESSION_ERRORS = 'RECEIVE_SESSION_ERRORS' 5 | export const CLEAR_SESSION_ERRORS = 'CLEAR_SESSION_ERRORS' 6 | 7 | import * as SessionAPIUtil from '../util/session_api_util' 8 | import {closeModal} from './modal_actions' 9 | 10 | const receiveCurrentUser = (user) => { 11 | 12 | return { 13 | type: RECEIVE_CURRENT_USER, 14 | user 15 | } 16 | } 17 | 18 | 19 | const receiveNewUser = (payload) => { 20 | 21 | return { 22 | type: RECEIVE_NEW_USER, 23 | payload 24 | } 25 | } 26 | 27 | 28 | const logoutCurrentUser = () => { 29 | return { 30 | type: LOGOUT_CURRENT_USER 31 | } 32 | } 33 | 34 | 35 | const receiveSessionErrors = (errors) => { 36 | 37 | return { 38 | type: RECEIVE_SESSION_ERRORS, 39 | errors 40 | } 41 | } 42 | 43 | 44 | export const clearSessionErrors = () => { 45 | return { 46 | type: CLEAR_SESSION_ERRORS 47 | } 48 | } 49 | 50 | 51 | 52 | export const signup = (user) => dispatch => { 53 | 54 | return SessionAPIUtil.createUser(user) 55 | .then((payload) => dispatch(receiveNewUser(payload))) 56 | .then(() => dispatch(closeModal())) 57 | .fail(err => dispatch(receiveSessionErrors(err))) 58 | 59 | 60 | 61 | } 62 | 63 | 64 | 65 | 66 | export const login = (user) => dispatch => { 67 | 68 | return SessionAPIUtil.createSession(user) 69 | .then((user) => dispatch(receiveCurrentUser(user))) 70 | .then(() => dispatch(closeModal())) 71 | .fail(err => dispatch(receiveSessionErrors(err))) 72 | 73 | 74 | } 75 | 76 | 77 | export const logout = () => dispatch => { 78 | 79 | return SessionAPIUtil.deleteSession() 80 | .then(() => dispatch(logoutCurrentUser())) 81 | 82 | 83 | 84 | 85 | } 86 | 87 | 88 | export const clearErrors = () => dispatch => { 89 | 90 | return () => { 91 | dispatch(clearSessionErrors()) 92 | } 93 | 94 | } -------------------------------------------------------------------------------- /frontend/components/Categories/CategoryShow.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react' 3 | import { connect } from 'react-redux' 4 | import { withRouter } from 'react-router-dom' 5 | import { requestVods, clearVods } from '../../actions/vod_actions' 6 | import { requestCategory } from '../../actions/category_actions' 7 | import classes from './CategoryShow.module.css' 8 | 9 | import VodIndexItem from '../Vods/VodsIndex/VodIndexItem' 10 | 11 | class CategoryShow extends React.Component { 12 | 13 | componentDidMount() { 14 | const { categoryName } = this.props.match.params 15 | this.props.requestVods({ 16 | category: categoryName, 17 | }) 18 | this.props.requestCategory(categoryName) 19 | } 20 | 21 | 22 | componentWillUnmount() { 23 | this.props.clearVods() 24 | } 25 | 26 | 27 | render() { 28 | const { categories, vods, channels } = this.props 29 | return ( 30 |
31 |
32 | { 33 | categories[0] ? ( 34 |
35 | 36 |
37 |

{categories[0].name}

38 |
{categories[0].description}
39 |
40 |
41 | ) : null 42 | } 43 |
44 |
45 |
46 |

Videos

47 |

{vods.length}

48 |
49 |
50 | { 51 | vods.map((vod) => { 52 | return 53 | }) 54 | } 55 |
56 |
57 | ) 58 | } 59 | } 60 | 61 | 62 | const mSTP = (state) => { 63 | return { 64 | vods: Object.values(state.entities.vods), 65 | channels: state.entities.channels, 66 | categories: Object.values(state.entities.categories), 67 | } 68 | } 69 | 70 | 71 | const mDTP = (dispatch) => { 72 | return { 73 | requestVods: (filter) => dispatch(requestVods(filter)), 74 | clearVods: () => dispatch(clearVods()), 75 | requestCategory: (name) => dispatch(requestCategory(name)), 76 | } 77 | } 78 | 79 | 80 | export default withRouter(connect(mSTP, mDTP)(CategoryShow)) 81 | -------------------------------------------------------------------------------- /frontend/components/Session/DemoForm/DemoForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withRouter } from 'react-router-dom' 3 | import { connect } from 'react-redux' 4 | import Typist from 'react-typist' 5 | import { login } from '../../../actions/session_actions' 6 | import classes from '../SessionForm.module.css' 7 | import TabNavs from '../TabNavs/TabsNav' 8 | /* eslint-disable */ 9 | 10 | 11 | class DemoForm extends React.Component { 12 | constructor(props) { 13 | super(props) 14 | this.state = { 15 | username: this.props.demoUser.username, 16 | password: '12345678', 17 | } 18 | 19 | this.handleSubmit = this.handleSubmit.bind(this) 20 | } 21 | 22 | 23 | handleSubmit() { 24 | this.props.login(this.state) 25 | } 26 | 27 | 28 | render() { 29 | 30 | return ( 31 |
32 | 33 | twitch-logo-font 34 | 35 | 40 |
41 | 49 | 50 | 59 | 60 | 61 |
62 |
63 | ) 64 | } 65 | } 66 | 67 | 68 | const mSTP = (state) => { 69 | 70 | return { 71 | demoUser: state.entities.users[state.ui.demoUserId], 72 | } 73 | } 74 | 75 | 76 | const mDTP = (dispatch) => { 77 | 78 | return { 79 | login: (user) => dispatch(login(user)), 80 | } 81 | } 82 | 83 | 84 | export default withRouter(connect(mSTP, mDTP)(DemoForm)) 85 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | ruby '2.5.1' 5 | 6 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 7 | gem 'rails', '~> 5.2.4', '>= 5.2.4.2' 8 | # Use postgresql as the database for Active Record 9 | gem 'pg', '>= 0.18', '< 2.0' 10 | # Use Puma as the app server 11 | gem 'puma', '~> 3.11' 12 | # Use SCSS for stylesheets 13 | gem 'sass-rails', '~> 5.0' 14 | # Use Uglifier as compressor for JavaScript assets 15 | gem 'uglifier', '>= 1.3.0' 16 | # See https://github.com/rails/execjs#readme for more supported runtimes 17 | # gem 'mini_racer', platforms: :ruby 18 | 19 | # Use CoffeeScript for .coffee assets and views 20 | gem 'coffee-rails', '~> 4.2' 21 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 22 | gem 'jbuilder', '~> 2.5' 23 | # Use Redis adapter to run Action Cable in production 24 | gem 'redis', '~> 4.0' 25 | # Use ActiveModel has_secure_password 26 | gem 'bcrypt', '~> 3.1.7' 27 | 28 | gem 'validates_email_format_of' 29 | 30 | # Use ActiveStorage variant 31 | # gem 'mini_magick', '~> 4.8' 32 | gem 'jquery-rails' 33 | # Use Capistrano for deployment 34 | # gem 'capistrano-rails', group: :development 35 | gem "aws-sdk-s3", require: false 36 | # Reduces boot times through caching; required in config/boot.rb 37 | gem 'bootsnap', '>= 1.1.0', require: false 38 | 39 | group :development, :test do 40 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 41 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] 42 | gem 'better_errors' 43 | gem 'binding_of_caller' 44 | gem 'pry-rails' 45 | gem 'annotate' 46 | gem 'jquery-rails' 47 | 48 | end 49 | 50 | group :development do 51 | # Access an interactive console on exception pages or by calling 'console' anywhere in the code. 52 | gem 'web-console', '>= 3.3.0' 53 | gem 'listen', '>= 3.0.5', '< 3.2' 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 | group :test do 60 | # Adds support for Capybara system testing and selenium driver 61 | gem 'capybara', '>= 2.15' 62 | gem 'selenium-webdriver' 63 | # Easy installation and use of chromedriver to run system tests with Chrome 64 | gem 'chromedriver-helper' 65 | end 66 | 67 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 68 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 69 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | config.web_console.whitelisted_ips = '10.0.0.101' 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable/disable caching. By default caching is disabled. 18 | # Run rails dev:cache to toggle caching. 19 | if Rails.root.join('tmp', 'caching-dev.txt').exist? 20 | config.action_controller.perform_caching = true 21 | 22 | config.cache_store = :memory_store 23 | config.public_file_server.headers = { 24 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 25 | } 26 | else 27 | config.action_controller.perform_caching = false 28 | 29 | config.cache_store = :null_store 30 | end 31 | 32 | 33 | 34 | # Store uploaded files on the local file system (see config/storage.yml for options) 35 | config.active_storage.service = :amazon_dev 36 | 37 | config.active_storage.service = :amazon_prod 38 | 39 | # Don't care if the mailer can't send. 40 | config.action_mailer.raise_delivery_errors = false 41 | 42 | config.action_mailer.perform_caching = false 43 | 44 | # Print deprecation notices to the Rails logger. 45 | config.active_support.deprecation = :log 46 | 47 | # Raise an error on page load if there are pending migrations. 48 | config.active_record.migration_error = :page_load 49 | 50 | # Highlight code that triggered database queries in logs. 51 | config.active_record.verbose_query_logs = true 52 | 53 | # Debug mode disables concatenation and preprocessing of assets. 54 | # This option may cause significant delays in view rendering with a large 55 | # number of complex assets. 56 | config.assets.debug = true 57 | 58 | # Suppress logger output for asset requests. 59 | config.assets.quiet = true 60 | 61 | # Raises error for missing translations 62 | # config.action_view.raise_on_missing_translations = true 63 | 64 | # Use an evented file watcher to asynchronously detect changes in source code, 65 | # routes, locales, etc. This feature depends on the listen gem. 66 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 67 | end 68 | -------------------------------------------------------------------------------- /frontend/components/MainNav/MainNav.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import React from 'react' 4 | import classes from './MainNav.module.css' 5 | import { withRouter } from 'react-router-dom' 6 | import Categories from '../Categories/Categories' 7 | import ChannelIndex from '../Channels/ChannelIndex' 8 | 9 | class MainNav extends React.Component { 10 | 11 | constructor(props) { 12 | super(props) 13 | this.state = { 14 | nextTab: 'channels' 15 | } 16 | 17 | this.changeTab = this.changeTab.bind(this) 18 | } 19 | 20 | 21 | // componentDidUpdate(prevProps, prevState) { 22 | 23 | // if (this.state.nextTab === "videos" && prevState.nextTab !== "videos") { 24 | // this.props.history.push(`/channels/${this.props.channelId}/${this.props.channelName}/videos`) 25 | // } 26 | // } 27 | 28 | changeTab(tab) { 29 | // this.props.history.push(`/channels/${this.props.channelId}/${this.props.channelName}/${tab}`) 30 | this.setState({ nextTab: tab }) 31 | } 32 | 33 | 34 | 35 | render() { 36 | 37 | let homeClasses = [] 38 | let videoClasses = [] 39 | 40 | switch (this.state.nextTab) { 41 | case "channels": 42 | homeClasses.push(classes.tabSelected) 43 | break; 44 | case "categories": 45 | videoClasses.push(classes.tabSelected) 46 | break; 47 | default: 48 | break; 49 | } 50 | 51 | return ( 52 |
53 |
54 |

Browse

55 |
    56 |
  • this.changeTab("channels")} className={homeClasses.join(' ')}> 57 |

    Channels

    58 |
  • 59 | 60 |
  • this.changeTab("categories")} className={videoClasses.join(' ')}> 61 |

    Categories

    62 |
  • 63 |
64 | 65 | { 66 | 67 | this.state.nextTab === 'channels' ? ( 68 | 69 | ) : ( 70 | 71 | ) 72 | 73 | } 74 |
75 |
76 | 77 | ) 78 | } 79 | } 80 | 81 | 82 | 83 | export default withRouter(MainNav); -------------------------------------------------------------------------------- /frontend/components/Channels/ChannelShow/ChannelNavs.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classes from './ChannelNavs.module.css' 3 | import {withRouter} from 'react-router-dom' 4 | 5 | class ChannelNavs extends React.Component { 6 | 7 | constructor(props) { 8 | super(props) 9 | let nextTab = 'home' 10 | if (props.location.state) { 11 | nextTab = props.location.state.tab 12 | } 13 | 14 | this.state = { 15 | nextTab: nextTab 16 | } 17 | 18 | this.changeTab = this.changeTab.bind(this) 19 | } 20 | 21 | componentDidMount() { 22 | this.setState({nextTab: this.getTabFromUrl()}) 23 | } 24 | 25 | getTabFromUrl() { 26 | let url = this.props.location.pathname 27 | let i = url.length 28 | while (i > 0) { 29 | if (url[i] === '/'){ 30 | return url.slice(i+1) 31 | } 32 | i-- 33 | } 34 | return null 35 | } 36 | 37 | 38 | changeTab(tab) { 39 | this.props.history.push(`/channels/${this.props.channelId}/${this.props.channelName}/${tab}`) 40 | this.setState({nextTab: tab}) 41 | } 42 | 43 | 44 | 45 | render() { 46 | 47 | let homeClasses = [] 48 | let videoClasses = [] 49 | let followersClasses = [] 50 | 51 | switch (this.state.nextTab) { 52 | case "home": 53 | homeClasses.push(classes.tabSelected) 54 | break; 55 | case "videos": 56 | videoClasses.push(classes.tabSelected) 57 | break; 58 | case 'followers': 59 | followersClasses.push(classes.tabSelected) 60 | break; 61 | default: 62 | break; 63 | } 64 | 65 | return( 66 |
    67 |
  • this.changeTab("home")} className={homeClasses.join(' ')}> 68 |

    Home

    69 |
  • 70 | 71 |
  • this.changeTab("videos")} className={videoClasses.join(' ')}> 72 |

    Videos

    73 |
  • 74 | 75 |
  • this.changeTab("followers")} className={followersClasses.join(' ')}> 76 |

    Followers

    77 |
  • 78 |
79 | 80 | ) 81 | } 82 | } 83 | 84 | 85 | 86 | export default withRouter(ChannelNavs); -------------------------------------------------------------------------------- /frontend/components/SessionControls/SessionControls.module.css: -------------------------------------------------------------------------------- 1 | .controlsWrapper{ 2 | display: flex; 3 | align-items: center; 4 | } 5 | 6 | 7 | .login { 8 | padding: 10px; 9 | background-color: #3A3A3D; 10 | border: none; 11 | color: whitesmoke; 12 | margin-right: 12px; 13 | text-decoration: none; 14 | border-radius: 5px; 15 | font-weight: 800; 16 | font-size: 14px; 17 | font-family: 'Ubuntu', sans-serif; 18 | 19 | 20 | 21 | } 22 | .login:hover { 23 | opacity: .7; 24 | } 25 | 26 | .signup { 27 | padding: 10px; 28 | background-color: #9147FF; 29 | border: none; 30 | color: whitesmoke; 31 | text-decoration: none; 32 | border-radius: 5px; 33 | font-weight: 800; 34 | font-size: 14px; 35 | margin-right: 12px; 36 | font-family: 'Ubuntu', sans-serif; 37 | 38 | } 39 | 40 | .signup:hover { 41 | opacity: .7; 42 | } 43 | 44 | .logout { 45 | padding: 10px; 46 | background-color: #3A3A3D; 47 | border: none; 48 | color: whitesmoke; 49 | margin-right: 5px; 50 | text-decoration: none; 51 | border-radius: 6px; 52 | font-weight: 750; 53 | font-size: 12px; 54 | 55 | } 56 | 57 | 58 | .welcomeWrapper { 59 | display: flex; 60 | align-items: center; 61 | } 62 | 63 | .welcomeMessage { 64 | color: white; 65 | margin-right: 20px; 66 | } 67 | 68 | .userIconWrapper { 69 | display: flex; 70 | justify-content: center; 71 | align-self: center; 72 | border-radius: 50%; 73 | z-index: 6000; 74 | 75 | } 76 | .userIcon{ 77 | border-radius: 50%; 78 | width: 40px; 79 | height: 40px; 80 | cursor: pointer; 81 | 82 | } 83 | 84 | .userIconLoggedOut{ 85 | color: white; 86 | font-size: large; 87 | margin-left: 10px; 88 | font-size: 19px; 89 | padding: 8px; 90 | border-radius: 50%; 91 | background-color: #00BAA3; 92 | cursor: pointer; 93 | } 94 | 95 | 96 | 97 | /* .dropDown{ 98 | position: absolute; 99 | width: 160px; 100 | height: 300px; 101 | background-color: #1F1F23; 102 | right: 5px; 103 | top: 50px; 104 | color: white; 105 | border-radius: 7px; 106 | z-index: 99999; 107 | } 108 | 109 | .dropDown > li { 110 | font-size: 13px; 111 | list-style: none; 112 | color:white; 113 | margin: 5px; 114 | padding: 10px; 115 | } 116 | 117 | .dropDown > li:hover { 118 | background-color: rgb(58,58,61); 119 | 120 | } 121 | 122 | .hide { 123 | display: none; 124 | } 125 | 126 | .show { 127 | display: block; 128 | } */ -------------------------------------------------------------------------------- /frontend/components/SessionControls/DropDownMenu/DropDownMenu.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react' 3 | import { withRouter } from 'react-router-dom' 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 5 | import { faVideo, faCog, faSignInAlt, faSignOutAlt } from '@fortawesome/free-solid-svg-icons' 6 | import classes from './DropDownMenu.module.css' 7 | 8 | 9 | const DropDownMenu = ({ currentUser, showMenu, logout, login, toggle, history, currentChannel }) => { 10 | 11 | const menuClasses = [classes.dropDown] 12 | if (showMenu) { 13 | menuClasses.push(classes.show) 14 | } else { 15 | menuClasses.push(classes.hide) 16 | } 17 | 18 | function handleClick(type) { 19 | switch (type) { 20 | case 'login': 21 | login() 22 | break 23 | case 'logout': 24 | logout() 25 | break 26 | case 'showChannel': 27 | history.push(`/channels/${currentChannel.id}/${currentUser.username}/home`) 28 | break 29 | case 'dashboard': 30 | history.push(`/${currentUser.username}/dashboard`) 31 | break 32 | default: 33 | break 34 | } 35 | toggle() 36 | } 37 | 38 | 39 | return ( 40 | 41 | currentUser ? ( 42 |
    e.stopPropagation()}> 43 |
    44 |
    handleClick('dashboard')} className={classes.userIconWrapper}> 45 | 46 |
    47 |
    {currentUser.username}
    48 |
    49 |
    50 |
  • handleClick('showChannel')}> 51 | 52 | Channel 53 |
  • 54 |
  • handleClick('dashboard')}> 55 | 56 | Dashboard 57 |
  • 58 |
  • handleClick('logout')}> 59 | 60 | Sign Out 61 |
  • 62 |
63 | ) : ( 64 |
    e.stopPropagation()}> 65 |
  • handleClick('login')}> 66 | 67 | Sign In 68 |
  • 69 |
70 | ) 71 | ) 72 | } 73 | 74 | 75 | export default withRouter(DropDownMenu) 76 | -------------------------------------------------------------------------------- /frontend/components/NavBar/SearchBar.module.css: -------------------------------------------------------------------------------- 1 | 2 | .searchForm{ 3 | position: absolute; 4 | left: calc(50% - 12.5vw); 5 | display: flex; 6 | width: 25vw; 7 | height: 35px; 8 | z-index: 9999; 9 | margin-left: 2vw; 10 | /* margin-left: -6vw; */ 11 | } 12 | .searchForm > input { 13 | flex-grow: 2; 14 | background-color: #3A3A3D; 15 | border-top-left-radius: 5px; 16 | border-bottom-left-radius: 5px; 17 | border: none; 18 | padding-left: 10px; 19 | color: white; 20 | z-index: 9999; 21 | 22 | 23 | } 24 | 25 | .searchForm:focus { 26 | border: solid #9147FF 2.2px; 27 | outline: none; 28 | 29 | } 30 | 31 | .searchForm > input:focus { 32 | border: solid #9147FF 2.2px; 33 | outline: none; 34 | background-color: black; 35 | } 36 | 37 | .searchForm > button { 38 | height: 100%; 39 | margin-left: 3px; 40 | z-index: 9999; 41 | 42 | } 43 | 44 | 45 | .searchResults { 46 | position: absolute; 47 | z-index: 8888; 48 | padding: 5px; 49 | padding-left: 5px; 50 | width: 25vw; 51 | margin-top: -6px; 52 | margin-left: -5px; 53 | min-height: 250px; 54 | display: flex; 55 | flex-direction: column; 56 | border-radius: 5px; 57 | /* background-color: blue; */ 58 | background-color: rgb(24,24,27); 59 | } 60 | 61 | .searchResults > li { 62 | list-style: none; 63 | color: white; 64 | padding: 10px; 65 | display: flex; 66 | align-items: center; 67 | cursor: pointer; 68 | 69 | } 70 | 71 | .searchResults > li:hover { 72 | background-color: rgb(58,58,61); 73 | 74 | } 75 | 76 | .searchResults > li:first-child { 77 | margin-top: 45px; 78 | } 79 | 80 | .searchResults > li > span { 81 | margin-left: 10px; 82 | font-weight: 700; 83 | } 84 | 85 | .logo{ 86 | border-radius: 50%; 87 | width: 40px; 88 | height: 40px; 89 | 90 | } 91 | 92 | .overlay { 93 | position: absolute; 94 | /* width: 100vw; 95 | height: 400vh; */ 96 | left: 0; 97 | right: 0; 98 | top: 0; 99 | bottom: 0; 100 | background-color: grey; 101 | opacity: 0; 102 | } 103 | 104 | 105 | .searchIconWrapper { 106 | height: 100%; 107 | margin-left: 3px; 108 | z-index: 9999; 109 | background-color: #3A3A3D; 110 | padding-left: 10px; 111 | padding-right: 10px; 112 | border-top-right-radius: 5px; 113 | border-bottom-right-radius: 5px; 114 | display: flex; 115 | justify-content: center; 116 | align-items: center; 117 | 118 | } 119 | 120 | .searchIcon { 121 | width: 55px; 122 | height: 55px; 123 | color:rgb(239,239,241) 124 | } 125 | 126 | ::placeholder { 127 | color: rgb(178,178,180); 128 | font-weight: 700; 129 | font-size: 15px; 130 | 131 | } 132 | 133 | ::-ms-input-placeholder { 134 | color: rgb(178,178,180); 135 | font-weight: 700; 136 | font-size: 15px; 137 | } -------------------------------------------------------------------------------- /frontend/components/NavBar/SearchBar.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react' 3 | import { connect } from 'react-redux' 4 | import { withRouter } from 'react-router-dom' 5 | import classes from './SearchBar.module.css' 6 | import { clearChannels, requestSearchedChannels } from '../../actions/channel_actions' 7 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 8 | import { faSearch } from '@fortawesome/free-solid-svg-icons' 9 | 10 | class SearchBar extends React.Component { 11 | constructor(props) { 12 | super(props) 13 | this.state = { 14 | searchInput: '', 15 | } 16 | this.handleUpdate = this.handleUpdate.bind(this) 17 | this.handleClick = this.handleClick.bind(this) 18 | } 19 | 20 | componentDidUpdate(prevState) { 21 | if (!prevState.searchInput && this.state.searchInput) { 22 | document.getElementById('overlay').addEventListener('click', () => this.setState({searchInput: ''})) 23 | } 24 | } 25 | 26 | handleUpdate(e) { 27 | this.setState( 28 | { searchInput: e.currentTarget.value }, 29 | () => { 30 | if (this.state.searchInput) this.props.requestSearchedChannels({ searchChannels: true, searchInput: this.state.searchInput }) 31 | }, 32 | ) 33 | } 34 | 35 | handleClick(e, channel) { 36 | this.props.history.push(`/channels/${channel.id}/${channel.channelName}/home`) 37 | this.setState({searchInput: ''}) 38 | } 39 | 40 | 41 | render() { 42 | const { searchInput } = this.state 43 | return ( 44 | <> 45 | { 46 | searchInput ?
: null 47 | } 48 |
49 | { 50 | searchInput ? ( 51 |
    52 | { 53 | this.props.channels.map((channel) => { 54 | return ( 55 |
  • this.handleClick(e, channel)}> 56 | 57 | {channel.channelName} 58 |
  • 59 | ) 60 | }) 61 | } 62 | 63 |
64 | ) : null 65 | } 66 | 67 |
68 | 69 | 70 |
71 |
72 | 73 | ) 74 | } 75 | } 76 | 77 | 78 | const mSTP = (state) => { 79 | return { 80 | channels: state.entities.channels.searched ? Object.values(state.entities.channels.searched) : [], 81 | } 82 | } 83 | 84 | 85 | const mDTP = (dispatch) => { 86 | return { 87 | clearChannels: () => dispatch(clearChannels()), 88 | requestSearchedChannels: (filter) => dispatch(requestSearchedChannels(filter)), 89 | } 90 | } 91 | 92 | 93 | export default withRouter(connect(mSTP, mDTP)(SearchBar)) 94 | -------------------------------------------------------------------------------- /frontend/components/Dashboard/Dashboard.module.css: -------------------------------------------------------------------------------- 1 | .dashboardWrapper { 2 | flex-grow: 6.0; 3 | display: flex; 4 | flex-direction: column; 5 | background-color: #0E0E10; 6 | align-items: flex-start; 7 | overflow-y: scroll; 8 | height: 1000px; 9 | 10 | } 11 | 12 | .dashboardWrapper > h1 { 13 | margin-top: 30px; 14 | margin-left: 20px; 15 | 16 | } 17 | 18 | .profileWrapper { 19 | 20 | margin-top: 30px; 21 | margin-left: 20px; 22 | 23 | } 24 | 25 | 26 | .profileWrapper > h2 { 27 | color: white; 28 | margin-bottom: 15px; 29 | 30 | } 31 | 32 | 33 | .profileForm { 34 | display: flex; 35 | align-items: center; 36 | width: 900px; 37 | height: 180px; 38 | background-color: #18181B; 39 | border: 1px solid #303032; 40 | border-radius: 7px; 41 | } 42 | 43 | 44 | .userIcon { 45 | margin-left: 20px; 46 | width: 100px; 47 | height: 100px; 48 | border-radius: 50%; 49 | } 50 | 51 | 52 | .dashboardWrapper > hr { 53 | width: 90%; 54 | margin-left: 20px; 55 | margin-top: 30px; 56 | background-color: rgb(48,48,50); 57 | height: .5px; 58 | border: none; 59 | 60 | } 61 | 62 | 63 | .profileBtnWrapper { 64 | position: relative; 65 | height: 30px; 66 | width: 160px; 67 | display: flex; 68 | cursor: pointer; 69 | 70 | 71 | 72 | } 73 | 74 | 75 | .profileInput { 76 | position: absolute; 77 | opacity: 0; 78 | top: 0; 79 | left: 0; 80 | right: 0; 81 | bottom: 0; 82 | width: 160px; 83 | cursor: pointer; 84 | 85 | } 86 | 87 | 88 | .profileBtnWrapper > label { 89 | position: absolute; 90 | top: 0; 91 | right: 0; 92 | left: 0; 93 | bottom: 0; 94 | background-color: #3A3A3D; 95 | color: rgb(226,226,228); 96 | display: flex; 97 | justify-content: center; 98 | align-items: center; 99 | border-radius: 6px; 100 | font-size: 12px; 101 | font-weight: 700; 102 | padding: 5px; 103 | cursor: pointer; 104 | 105 | 106 | } 107 | 108 | .profileBtnWrapper > label:hover { 109 | opacity: .7; 110 | } 111 | 112 | .btnDirectionWrapper { 113 | margin-left: 20px; 114 | display: flex; 115 | flex-direction: column; 116 | color: #C0C0C4; 117 | 118 | } 119 | 120 | .btnDirectionWrapper > h5 { 121 | margin-top: 8px ; 122 | } 123 | 124 | 125 | 126 | .submitBtn{ 127 | background-color: #3A3A3D; 128 | color: rgb(226,226,228); 129 | /* margin-bottom: 25px;*/ 130 | margin-left: 170px; 131 | height: 30px; 132 | width: 100px; 133 | border-radius: 6px; 134 | border: none; 135 | cursor: pointer; 136 | 137 | } 138 | 139 | .submitBtn:hover { 140 | opacity: .5; 141 | } 142 | 143 | .disabledBtn { 144 | background-color: #3A3A3D; 145 | opacity: .5; 146 | } 147 | 148 | .imgPreviewWrapper { 149 | margin-left: 70px; 150 | display: flex; 151 | flex-direction: column; 152 | align-items: center; 153 | justify-content: center; 154 | border: 2px solid #303032; 155 | border-radius: 5px; 156 | padding: 15px; 157 | 158 | } 159 | 160 | .imgPreviewWrapper > h3 { 161 | color: white; 162 | 163 | } 164 | 165 | .imgPreviewWrapper > img { 166 | margin-top: 10px; 167 | width: 80px; 168 | height: 80px; 169 | border-radius: 50%; 170 | } -------------------------------------------------------------------------------- /frontend/components/ChatRoom/ChatRoom.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import classes from './ChatRoom.module.css' 4 | 5 | import { withRouter } from 'react-router-dom' 6 | import { connect } from 'react-redux' 7 | import { openModal } from '../../actions/modal_actions' 8 | 9 | import MessageForm from './MessageForm/MessageForm' 10 | 11 | 12 | class ChatRoom extends React.Component { 13 | constructor(props) { 14 | super(props) 15 | this.state = { 16 | messages: [], 17 | } 18 | this.bottom = React.createRef() 19 | } 20 | 21 | componentDidMount() { 22 | // Subscribe to a channel with a given id. Define methods to receive and send data from/to socket 23 | App.cable.subscriptions.create( 24 | { channel: 'ChatRoomsChannel', id: this.props.match.params.channelName }, 25 | { 26 | received: (data) => { 27 | this.setState({ 28 | messages: [...this.state.messages, [data.message, data.username, data.color]], 29 | }) 30 | }, 31 | speak: function(data) { 32 | return this.perform('speak', data) 33 | }, 34 | }, 35 | ) 36 | } 37 | 38 | componentDidUpdate(prevProps) { 39 | if (prevProps.match.params.channelName !== this.props.match.params.channelName) { 40 | App.cable.disconnect() 41 | App.cable.subscriptions.create( 42 | { channel: 'ChatRoomsChannel', id: this.props.match.params.channelName }, 43 | { 44 | received: (data) => { 45 | this.setState({ 46 | messages: [...this.state.messages, [data.message, data.username, data.color]], 47 | }) 48 | }, 49 | speak: function(data) { 50 | return this.perform("speak", data); 51 | }, 52 | }, 53 | ) 54 | this.setState({ messages: [] }) 55 | } 56 | 57 | if (this.bottom.current !== null) { 58 | this.bottom.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' }); 59 | // this.bottom.current.scrollIntoView(); 60 | } 61 | } 62 | 63 | render() { 64 | const messageList = this.state.messages.map((message, idx) => { 65 | return ( 66 |
  • 67 |

    68 | 69 | {`${message[1]}: `} 70 | 71 | 72 | {`${message[0]}`} 73 | 74 |

    75 |
    76 |
  • 77 | ) 78 | }) 79 | 80 | return ( 81 |
    82 |
    83 | STREAM CHAT {this.props.channelId} 84 |
    85 |
    86 | {messageList} 87 |
    88 | 89 |
    90 | ) 91 | } 92 | } 93 | 94 | 95 | const mSTP = (state) => { 96 | return { 97 | currentUser: state.entities.users[state.session.currentUserId], 98 | } 99 | } 100 | 101 | const mDTP = (dispatch) => { 102 | return { 103 | openModal: (form) => dispatch(openModal(form)), 104 | } 105 | } 106 | 107 | 108 | export default withRouter(connect(mSTP, mDTP)(ChatRoom)) 109 | -------------------------------------------------------------------------------- /frontend/components/Session/SessionForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withRouter } from 'react-router-dom' 3 | import classes from './SessionForm.module.css' 4 | 5 | import TabNavs from './TabNavs/TabsNav' 6 | import ErrorBox from './ErrorBox/ErrorBox' 7 | /* eslint-disable */ 8 | 9 | 10 | class SessionForm extends React.Component { 11 | constructor(props) { 12 | super(props) 13 | this.state = { 14 | username: '', 15 | password: '', 16 | confirmPassword: '', 17 | passwordMatch: true, 18 | } 19 | this.handleSubmit = this.handleSubmit.bind(this) 20 | } 21 | 22 | componentDidMount() { 23 | const input = document.getElementById('usernameLogin') 24 | input.focus() 25 | } 26 | 27 | update(type) { 28 | return (e) => { 29 | this.setState({ [type]: e.currentTarget.value }) 30 | } 31 | } 32 | 33 | handleSubmit(e) { 34 | e.preventDefault() 35 | if (this.state.confirmPassword !== this.state.password && this.props.formType === 'Sign Up') { 36 | this.setState({ passwordMatch: false }) 37 | } else { 38 | this.props.processForm({ 39 | username: this.state.username, 40 | password: this.state.password, 41 | }) 42 | } 43 | } 44 | 45 | render() { 46 | let disableBtn = false 47 | const submitBtnClasses = [classes.formSubmit] 48 | if (this.state.username === '' || this.state.password === '') { 49 | disableBtn = true 50 | submitBtnClasses.push(classes.disableSubmit) 51 | } 52 | return ( 53 |
    54 | { 55 | this.props.formType === 'Sign Up' ? ( 56 | 57 | twitch-logo-font 58 | 59 | ) : ( 60 | 61 | twitch-logo-font 62 | 63 | ) 64 | } 65 | 66 | 67 |
    68 | 72 | 76 | { 77 | this.props.formType === 'Sign Up' ? ( 78 | 82 | ) : null 83 | } 84 | 85 |
    86 |
    87 | ) 88 | } 89 | } 90 | 91 | 92 | export default withRouter(SessionForm) 93 | --------------------------------------------------------------------------------