├── 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 | Close
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 |
16 |
17 |
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 |
20 |
21 |
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 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Browse
21 |
22 |
23 |
24 |
25 | About
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
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 |
15 |
16 | ) : (
)
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 |
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 |
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 |
58 |
59 | ) : (
60 |
61 |
62 |
63 | )
64 | }
65 |
66 |
67 |
86 |
87 | )
88 | }
89 | }
90 |
91 |
92 | export default withRouter(SessionForm)
93 |
--------------------------------------------------------------------------------