├── .flaskenv ├── migrations ├── README ├── script.py.mako ├── alembic.ini ├── versions │ └── 20230203_213751_.py └── env.py ├── react-app ├── .env.example ├── public │ ├── favicon.ico │ └── index.html ├── .gitignore ├── src │ ├── components │ │ ├── auth │ │ │ ├── ProtectedRoute.js │ │ │ ├── LogoutButton.js │ │ │ ├── LoginForm.js │ │ │ └── SignUpForm.js │ │ ├── SingUpFormModal │ │ │ ├── index.js │ │ │ ├── SignupForm.css │ │ │ └── SignupForm.js │ │ ├── LoginFormModal │ │ │ ├── index.js │ │ │ ├── LoginForm.js │ │ │ └── LoginForm.css │ │ ├── UsersList2.js │ │ ├── UsersList.js │ │ ├── FollowList.js │ │ ├── FollowButton.js │ │ ├── CaptionEditForm.js │ │ ├── CommentEditForm.js │ │ ├── CommentForm.js │ │ ├── User.css │ │ ├── FollowFeed.js │ │ ├── TopCreators.js │ │ ├── User.js │ │ ├── sideBar2.js │ │ ├── SideBar.js │ │ ├── NavBar.js │ │ ├── FastUpload.js │ │ ├── FastForward.js │ │ ├── NavBar.css │ │ ├── FastForwardIndexItem.js │ │ └── FastForwards.css │ ├── index.js │ ├── context │ │ ├── Modal.css │ │ └── Modal.js │ ├── index.css │ ├── store │ │ ├── index.js │ │ ├── fastForwardDetails.js │ │ ├── session.js │ │ ├── fastForward.js │ │ ├── follower.js │ │ ├── likePosts.js │ │ └── comment.js │ └── App.js ├── README.md └── package.json ├── app ├── dev.db ├── models │ ├── __init__.py │ ├── db.py │ ├── follow.py │ ├── like_post.py │ ├── comment.py │ ├── fast_forward.py │ └── user.py ├── forms │ ├── __init__.py │ ├── fast_forward_edit_form.py │ ├── fast_forward_form.py │ ├── comment_form.py │ ├── login_form.py │ └── signup_form.py ├── config.py ├── api │ ├── clip_routes.py │ ├── like_routes.py │ ├── user_routes.py │ ├── follower_routes.py │ ├── comment_routes.py │ ├── auth_routes.py │ └── fast_forward_routes.py ├── seeds │ ├── __init__.py │ ├── users.py │ └── fast_forwards.py ├── aws.py └── __init__.py ├── .gitignore ├── instance └── dev.db ├── .env.example ├── requirements.txt ├── Pipfile └── README.md /.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=app 2 | FLASK_ENV=development -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /react-app/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_BASE_URL=http://localhost:5000 2 | -------------------------------------------------------------------------------- /app/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jessie-Baron/Fast-Forward/HEAD/app/dev.db -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | __pycache__/ 3 | *.py[cod] 4 | .venv 5 | .DS_Store 6 | .vscode/ 7 | -------------------------------------------------------------------------------- /instance/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jessie-Baron/Fast-Forward/HEAD/instance/dev.db -------------------------------------------------------------------------------- /react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jessie-Baron/Fast-Forward/HEAD/react-app/public/favicon.ico -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from .user import User 3 | from .comment import Comment 4 | from .like_post import LikePost 5 | from .fast_forward import FastForward 6 | from .db import environment, SCHEMA 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # No need for DATABASE_URL to be set if developing from within a devcontainer 2 | 3 | SECRET_KEY=lkasjdf09ajsdkfljalsiorj12n3490re9485309irefvn,u90818734902139489230 4 | DATABASE_URL=sqlite:///dev.db 5 | SCHEMA=flask_schema -------------------------------------------------------------------------------- /app/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from .login_form import LoginForm 2 | from .signup_form import SignUpForm 3 | from .fast_forward_form import FastForwardForm 4 | from .fast_forward_edit_form import FastForwardEditForm 5 | from .comment_form import CommentForm 6 | -------------------------------------------------------------------------------- /app/forms/fast_forward_edit_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField 3 | from wtforms.validators import DataRequired, Email, ValidationError 4 | 5 | 6 | class FastForwardEditForm(FlaskForm): 7 | caption = StringField('caption', validators=[DataRequired()]) 8 | -------------------------------------------------------------------------------- /app/models/db.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | import os 4 | environment = os.getenv("FLASK_ENV") 5 | SCHEMA = os.environ.get("SCHEMA") 6 | 7 | 8 | db = SQLAlchemy() 9 | 10 | # helper function for adding prefix to foreign key column references in production 11 | def add_prefix_for_prod(attr): 12 | if environment == "production": 13 | return f"{SCHEMA}.{attr}" 14 | else: 15 | return attr -------------------------------------------------------------------------------- /react-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* -------------------------------------------------------------------------------- /app/models/follow.py: -------------------------------------------------------------------------------- 1 | from .db import db, environment, SCHEMA, add_prefix_for_prod 2 | 3 | follows = db.Table( 4 | "follows", 5 | db.Model.metadata, 6 | db.Column("follower_id", db.Integer, db.ForeignKey(add_prefix_for_prod("users.id")), primary_key=True), 7 | db.Column("followed_id", db.Integer, db.ForeignKey(add_prefix_for_prod("users.id")), primary_key=True), 8 | ) 9 | 10 | if environment == 'production': 11 | follows.schema = SCHEMA 12 | -------------------------------------------------------------------------------- /react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | FastForward - Make Your Day 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /react-app/src/components/auth/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { Route, Redirect } from 'react-router-dom'; 4 | 5 | const ProtectedRoute = props => { 6 | const user = useSelector(state => state.session.user) 7 | return ( 8 | 9 | {(user)? props.children : } 10 | 11 | ) 12 | }; 13 | 14 | 15 | export default ProtectedRoute; 16 | -------------------------------------------------------------------------------- /react-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import './index.css'; 5 | import App from './App'; 6 | import configureStore from './store'; 7 | 8 | const store = configureStore(); 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | , 16 | document.getElementById('root') 17 | ); 18 | -------------------------------------------------------------------------------- /react-app/src/context/Modal.css: -------------------------------------------------------------------------------- 1 | #modal { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | left: 0; 6 | bottom: 0; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | z-index: 50; 11 | } 12 | 13 | #modal-background { 14 | position: fixed; 15 | top: 0; 16 | right: 0; 17 | left: 0; 18 | bottom: 0; 19 | background-color: rgba(0, 0, 0, 0.68); 20 | } 21 | 22 | #modal-content { 23 | position: absolute; 24 | } 25 | -------------------------------------------------------------------------------- /react-app/src/index.css: -------------------------------------------------------------------------------- 1 | /* TODO Add site wide styles */ 2 | 3 | body { 4 | font-family: Arial,Tahoma,PingFangSC,sans-serif; 5 | background-color: rgb(18, 18, 18);; 6 | } 7 | 8 | body::-webkit-scrollbar { 9 | width: 8px; 10 | } 11 | 12 | body::-webkit-scrollbar-thumb { 13 | width: 6px; 14 | height: 100%; 15 | border-radius: 3px; 16 | background: rgba(255, 255, 255, .08); 17 | } 18 | 19 | .master-follow-feed { 20 | color: white; 21 | margin-left: 40%; 22 | margin-top: 25%; 23 | width: 750px; 24 | } 25 | -------------------------------------------------------------------------------- /react-app/src/components/auth/LogoutButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { logout } from '../../store/session'; 4 | import { useHistory } from 'react-router-dom'; 5 | 6 | const LogoutButton = () => { 7 | const dispatch = useDispatch() 8 | const history = useHistory() 9 | const onLogout = async (e) => { 10 | await dispatch(logout()) 11 | .then(history.push('/')) 12 | }; 13 | 14 | return
Log out
; 15 | }; 16 | 17 | export default LogoutButton; 18 | -------------------------------------------------------------------------------- /app/forms/fast_forward_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField 3 | from wtforms.validators import DataRequired, Email, ValidationError 4 | 5 | def caption_is_not_too_long(form, field): 6 | caption = field.data 7 | if len(caption) >= 2500: 8 | print("error") 9 | raise ValidationError("Character limit exceeded") 10 | 11 | 12 | class FastForwardForm(FlaskForm): 13 | caption = StringField('caption', validators=[DataRequired(), caption_is_not_too_long]) 14 | url = StringField('url', validators=[DataRequired()]) 15 | -------------------------------------------------------------------------------- /react-app/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | Your React App will live here. You will need to run `npm install` to install all your dependencies before starting up the application. While in development, run this application from this location using `npm start`. 4 | 5 | No environment variables are needed to run this application in development, but be sure to set the REACT_APP_BASE_URL environment variable when you deploy! 6 | 7 | This app will be automatically built when you push to your main branch on Github. 8 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class Config: 5 | SECRET_KEY = os.environ.get('SECRET_KEY') 6 | SQLALCHEMY_TRACK_MODIFICATIONS = False 7 | # SQLAlchemy 1.4 no longer supports url strings that start with 'postgres' 8 | # (only 'postgresql') but heroku's postgres add-on automatically sets the 9 | # url in the hidden config vars to start with postgres. 10 | # so the connection uri must be updated here (for production) 11 | SQLALCHEMY_DATABASE_URI = os.environ.get( 12 | 'DATABASE_URL').replace('postgres://', 'postgresql://') 13 | SQLALCHEMY_ECHO = True 14 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} -------------------------------------------------------------------------------- /app/forms/comment_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField 3 | from wtforms.validators import DataRequired, ValidationError 4 | 5 | def body_is_not_empty(form, field): 6 | body = field.data 7 | if body == "": 8 | print("error") 9 | raise ValidationError("Can't submit an empty comment field") 10 | 11 | def body_is_not_too_long(form, field): 12 | body = field.data 13 | if len(body) >= 500: 14 | print("error") 15 | raise ValidationError("Character limit exceeded") 16 | 17 | 18 | class CommentForm(FlaskForm): 19 | body = StringField('body', validators=[DataRequired(), body_is_not_empty, body_is_not_too_long]) 20 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # These requirements were autogenerated by pipenv 3 | # To regenerate from the project's Pipfile, run: 4 | # 5 | # pipenv lock --requirements 6 | # 7 | 8 | -i https://pypi.org/simple 9 | alembic==1.6.5 10 | click==7.1.2 11 | flask-cors==3.0.8 12 | flask-login==0.5.0 13 | flask-migrate==3.0.1 14 | flask-sqlalchemy==2.5.1 15 | flask-wtf==0.15.1 16 | flask==2.0.1 17 | greenlet==1.1.0 18 | gunicorn==20.1.0 19 | itsdangerous==2.0.1 20 | jinja2==3.0.1 21 | mako==1.1.4 22 | markupsafe==2.0.1 23 | python-dateutil==2.8.1 24 | python-dotenv==0.14.0 25 | python-editor==1.0.4 26 | six==1.15.0 27 | sqlalchemy==1.4.19 28 | werkzeug==2.0.1 29 | wtforms==2.3.3 30 | boto3==1.26.16 31 | email-validator==1.3.0 32 | -------------------------------------------------------------------------------- /react-app/src/components/SingUpFormModal/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Modal } from "../../context/Modal"; 3 | import LoginForm from "./SignupForm"; 4 | 5 | function SignupFormModal({startedButton}) { 6 | const [showModal, setShowModal] = useState(false); 7 | 8 | return ( 9 | <> 10 | setShowModal(true)} 13 | > 14 | Sign Up 15 | 16 | {showModal && ( 17 | setShowModal(false)}> 18 | 19 | 20 | )} 21 | 22 | ); 23 | } 24 | 25 | export default SignupFormModal; 26 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | click = "==7.1.2" 8 | gunicorn = "==20.1.0" 9 | itsdangerous = "==2.0.1" 10 | python-dotenv = "==0.14.0" 11 | six = "==1.15.0" 12 | Flask = "==2.0.1" 13 | Flask-Cors = "==3.0.8" 14 | Flask-SQLAlchemy = "==2.5.1" 15 | Flask-WTF = "==0.15.1" 16 | Jinja2 = "==3.0.1" 17 | MarkupSafe = "==2.0.1" 18 | SQLAlchemy = "==1.4.19" 19 | Werkzeug = "==2.0.1" 20 | WTForms = "==2.3.3" 21 | Flask-Migrate = "==3.0.1" 22 | Flask-Login = "==0.5.0" 23 | alembic = "==1.6.5" 24 | python-dateutil = "==2.8.1" 25 | python-editor = "==1.0.4" 26 | greenlet = "==1.1.0" 27 | Mako = "==1.1.4" 28 | boto3 = "*" 29 | email-validator = "*" 30 | 31 | [dev-packages] 32 | 33 | [requires] 34 | python_version = "3.9" 35 | -------------------------------------------------------------------------------- /app/api/clip_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request 2 | from app.models import db, fast_forward, user 3 | from flask_login import current_user, login_required 4 | from app.aws import ( 5 | upload_file_to_s3, allowed_file, get_unique_filename) 6 | 7 | clip_routes = Blueprint("clips", __name__) 8 | 9 | 10 | @clip_routes.route('', methods=["POST"]) 11 | def upload_clip(): 12 | if "clip" not in request.files: 13 | return {"errors": "clip required"}, 400 14 | 15 | clip = request.files["clip"] 16 | print(clip, "this is the clip") 17 | 18 | if not allowed_file(clip.filename): 19 | return {"errors": "file type not permitted"}, 400 20 | 21 | clip.filename = get_unique_filename(clip.filename) 22 | 23 | upload = upload_file_to_s3(clip) 24 | print(upload, "this is the upload") 25 | 26 | return upload 27 | -------------------------------------------------------------------------------- /app/models/like_post.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | from .db import db, add_prefix_for_prod, environment, SCHEMA 3 | 4 | class LikePost(db.Model): 5 | __tablename__ = 'likePosts' 6 | if environment == "production": 7 | __table_args__ = {'schema': SCHEMA} 8 | id = db.Column(db.Integer, primary_key=True) 9 | user_id = db.Column(db.Integer, db.ForeignKey(add_prefix_for_prod("users.id"))) 10 | fast_forward_id = db.Column(db.Integer, db.ForeignKey(add_prefix_for_prod("fastForwards.id"))) 11 | user = db.relationship("User", backref="likePosts") 12 | fast_forwards = db.relationship("FastForward", backref="likePosts") 13 | 14 | def to_dict(self): 15 | return { 16 | 'id': self.id, 17 | 'user_id': self.user_id, 18 | 'fast_forward_id': self.fast_forward_id, 19 | 'User': self.user.to_dict() 20 | } 21 | -------------------------------------------------------------------------------- /react-app/src/context/Modal.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useRef, useState, useEffect } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./Modal.css"; 4 | 5 | const ModalContext = React.createContext(); 6 | 7 | export function ModalProvider({ children }) { 8 | const modalRef = useRef(); 9 | const [value, setValue] = useState(); 10 | 11 | useEffect(() => { 12 | setValue(modalRef.current); 13 | }, []); 14 | 15 | return ( 16 | <> 17 | {children} 18 |
19 | 20 | ); 21 | } 22 | 23 | export function Modal({ onClose, children }) { 24 | const modalNode = useContext(ModalContext); 25 | if (!modalNode) return null; 26 | 27 | return ReactDOM.createPortal( 28 | 96 |
97 | 98 | 104 |
105 |
106 | 107 | 113 |
114 |
115 | 116 | 122 |
123 |
124 | 125 | 131 |
132 |
133 | 134 | 141 |
142 | 143 | 144 | ); 145 | }; 146 | 147 | export default SignUpForm; 148 | -------------------------------------------------------------------------------- /react-app/src/store/likePosts.js: -------------------------------------------------------------------------------- 1 | import { fetchFastForwardDetails } from './fastForwardDetails'; 2 | 3 | export const LOAD_LIKE = "likes/LOAD_likeS"; 4 | export const UPDATE_LIKE = "likes/UPDATE_likeS"; 5 | export const REMOVE_LIKE = "likes/REMOVE_likeS"; 6 | export const ADD_LIKE = "likes/ADD_likeS"; 7 | 8 | export const load = (likes) => ({ 9 | type: LOAD_LIKE, 10 | likes 11 | }); 12 | 13 | 14 | export const add = (like) => ({ 15 | type: ADD_LIKE, 16 | like 17 | }); 18 | 19 | export const edit = (like) => ({ 20 | type: UPDATE_LIKE, 21 | like 22 | }); 23 | 24 | export const remove = (likeId) => ({ 25 | type: REMOVE_LIKE, 26 | likeId 27 | }) 28 | 29 | 30 | 31 | export const getLikes = () => async dispatch => { 32 | 33 | const response = await fetch(`/api/fastForwards/likes`); 34 | 35 | if (response.ok) { 36 | const list = await response.json(); 37 | console.log("this is the like list", list) 38 | dispatch(load(list)); 39 | } 40 | }; 41 | 42 | export const getLikeDetails = (likeId) => async dispatch => { 43 | 44 | const response = await fetch(`/api/likes/${likeId}`); 45 | 46 | if (response.ok) { 47 | const list = await response.json(); 48 | dispatch(add(list)); 49 | } 50 | }; 51 | 52 | export const getLikesByUser = (userId) => async dispatch => { 53 | const response = await fetch(`/api/artists/${userId}/likes`); 54 | 55 | if (response.ok) { 56 | const list = await response.json(); 57 | dispatch(load(list)); 58 | } 59 | }; 60 | 61 | export const createLike = (postId) => async dispatch => { 62 | const response = await fetch(`/api/fastForwards/${postId}/likes`, { 63 | method: 'POST', 64 | headers: { 65 | "Content-Type": "application/json" 66 | }, 67 | body: JSON.stringify({ 68 | fast_forward_id: postId 69 | }) 70 | }) 71 | // const response = await fetch(`/api/fastForwards/${fastForwardId}/likes`, { 72 | // method: "POST", 73 | // headers: { 74 | 75 | // } 76 | // }) 77 | 78 | if (response.ok) { 79 | const like = await response.json(); 80 | dispatch(add(like)); 81 | } else if (response.status < 500) { 82 | const data = await response.json(); 83 | console.log(data) 84 | if (data.errors) { 85 | return data.errors; 86 | } 87 | } else { 88 | return ['An error occurred. Please try again.'] 89 | } 90 | 91 | }; 92 | 93 | 94 | export const editLike = (likeId, payload, fastForwardId) => async dispatch => { 95 | console.log(payload) 96 | const response = await fetch(`/api/likes/${likeId}`, { 97 | method: 'PUT', 98 | headers: { 99 | "Content-Type": "application/json" 100 | }, 101 | body: JSON.stringify(payload) 102 | }) 103 | 104 | if (response.ok) { 105 | const like = await response.json(); 106 | console.log("this is the payload", payload) 107 | dispatch(add(like)); 108 | dispatch(fetchFastForwardDetails(fastForwardId)); 109 | } 110 | }; 111 | 112 | 113 | export const deleteLike = (id, fastForwardId) => async dispatch => { 114 | console.log("this is the id to be removed", id) 115 | const response = await fetch(`/api/likes/${id}`, { 116 | method: 'DELETE' 117 | }); 118 | 119 | if (response.ok) { 120 | dispatch(remove(id)); 121 | dispatch(fetchFastForwardDetails(fastForwardId)) 122 | } 123 | } 124 | const initialState = { allLikes: {}, singleLike: {} }; 125 | 126 | const postLikeReducer = (state = initialState, action) => { 127 | let newState = { ...state } 128 | switch (action.type) { 129 | case LOAD_LIKE: 130 | newState = { ...state, allLikes: {} } 131 | action.likes.likes.forEach(like => newState.allLikes[like.id] = like) 132 | return newState; 133 | case UPDATE_LIKE: 134 | newState.singleLike = action.like 135 | return newState 136 | case ADD_LIKE: 137 | newState.allLikes[action.like.id] = action.like 138 | newState.singleLike = action.like 139 | return newState; 140 | case REMOVE_LIKE: 141 | newState = { ...state, allLikes: { ...state.allLikes }, singleLike: { ...state.singleLike } } 142 | delete newState.allLikes[action.likeId] 143 | newState.singleLike = {} 144 | return newState 145 | default: 146 | return state; 147 | } 148 | } 149 | 150 | export default postLikeReducer; 151 | -------------------------------------------------------------------------------- /react-app/src/components/LoginFormModal/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { login } from "../../store/session"; 4 | import { useHistory } from "react-router-dom"; 5 | import './LoginForm.css' 6 | import SignupFormModal from "../SingUpFormModal"; 7 | import setShowModal from "./index" 8 | 9 | const LoginForm = () => { 10 | const [errors, setErrors] = useState([]); 11 | const [email, setEmail] = useState(""); 12 | const [password, setPassword] = useState(""); 13 | const user = useSelector((state) => state.session.user); 14 | const dispatch = useDispatch(); 15 | const history = useHistory(); 16 | 17 | const onLogin = async (e) => { 18 | e.preventDefault(); 19 | const data = await dispatch(login(email, password)).then( 20 | history.push("/") 21 | ); 22 | if (data) { 23 | setErrors(data); 24 | } 25 | }; 26 | 27 | const updateEmail = (e) => { 28 | setEmail(e.target.value); 29 | }; 30 | 31 | const updatePassword = (e) => { 32 | setPassword(e.target.value); 33 | }; 34 | 35 | const signInDemo = async (e) => { 36 | // e.preventDefault() 37 | // setErrors([]); 38 | e.preventDefault(); 39 | const data = await dispatch(login("demo@aa.io", "password")).then( 40 | history.push("/") 41 | ); 42 | if (data) { 43 | setErrors(data); 44 | } 45 | }; 46 | 47 | // if (user) { 48 | // return ; 49 | // } 50 | 51 | return ( 52 |
53 |
54 |
55 | {errors.map((error, ind) => ( 56 |
{error}
57 | ))} 58 |
59 |
60 |

Log in to FastForward

61 |
62 |
63 |
64 | 73 |
74 |
75 |
76 | 85 |
86 |
87 | {/* 91 | 95 | */} 99 |
100 | 103 | 106 |
107 |
108 |
109 |
110 |

Don’t have an account? Sign Up

111 |
112 |
113 |
114 | ); 115 | }; 116 | 117 | export default LoginForm; 118 | -------------------------------------------------------------------------------- /react-app/src/store/comment.js: -------------------------------------------------------------------------------- 1 | import { fetchFastForwardDetails } from './fastForwardDetails'; 2 | 3 | export const LOAD_COMMENT = "comments/LOAD_COMMENTS"; 4 | export const UPDATE_COMMENT = "comments/UPDATE_COMMENTS"; 5 | export const REMOVE_COMMENT = "comments/REMOVE_COMMENTS"; 6 | export const ADD_COMMENT = "comments/ADD_COMMENTS"; 7 | 8 | export const load = (comments) => ({ 9 | type: LOAD_COMMENT, 10 | comments 11 | }); 12 | 13 | 14 | export const add = (comment) => ({ 15 | type: ADD_COMMENT, 16 | comment 17 | }); 18 | 19 | export const edit = (comment) => ({ 20 | type: UPDATE_COMMENT, 21 | comment 22 | }); 23 | 24 | export const remove = (commentId) => ({ 25 | type: REMOVE_COMMENT, 26 | commentId 27 | }) 28 | 29 | 30 | 31 | export const getComments = (fastForwardId) => async dispatch => { 32 | 33 | const response = await fetch(`/api/fastForwards/${fastForwardId}/comments`); 34 | 35 | if (response.ok) { 36 | const list = await response.json(); 37 | dispatch(load(list)); 38 | } 39 | }; 40 | 41 | export const getCommentDetails = (commentId) => async dispatch => { 42 | 43 | const response = await fetch(`/api/comments/${commentId}`); 44 | 45 | if (response.ok) { 46 | const list = await response.json(); 47 | dispatch(add(list)); 48 | } 49 | }; 50 | 51 | export const getCommentsByUser = (userId) => async dispatch => { 52 | const response = await fetch(`/api/artists/${userId}/comments`); 53 | 54 | if (response.ok) { 55 | const list = await response.json(); 56 | dispatch(load(list)); 57 | } 58 | }; 59 | 60 | export const createComment = (fastForwardId, payload) => async dispatch => { 61 | console.log(payload) 62 | const response = await fetch(`/api/fastForwards/${fastForwardId}/comments`, { 63 | method: 'POST', 64 | headers: { 65 | "Content-Type": "application/json" 66 | }, 67 | body: JSON.stringify(payload) 68 | }) 69 | // const response = await fetch(`/api/fastForwards/${fastForwardId}/comments`, { 70 | // method: "POST", 71 | // headers: { 72 | 73 | // } 74 | // }) 75 | 76 | if (response.ok) { 77 | const comment = await response.json(); 78 | dispatch(add(comment)); 79 | } else if (response.status < 500) { 80 | const data = await response.json(); 81 | console.log(data) 82 | if (data.errors) { 83 | return data.errors; 84 | } 85 | } else { 86 | return ['An error occurred. Please try again.'] 87 | } 88 | 89 | }; 90 | 91 | 92 | export const editComment = (commentId, payload, fastForwardId) => async dispatch => { 93 | console.log(payload) 94 | const response = await fetch(`/api/comments/${commentId}`, { 95 | method: 'PUT', 96 | headers: { 97 | "Content-Type": "application/json" 98 | }, 99 | body: JSON.stringify(payload) 100 | }) 101 | 102 | if (response.ok) { 103 | const comment = await response.json(); 104 | console.log("this is the payload", payload) 105 | dispatch(add(comment)); 106 | dispatch(fetchFastForwardDetails(fastForwardId)); 107 | } 108 | }; 109 | 110 | 111 | export const deleteComment = (id, fastForwardId) => async dispatch => { 112 | const response = await fetch(`/api/comments/${id}`, { 113 | method: 'DELETE' 114 | }); 115 | 116 | if (response.ok) { 117 | const list = await response.json(); 118 | dispatch(remove(id)); 119 | dispatch(fetchFastForwardDetails(fastForwardId)) 120 | } 121 | } 122 | const initialState = { allComments: {}, singleComment: {} }; 123 | 124 | const commentReducer = (state = initialState, action) => { 125 | let newState = { ...state } 126 | switch (action.type) { 127 | case LOAD_COMMENT: 128 | newState = { ...state, allComments: {} } 129 | action.comments.comments.forEach(comment => newState.allComments[comment.id] = comment) 130 | return newState; 131 | case UPDATE_COMMENT: 132 | newState.singleComment = action.comment 133 | return newState 134 | case ADD_COMMENT: 135 | newState.allComments[action.comment.id] = action.comment 136 | newState.singleComment = action.comment 137 | return newState; 138 | case REMOVE_COMMENT: 139 | newState = { ...state, allComments: { ...state.allComments }, singleComment: { ...state.singleComment } } 140 | delete newState.allComments[action.commentId] 141 | newState.singleComment = {} 142 | return newState 143 | default: 144 | return state; 145 | } 146 | } 147 | 148 | export default commentReducer; 149 | -------------------------------------------------------------------------------- /app/api/fast_forward_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, jsonify 2 | from sqlalchemy.orm import relationship, sessionmaker, joinedload 3 | from app.models import FastForward, db, Comment, User, LikePost 4 | from flask_login import login_required, current_user 5 | from app.forms import FastForwardForm 6 | from app.forms import CommentForm 7 | from app.forms import FastForwardEditForm 8 | 9 | 10 | fast_forward_routes = Blueprint('fastForwards', __name__) 11 | 12 | def validation_errors_to_error_messages(validation_errors): 13 | """ 14 | Simple function that turns the WTForms validation errors into a simple list 15 | """ 16 | errorMessages = [] 17 | for field in validation_errors: 18 | for error in validation_errors[field]: 19 | errorMessages.append(f'{error}') 20 | return errorMessages 21 | 22 | @fast_forward_routes.route('') 23 | def get_fast_forward(): 24 | data = FastForward.query.all() 25 | print("this is the data", data) 26 | return {fast_forward.to_dict()['id']: fast_forward.to_dict() for fast_forward in data} 27 | 28 | @fast_forward_routes.route('/') 29 | def get_fast_forward_details(id): 30 | data = FastForward.query.get(id) 31 | return data.to_dict() 32 | 33 | 34 | @fast_forward_routes.route('', methods=['POST']) 35 | @login_required 36 | def post_fast_forward(): 37 | form = FastForwardForm() 38 | form['csrf_token'].data = request.cookies['csrf_token'] 39 | if form.validate_on_submit(): 40 | fast_forward_upload = FastForward( 41 | caption=form.data['caption'], 42 | url=form.data['url'], 43 | user_id=current_user.id 44 | ) 45 | db.session.add(fast_forward_upload) 46 | db.session.commit() 47 | return fast_forward_upload.to_dict() 48 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401 49 | 50 | @fast_forward_routes.route('/', methods=['PUT']) 51 | @login_required 52 | def edit_fast_forward(id): 53 | fast_forward = FastForward.query.get(id) 54 | if current_user.id == fast_forward.user_id: 55 | form = FastForwardEditForm() 56 | form['csrf_token'].data = request.cookies['csrf_token'] 57 | print(form.data) 58 | if form.validate_on_submit(): 59 | fast_forward.caption = form.data['caption'] 60 | db.session.add(fast_forward) 61 | db.session.commit() 62 | return fast_forward.to_dict() 63 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401 64 | return {'errors': ['Unauthorized']} 65 | 66 | @fast_forward_routes.route('/', methods=['DELETE']) 67 | @login_required 68 | def delete_fast_forward(id): 69 | fast_forward = FastForward.query.get(id) 70 | print(fast_forward) 71 | print(id) 72 | if current_user.id == fast_forward.user_id: 73 | db.session.delete(fast_forward) 74 | db.session.commit() 75 | return {"data": "Deleted"} 76 | return {'errors': ['Unauthorized']} 77 | 78 | @fast_forward_routes.route('/comments') 79 | @login_required 80 | def get_comments(): 81 | """ 82 | Query for all comments for a fast_forward and returns them in a list of dictionaries 83 | """ 84 | comments = Comment.query.all() 85 | print(comments) 86 | return comments.to_dict() 87 | 88 | 89 | @fast_forward_routes.route('//comments', methods=['POST']) 90 | @login_required 91 | def post_comment(id): 92 | """ 93 | Posts a comment to a fast_forward 94 | """ 95 | form = CommentForm() 96 | print(request) 97 | form['csrf_token'].data = request.cookies['csrf_token'] 98 | print(form.data) 99 | print(form.errors) 100 | print(form.validate_on_submit()) 101 | if form.validate_on_submit(): 102 | comment = Comment(body=form.data['body'], 103 | user_id=current_user.id, 104 | fast_forward_id=id) 105 | db.session.add(comment) 106 | db.session.commit() 107 | return comment.to_dict() 108 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401 109 | 110 | @fast_forward_routes.route('//likes', methods=['POST']) 111 | @login_required 112 | def post_like(id): 113 | """ 114 | Posts a like to a fast_forward 115 | """ 116 | like = LikePost(user_id=current_user.id, 117 | fast_forward_id=id) 118 | db.session.add(like) 119 | db.session.commit() 120 | return like.to_dict() 121 | 122 | @fast_forward_routes.route('/likes') 123 | @login_required 124 | def get_likes(): 125 | """ 126 | Query for all comments for a fast_forward and returns them in a list of dictionaries 127 | """ 128 | likes = LikePost.query.all() 129 | return likes.to_dict() 130 | -------------------------------------------------------------------------------- /react-app/src/components/User.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { NavLink, useParams, useHistory, useLocation } from 'react-router-dom'; 4 | import * as fastForwardActions from "../store/fastForward"; 5 | import * as followActions from '../store/follower' 6 | import './User.css' 7 | 8 | function User() { 9 | const currentUser = useSelector(state => state.session.user) 10 | const fastForwardUserId = Number(useLocation().pathname.split("/")[2]); 11 | console.log("this is the user of this profile", fastForwardUserId) 12 | const fastForwardsObj = useSelector(state => state.fastForward) 13 | const fastForwards = Object.values(fastForwardsObj) 14 | let followings = useSelector((state) => Object.values(state.follower.following)) 15 | followings = followings.map((user) => user.id) 16 | 17 | const [user, setUser] = useState({}); 18 | const [followsUser, setFollowsUser] = useState([]); 19 | const [followingUser, setFollowingUser] = useState([]); 20 | const [following, setFollowing] = useState(followings.includes(fastForwardUserId)) 21 | const [isLoaded, setIsLoaded] = useState(false); 22 | const { userId } = useParams(); 23 | const dispatch = useDispatch(); 24 | 25 | console.log("this is the value of followings", followings) 26 | console.log("this is the value of following", following) 27 | 28 | 29 | useEffect(() => { 30 | dispatch(fastForwardActions.fetchAllFastForwards()); 31 | }, [dispatch, userId]); 32 | 33 | useEffect(() => { 34 | if (currentUser) { 35 | dispatch(followActions.followingList(currentUser.id)) 36 | .then(() => setIsLoaded(true)) 37 | } 38 | }, [dispatch, isLoaded, currentUser]); 39 | 40 | 41 | useEffect(() => { 42 | async function fetchData() { 43 | const response = await fetch(`/api/users/${userId}/following`); 44 | const responseData = await response.json(); 45 | setFollowsUser(Object.values(responseData)); 46 | } 47 | fetchData(); 48 | }, []); 49 | 50 | useEffect(() => { 51 | async function fetchData() { 52 | const response = await fetch(`/api/users/${userId}/followers`); 53 | const responseData2 = await response.json(); 54 | setFollowingUser(Object.values(responseData2)); 55 | } 56 | fetchData(); 57 | }, []); 58 | 59 | useEffect(() => { 60 | if (user) { 61 | setFollowing(followings.includes(fastForwardUserId)); 62 | } 63 | }, [dispatch, followings]); 64 | 65 | const handleFollow = (followerId, followedId) => { 66 | if (!following) { 67 | dispatch(followActions.follow(followerId, followedId)) 68 | .then(() => setFollowing(true)) 69 | console.log("this is whether or not the follow button worked", "followed") 70 | } else { 71 | dispatch(followActions.unfollow(followerId, followedId)) 72 | .then(() => setFollowing(false)) 73 | console.log("this is whether or not the follow button worked", "unfollowed") 74 | } 75 | } 76 | 77 | useEffect(() => { 78 | if (currentUser) { 79 | dispatch(followActions.followingList(currentUser.id)) 80 | .then(() => setIsLoaded(true)) 81 | } 82 | }, [dispatch, isLoaded]); 83 | 84 | const filtered = fastForwards.filter(fastForward => fastForward.user_id === Number(userId)) 85 | 86 | useEffect(() => { 87 | if (!userId) { 88 | return; 89 | } 90 | (async () => { 91 | const response = await fetch(`/api/users/${userId}`); 92 | const user = await response.json(); 93 | setUser(user); 94 | })(); 95 | }, [userId]); 96 | 97 | if (!user) { 98 | return null; 99 | } 100 | 101 | return ( 102 |
103 |
104 |
105 | profile 106 |
107 |
108 |

{user.username}

109 |
110 | {user.first_name} {user.last_name} 111 |
112 |
113 | {currentUser && currentUser.id !== user.id &&
handleFollow(currentUser.id, fastForwardUserId)}>{!following ? "Follow" : "Following"}
} 114 |
115 |
116 |
117 | {/*
118 |
{followsUser.length} following
119 |
{followingUser.length} followers
120 |
*/} 121 |
{user.bio}
122 |
123 | {filtered.map(fastForward => ( 124 |
125 | 127 | {fastForward.caption} 128 |
129 | ))} 130 |
131 |
132 | ); 133 | } 134 | export default User; 135 | -------------------------------------------------------------------------------- /react-app/src/components/sideBar2.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { useHistory, NavLink } from 'react-router-dom'; 4 | import LogoutButton from './auth/LogoutButton'; 5 | import FollowingList from './FollowList'; 6 | import LoginFormModal from './LoginFormModal'; 7 | import "./NavBar.css" 8 | import UsersList from './UsersList'; 9 | import UsersList2 from './UsersList2'; 10 | 11 | const SideBar2 = () => { 12 | const [showMenu, setShowMenu] = useState(false) 13 | const [forButton, setForButton] = useState(true) 14 | const [seeMore, setSeeMore] = useState(false) 15 | const [seeMore2, setSeeMore2] = useState(false) 16 | const [followingButton, setFollowingButton] = useState(false) 17 | 18 | 19 | const openMenu = () => { 20 | 21 | if (showMenu) return 22 | setShowMenu(true) 23 | console.log("opening") 24 | } 25 | 26 | useEffect(() => { 27 | const closeMenu = () => { 28 | if (!showMenu) return 29 | setShowMenu(false) 30 | console.log("closing") 31 | } 32 | 33 | document.addEventListener("click", closeMenu) 34 | return () => document.removeEventListener("click", closeMenu) 35 | }, [showMenu]) 36 | 37 | 38 | const user = useSelector((state) => state.session.user); 39 | 40 | return ( 41 |
42 |
43 |
44 | 45 | 46 |
For You
47 |
48 |
49 |
50 | 51 | 52 |
Following
53 |
54 |
55 |
56 |
57 | {!user &&

Log in to follow creators, like videos, and view comments.

} 58 | {!user &&
59 | 60 |
61 | } 62 |
63 |

Suggested accounts

64 | {!seeMore ? : } 65 | {!seeMore &&

setSeeMore(true)} className='suggested-see-all'>See all

} 66 | {seeMore &&

setSeeMore(false)} className='suggested-see-all'>See less

} 67 |
68 | {/* {user &&
69 |

Followed accounts

70 | 71 | {!seeMore2 &&

setSeeMore2(true)} className='suggested-see-all'>See More

} 72 | {seeMore2 &&

setSeeMore2(false)} className='suggested-see-all'>See less

} 73 |
} */} 74 |
75 |

Hey There! 👋 My name's Jessie! I'm a Full Stack Developer and the creator of this website! View, upload, edit or remove videos yourself, or interact with videos uploaded by other content creators! Want to know more about me? look below! 76 |

77 |

78 | 👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇 79 |

Interested in hiring me?

80 |

Let's Connect!

81 |

82 |

83 |
84 |
85 | 86 |
87 |
88 | 89 |
90 |
91 |

92 |

93 |
94 |

95 |

Check Out Some of My Other Work!

96 |

97 |

98 |
99 | 100 |
101 |

102 |
103 |
104 |
A Jessie Baron creation
105 |
inspired by TikTok
106 |
107 |
108 |
109 | ) 110 | } 111 | 112 | export default SideBar2; 113 | -------------------------------------------------------------------------------- /react-app/src/components/LoginFormModal/LoginForm.css: -------------------------------------------------------------------------------- 1 | .loginForm { 2 | width: 483px; 3 | height: 400px; 4 | display:flex; 5 | padding: 48px 0px 0px; 6 | flex-direction: column; 7 | align-items: center; 8 | background-color: rgb(37, 37, 37); 9 | border: 1px solid rgba(0, 0, 0, 0.04); 10 | border-radius: 7px; 11 | box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14); 12 | } 13 | 14 | .modal-header { 15 | color: white; 16 | top: 0; 17 | font-size: 32px; 18 | } 19 | 20 | .outer-login { 21 | width: 100%; 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | } 26 | 27 | .inner-login { 28 | display: flex; 29 | flex-direction: column; 30 | align-items: center; 31 | gap: 2px; 32 | width: 80%; 33 | } 34 | 35 | .loginPassword { 36 | font-size:14px; 37 | color: white; 38 | } 39 | 40 | .loginEmail { 41 | font-size:14px; 42 | color: white; 43 | padding-right: 170px; 44 | } 45 | #modal-content > form > div:nth-child(2) > input { 46 | width: 200px; 47 | } 48 | 49 | .loginEmailInput { 50 | border: none; 51 | background-color: rgb(46, 46, 46); 52 | caret-color: rgb(255, 59, 92); 53 | color: rgba(255, 255, 255, 0.9); 54 | outline: none; 55 | width: 100%; 56 | padding-left: 15px; 57 | resize: none; 58 | } 59 | .login-input { 60 | color: rgba(255, 255, 255, 0.9); 61 | font-weight: 600; 62 | border: 1px solid rgba(255, 255, 255, 0); 63 | font-size: 15px; 64 | padding: 0px 12px; 65 | height: 44px; 66 | background: rgb(46, 46, 46); 67 | margin-bottom: 15px; 68 | display: flex; 69 | background: transparent; 70 | outline: none; 71 | width: 310px; 72 | } 73 | 74 | .login-input2 { 75 | width: 285px; 76 | color: rgba(255, 255, 255, 0.9); 77 | font-weight: 600; 78 | border: 1px solid rgba(255, 255, 255, 0); 79 | font-size: 15px; 80 | padding: 0px 12px; 81 | height: 44px; 82 | background: rgb(46, 46, 46); 83 | margin-bottom: 15px; 84 | display: flex; 85 | outline: none; 86 | } 87 | 88 | .loginPasswordInput { 89 | border: none; 90 | width: 95%; 91 | caret-color: rgb(255, 59, 92); 92 | background-color: rgb(46, 46, 46); 93 | color: rgba(255, 255, 255, 0.9); 94 | outline: none; 95 | } 96 | 97 | #modal-content > form > div:nth-child(4) { 98 | display: flex; 99 | gap: 10px; 100 | } 101 | 102 | #modal-content > form > div:nth-child(2) { 103 | margin-bottom: 15px; 104 | } 105 | 106 | #login { 107 | border-radius: 4px; 108 | border: none; 109 | color: rgb(255, 255, 255); 110 | background-color: rgb(255, 59, 92); 111 | min-width: 120px; 112 | min-height: 46px; 113 | font-size: 16px; 114 | line-height: 22px; 115 | } 116 | 117 | #login:hover { 118 | background-color: rgb(250, 41, 76); 119 | cursor: pointer; 120 | } 121 | 122 | 123 | #modal-content > form > div > div.inner-login > div.signin-buttons > button.demo-btn { 124 | border-radius: 4px; 125 | border: none; 126 | color: rgb(255, 255, 255); 127 | background-color: rgb(255, 59, 92); 128 | min-width: 120px; 129 | min-height: 46px; 130 | font-size: 16px; 131 | line-height: 22px; 132 | } 133 | 134 | #modal-content > form > div > div.inner-login > div.signin-buttons > button.demo-btn:hover { 135 | background-color: rgb(250, 41, 76); 136 | cursor: pointer; 137 | } 138 | 139 | #modal-content > form > div:nth-child(4) > button.loginBt { 140 | background-color: white; 141 | border: 1px solid grey; 142 | border-radius: 33px; 143 | width: 55px; 144 | height: 30px; 145 | font-size: 14px; 146 | } 147 | 148 | #modal-content > form > div:nth-child(4) > button.loginBt:hover { 149 | cursor: pointer; 150 | border: 1.2px solid black; 151 | } 152 | 153 | #modal-content > form > div:nth-child(4) > button:nth-child(2) { 154 | background-color: #696868; 155 | border: 1px solid grey; 156 | border-radius: 33px; 157 | width: 55px; 158 | height: 30px; 159 | font-size: 14px; 160 | color: white; 161 | } 162 | 163 | #modal-content > form > div:nth-child(4) > button:nth-child(2):hover { 164 | cursor: pointer; 165 | border: 1.2px solid black; 166 | } 167 | 168 | .demos { 169 | width: 80%; 170 | color: rgba(255, 255, 255, 0.9); 171 | font-weight: 600; 172 | border: 1px solid rgba(255, 255, 255, 0); 173 | font-size: 15px; 174 | padding: 0px 12px; 175 | height: 44px; 176 | background: rgb(46, 46, 46); 177 | margin-bottom: 15px; 178 | display: flex; 179 | gap: 10px; 180 | } 181 | 182 | .demo-text { 183 | padding-top: 10px; 184 | } 185 | 186 | .google { 187 | width: 25px; 188 | padding-top: 6px; 189 | padding-left: 35px; 190 | } 191 | 192 | .facebook { 193 | width: 20px; 194 | padding-top: 7px; 195 | padding-left: 35px; 196 | } 197 | 198 | .insta { 199 | width: 20px; 200 | padding-top: 7px; 201 | padding-left: 35px; 202 | } 203 | 204 | .signup-text { 205 | color: white; 206 | font-size: 12px; 207 | } 208 | 209 | .signup-text-navi { 210 | font-size: 12px; 211 | text-decoration: none; 212 | color: rgb(255, 59, 92); 213 | } 214 | 215 | .signin-buttons { 216 | padding-bottom: 25px; 217 | display: flex; 218 | gap: 25px; 219 | } 220 | 221 | .login-divider { 222 | width: 100%; 223 | border-color: rgba(255, 255, 255, 0.12);; 224 | } 225 | 226 | #modal-content > form > div > div:nth-child(1) > div { 227 | color: rgb(203, 0, 0); 228 | font-size: 14px; 229 | } 230 | -------------------------------------------------------------------------------- /react-app/src/components/SideBar.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { useHistory, NavLink } from 'react-router-dom'; 4 | import LogoutButton from './auth/LogoutButton'; 5 | import FollowingList from './FollowList'; 6 | import LoginFormModal from './LoginFormModal'; 7 | import "./NavBar.css" 8 | import UsersList from './UsersList'; 9 | import UsersList2 from './UsersList2'; 10 | 11 | const SideBar = () => { 12 | const [showMenu, setShowMenu] = useState(false) 13 | const [forButton, setForButton] = useState(true) 14 | const [seeMore, setSeeMore] = useState(false) 15 | const [seeMore2, setSeeMore2] = useState(false) 16 | const [followingButton, setFollowingButton] = useState(false) 17 | 18 | useEffect(() => { 19 | const closeMenu = () => { 20 | if (!showMenu) return 21 | setShowMenu(false) 22 | console.log("closing") 23 | } 24 | 25 | document.addEventListener("click", closeMenu) 26 | return () => document.removeEventListener("click", closeMenu) 27 | }, [showMenu]) 28 | 29 | const forYou = () => { 30 | if (forButton) return 31 | setForButton(true) 32 | setFollowingButton(false) 33 | } 34 | 35 | const following = () => { 36 | if (followingButton) return 37 | setFollowingButton(true) 38 | setForButton(false) 39 | } 40 | 41 | 42 | const user = useSelector((state) => state.session.user); 43 | 44 | return ( 45 |
46 |
47 |
48 | 49 | 50 |
For You
51 |
52 |
53 |
54 | 55 | 56 |
Following
57 |
58 |
59 |
60 |
61 | {!user &&

Log in to follow creators, like videos, and view comments.

} 62 | {!user &&
63 | 64 |
} 65 |
66 |

Suggested accounts

67 | {!seeMore ? : } 68 | {!seeMore &&

setSeeMore(true)} className='suggested-see-all'>See all

} 69 | {seeMore &&

setSeeMore(false)} className='suggested-see-all'>See less

} 70 |
71 | {/* {user &&
72 |

Followed accounts

73 | 74 | {!seeMore2 &&

setSeeMore2(true)} className='suggested-see-all'>See More

} 75 | {seeMore2 &&

setSeeMore2(false)} className='suggested-see-all'>See less

} 76 |
} */} 77 |
78 |

Hey There! 👋 My name's Jessie! I'm a Full Stack Developer and the creator of this website! View, upload, edit or remove videos yourself, or interact with videos uploaded by other content creators! Want to know more about me? look below! 79 |

80 |

81 | 👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇 82 |

Interested in hiring me?

83 |

Let's Connect!

84 |

85 |

86 |
87 |
88 | 89 |
90 |
91 | 92 |
93 |
94 |

95 |

96 |
97 |

98 |

Check Out Some of My Other Work!

99 |

100 |

101 |
102 | 103 |
104 |

105 |
106 |
107 |
A Jessie Baron creation
108 |
inspired by TikTok
109 |
110 |
111 |
112 | ) 113 | } 114 | 115 | export default SideBar; 116 | -------------------------------------------------------------------------------- /react-app/src/components/NavBar.js: -------------------------------------------------------------------------------- 1 | 2 | import { React, useEffect, useState } from 'react'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { NavLink, Redirect, useHistory, useLocation } from 'react-router-dom'; 5 | import * as fastForwardActions from "../store/fastForward"; 6 | import LoginFormModal from './LoginFormModal'; 7 | import LogoutButton from './auth/LogoutButton'; 8 | import './NavBar.css' 9 | import FastForwards from './FastForward'; 10 | 11 | const NavBar = () => { 12 | 13 | const history = useHistory() 14 | const user = useSelector((state) => state.session.user); 15 | const dispatch = useDispatch(); 16 | const [showMenu, setShowMenu] = useState(false) 17 | const [usersNav, setUsersNav] = useState([]); 18 | const [query, setQuery] = useState("") 19 | const fastForwards = Object.values(useSelector((state) => state.fastForward)); 20 | 21 | useEffect(() => { 22 | async function fetchData() { 23 | const response = await fetch('/api/users/'); 24 | const responseData = await response.json(); 25 | setUsersNav(responseData.users); 26 | } 27 | fetchData(); 28 | }, []); 29 | 30 | useEffect(() => { 31 | dispatch(fastForwardActions.fetchAllFastForwards()); 32 | }, [dispatch]); 33 | 34 | 35 | const openMenu = () => { 36 | if (showMenu) return 37 | setShowMenu(true) 38 | console.log("opening") 39 | } 40 | 41 | useEffect(() => { 42 | const closeMenu = () => { 43 | if (!showMenu) return 44 | setShowMenu(false) 45 | console.log("closing") 46 | } 47 | 48 | document.addEventListener("click", closeMenu) 49 | return () => document.removeEventListener("click", closeMenu) 50 | }, [showMenu]) 51 | 52 | const searchItem = document.querySelector(".user-header-navi") 53 | const searchParam = Number(useLocation().pathname.split("/")[2]); 54 | 55 | 56 | 57 | const searchClick = () => { 58 | setQuery("") 59 | history.push(`/users/${searchItem?.href.slice(-1)}`) 60 | } 61 | 62 | const searchClick2 = (userId) => { 63 | history.push(`/users/${userId}`) 64 | setQuery("") 65 | } 66 | return ( 67 | 138 | ); 139 | } 140 | 141 | export default NavBar; 142 | -------------------------------------------------------------------------------- /react-app/src/components/FastUpload.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { Redirect, useHistory } from "react-router-dom"; 4 | import './FastForwards.css' 5 | import * as fastForwardActions from "../store/fastForward"; 6 | import { toast } from 'react-hot-toast'; 7 | import NavBar from "./NavBar"; 8 | 9 | 10 | const UploadClip = () => { 11 | const [clip, setClip] = useState(null); 12 | const [clipLoading, setClipLoading] = useState(false); 13 | const [caption, setCaption] = useState(""); 14 | const [hasSubmitted, setHasSubmitted] = useState(false); 15 | const [validationErrors, setValidationErrors] = useState(""); 16 | const user = useSelector((state) => state.session.user); 17 | const dispatch = useDispatch() 18 | const history = useHistory() 19 | 20 | 21 | 22 | 23 | const handleSubmit = async (e) => { 24 | 25 | const clipTypes = ["video/mp4", "video/webM"] 26 | 27 | e.preventDefault(); 28 | if (!user) return toast.error(`Please log in to upload a video`) 29 | if (!(clipTypes.includes(clip.type))) { 30 | return toast.error(`Please submit a valid mp4 or WebM file`); 31 | } 32 | console.log(clip) 33 | const formData = new FormData(); 34 | formData.append("clip", clip); 35 | 36 | if(caption.length >= 2500) { 37 | toast.error("Character limit exceeded") 38 | setClipLoading(false) 39 | return; 40 | } 41 | 42 | // aws uploads can be a bit slow—displaying 43 | // some sort of loading message is a good idea 44 | setClipLoading(true); 45 | setHasSubmitted(true); 46 | 47 | const res = await fetch('/api/clips', { 48 | method: "POST", 49 | body: formData, 50 | }); 51 | 52 | const res2 = await res.json(); 53 | 54 | if (res.ok) { 55 | console.log(res) 56 | setClipLoading(false); 57 | } 58 | 59 | else if (!(res.url.endsWith("webM"))) { 60 | setClipLoading(false); 61 | setHasSubmitted(false) 62 | // error handling 63 | return toast.error(`Please submit a valid mp4 or WebM file`); 64 | } 65 | 66 | else if (!(res.url.endsWith("mp4"))) { 67 | setClipLoading(false); 68 | setHasSubmitted(false) 69 | // error handling 70 | return toast.error(`Please submit a valid mp4 or WebM file`); 71 | } 72 | 73 | else { 74 | setClipLoading(false); 75 | setHasSubmitted(false) 76 | // error handling 77 | return alert(`Please submit a valid mp4 or WebM file`); 78 | } 79 | 80 | const fastForward = { 81 | caption, 82 | url: res2.url 83 | } 84 | 85 | await dispatch(fastForwardActions.fetchPostFastForward(fastForward)) 86 | .then(history.push(`/users/${user.id}`)) 87 | } 88 | 89 | const updateClip = (e) => { 90 | const file = e.target.files[0]; 91 | setClip(file); 92 | } 93 | 94 | 95 | 96 | 97 | return ( 98 |
99 |
100 |

Upload Video

101 |

Post a video to your account

102 |
103 |
104 |
105 | . 106 |

Select a local file to upload

107 |
108 |
109 | . 110 |

Write a caption below

111 |
112 |
113 | . 114 |

Submit your file to be uploaded!

115 |
116 |
117 |
118 |
119 |
120 |

Select video to upload

121 | 122 | 123 |
124 |
125 |
    126 |
    MP4 or WebM
    127 |
    720x1280 resolution or higher
    128 |
    Up to 10 minutes
    129 |
    Less than 2 GB
    130 |
131 |
132 | 140 |
141 |
142 | 143 | setCaption(e.target.value)} 148 | required 149 | /> 150 |
151 |
152 |
153 | {!hasSubmitted && } 154 | {(clipLoading) &&

Loading...

} 155 |
156 |
157 |
158 | ) 159 | } 160 | 161 | export default UploadClip; 162 | -------------------------------------------------------------------------------- /react-app/src/components/SingUpFormModal/SignupForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { Redirect } from "react-router-dom"; 4 | import { signUp } from "../../store/session"; 5 | import './SignupForm.css' 6 | 7 | const SignUpForm = () => { 8 | const [errors, setErrors] = useState([]); 9 | const [clip, setClip] = useState(null); 10 | const [clipLoading, setClipLoading] = useState(false); 11 | const [hasSubmitted, setHasSubmitted] = useState(false); 12 | const [username, setUsername] = useState(''); 13 | const [firstName, setFirstName] = useState(''); 14 | const [lastName, setLastName] = useState(''); 15 | const [imageUrl, setImageUrl] = useState(''); 16 | const [bio, setBio] = useState(''); 17 | const [email, setEmail] = useState(''); 18 | const [password, setPassword] = useState(''); 19 | const [repeatPassword, setRepeatPassword] = useState(''); 20 | const user = useSelector(state => state.session.user); 21 | const dispatch = useDispatch(); 22 | 23 | const onSignUp = async (e) => { 24 | e.preventDefault(); 25 | const formData = new FormData(); 26 | formData.append("clip", clip); 27 | 28 | if (password === repeatPassword) { 29 | 30 | setClipLoading(true); 31 | setHasSubmitted(true); 32 | 33 | console.log(formData) 34 | const res = await fetch('/api/clips', { 35 | method: "POST", 36 | body: formData, 37 | }); 38 | 39 | const res2 = await res.json(); 40 | 41 | if (res.ok) { 42 | setClipLoading(false); 43 | } 44 | else { 45 | setClipLoading(false); 46 | setHasSubmitted(false) 47 | // error handling 48 | } 49 | 50 | const user = { 51 | firstName, 52 | lastName, 53 | bio, 54 | imageUrl: res2.url, 55 | username, 56 | email, 57 | password 58 | } 59 | 60 | const data = await dispatch(signUp(user)); 61 | if (data) { 62 | setErrors(data) 63 | } 64 | } 65 | else { 66 | setErrors(["Passwords must match"]) 67 | console.log(errors) 68 | return; 69 | } 70 | }; 71 | 72 | const updatedFirstName = (e) => { 73 | setFirstName(e.target.value); 74 | }; 75 | const updatedLastName = (e) => { 76 | setLastName(e.target.value); 77 | }; 78 | const updatedBio = (e) => { 79 | setBio(e.target.value); 80 | }; 81 | const updateImageUrl = (e) => { 82 | setImageUrl(e.target.value); 83 | }; 84 | 85 | const updateUsername = (e) => { 86 | setUsername(e.target.value); 87 | }; 88 | 89 | const updateEmail = (e) => { 90 | setEmail(e.target.value); 91 | }; 92 | 93 | const updatePassword = (e) => { 94 | setPassword(e.target.value); 95 | }; 96 | 97 | const updateRepeatPassword = (e) => { 98 | setRepeatPassword(e.target.value); 99 | }; 100 | 101 | if (user) { 102 | return ; 103 | } 104 | 105 | const updateClip = (e) => { 106 | const file = e.target.files[0]; 107 | setClip(file); 108 | } 109 | 110 | return ( 111 |
112 |
113 | {errors.map((error, ind) => ( 114 |
{error}
115 | ))} 116 |
117 |
118 |

Sign Up

119 |
120 |
121 |
122 | 131 |
132 |
133 | 142 |
143 |
144 | 153 |
154 |
155 | 164 |
165 |
166 | 175 |
176 |
177 | 186 |
187 |
188 | 197 |
198 |
199 |
Upload a Profile Picture!
200 |
201 |
202 | 210 |
211 |
212 |
213 | 214 |
215 |
216 | ); 217 | }; 218 | 219 | export default SignUpForm; 220 | -------------------------------------------------------------------------------- /react-app/src/components/FastForward.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { NavLink, useHistory } from "react-router-dom"; 4 | import * as fastForwardActions from "../store/fastForward"; 5 | import { getComments, deleteComment } from "../store/comment"; 6 | import CommentForm from "./CommentForm"; 7 | import CommentEditForm from "./CommentEditForm"; 8 | import { toast } from 'react-hot-toast'; 9 | import * as followActions from '../store/follower' 10 | import * as likeActions from '../store/likePosts' 11 | import './FastForwards.css' 12 | import FollowButton from "./FollowButton"; 13 | 14 | const FastForwards = () => { 15 | // const user = useSelector((state) => state.session.user); 16 | const history = useHistory(); 17 | const fastForwards = Object.values(useSelector((state) => state.fastForward)); 18 | 19 | const dispatch = useDispatch(); 20 | const user = useSelector((state) => state.session.user); 21 | const commentsObj = useSelector(state => state.comment.allComments) 22 | const [showMenu, setShowMenu] = useState(false); 23 | const [editId, setEditId] = useState(-1); 24 | const [commentBody, setCommentBody] = useState(""); 25 | let followings = useSelector((state) => Object.values(state.follower.following)) 26 | followings = followings.map((user) => user.id) 27 | 28 | 29 | 30 | useEffect(() => { 31 | dispatch(fastForwardActions.fetchAllFastForwards()); 32 | }, [dispatch]); 33 | 34 | useEffect(() => { 35 | dispatch(followActions.followingList(user?.id)); 36 | }, [dispatch, user?.id]); 37 | 38 | const handleFollow = (followerId, followedId) => { 39 | if (!followings.includes(followedId)) { 40 | dispatch(followActions.follow(followerId, followedId)) 41 | } else { 42 | dispatch(followActions.unfollow(followerId, followedId)) 43 | } 44 | } 45 | 46 | const handleLike = async (fastForward) => { 47 | console.log(fastForward.id) 48 | const likes = fastForward?.LikePosts?.filter(like => like.user_id === user.id) 49 | if (!likes.length > 0) { 50 | await dispatch(likeActions.createLike(fastForward.id)) 51 | await dispatch(fastForwardActions.fetchAllFastForwards()) 52 | } 53 | else { 54 | await dispatch(likeActions.deleteLike(likes[0].id, fastForward.id)) 55 | await dispatch(fastForwardActions.fetchAllFastForwards()) 56 | } 57 | } 58 | 59 | const handleCopy = (fastForward) => { 60 | navigator.clipboard.writeText(fastForward.url) 61 | toast("URL copied to clipnoard") 62 | } 63 | 64 | 65 | 66 | const openMenu = () => { 67 | if (!showMenu) setShowMenu(true); 68 | if (showMenu) setShowMenu(false); 69 | }; 70 | 71 | const handleDelete = async (commentId, fastForwardId) => { 72 | await dispatch(deleteComment(commentId, fastForwardId)) 73 | await dispatch(fastForwardActions.fetchAllFastForwards()) 74 | }; 75 | 76 | return ( 77 |
78 |
{fastForwards?.map((fastForward) => ( 79 |
80 |
81 |
82 | Profile 87 |
88 |
89 |
90 | {fastForward.User.username} 91 |
{fastForward.User.first_name} {fastForward.User.last_name}
92 |
93 |
94 | {fastForward.caption} 95 |
96 |
97 | {user &&
98 |
handleFollow(user.id, fastForward.User.id)}>{!followings.includes(fastForward.User.id) ? "Follow" : "Following"}
99 |
} 100 |
101 |
102 | 104 | {user && 105 |
106 |
107 |
handleLike(fastForward)}> 108 | like.user_id === user.id).length > 0 ? "liked-feed" : "un-liked-feed"} class="fa-solid fa-heart"> 109 |
110 |
{fastForward?.LikePosts?.length}
111 |
112 |
113 | 114 |
115 | 116 |
117 |
118 |
{fastForward?.Comments?.length}
119 |
120 |
handleCopy(fastForward)} className="copy-wrapper"> 121 |
122 | 123 |
124 |
125 |
} 126 |
127 |
128 |
129 |
))}
130 |
131 | ); 132 | }; 133 | 134 | export default FastForwards; 135 | -------------------------------------------------------------------------------- /app/seeds/fast_forwards.py: -------------------------------------------------------------------------------- 1 | from app.models import db, FastForward, environment, SCHEMA 2 | 3 | 4 | # Adds a demo user, you can add other users here if you want 5 | def seed_fast_forwards(): 6 | ff1 = FastForward( 7 | user_id=4, url='https://jessie-projects.s3.amazonaws.com/51069aed6aa0451088ebe0b976d3dc11.mp4', caption='In my active era✨') 8 | ff2 = FastForward( 9 | user_id=4, url='https://jessie-projects.s3.amazonaws.com/8c23ae438e514b4a8ab0157931ec9240.mp4', caption='I want to go to Brazil😌') 10 | ff3 = FastForward( 11 | user_id=4, url='https://jessie-projects.s3.amazonaws.com/a68f0f7ee195450c957b5d42e82f7eab.mp4', caption='Let’s make lemonade🍋') 12 | ff4 = FastForward( 13 | user_id=4, url='https://jessie-projects.s3.amazonaws.com/e13af6003a4e44889fd194e265ddb060.mp4', caption='It’s always an honor. Thank you to the queen @Rihanna for having me back😌🖤 Nov 9th #SAVAGEXFENTYSHOW') 14 | ff5 = FastForward( 15 | user_id=4, url='https://jessie-projects.s3.amazonaws.com/932f5b04220b424cb9e510eaff1182a8.mp4', caption='A behind the scenes look at my #MADDENWOOD dreamworld✨ Want to join? Link in bio to shop!') 16 | ff6 = FastForward( 17 | user_id=5, url='https://jessie-projects.s3.amazonaws.com/d3ea7f2128e44f4c8e4d1bff6e41c0b0.mp4', caption='You’re in his DMs. I’m shooting his slo mo walks. We are different. Inspo: @Max Goodrich') 18 | ff7 = FastForward( 19 | user_id=5, url='https://jessie-projects.s3.amazonaws.com/a8b8592b37b34b6a90378e8c8491d752.mp4', caption='Y’all know what I’m talkin bout') 20 | ff8 = FastForward( 21 | user_id=5, url='https://jessie-projects.s3.amazonaws.com/b0d96e621b9a4281a4092aaea4c808ea.mp4', caption='I went viral') 22 | ff9 = FastForward( 23 | user_id=5, url='https://jessie-projects.s3.amazonaws.com/9a6ac3e03d3a461fb78e46ee9972aab3.mp4', caption='Sorry') 24 | ff10 = FastForward( 25 | user_id=5, url='https://jessie-projects.s3.amazonaws.com/4f7bc394e65f4e1ba894a4fc7958ad53.mp4', caption='I got one answer to every question') 26 | ff11 = FastForward( 27 | user_id=6, url='https://jessie-projects.s3.amazonaws.com/e0a1da2e75984ab2ad3aba0894882a21.mp4', caption='“Time is an illusion” - Albert Einstein | I traveled to London to shoot this video and as I shot, the first few tales weren’t feeling right, it’s because tourists were still taking photos of #bigben , but that’s when I had an idea. We had a crowd of fans around me as I was shooting. So we asked them to be in the background of the video. So a lot of the people behind me are actually fans on the bridge and when they react to the clock being gone. And bonus, that Bus name in the background was NOT planned! Crazy') 28 | ff12 = FastForward( 29 | user_id=6, url='https://jessie-projects.s3.amazonaws.com/ff89011659ce43d9a8c092ee545525bb.mp4', caption='Mad respect for anyone that is a street performer') 30 | ff13 = FastForward( 31 | user_id=6, url='https://jessie-projects.s3.amazonaws.com/4f6d83a32d7e4f979e5a8db2b0ceb756.mp4', caption='This trick shot took 127 takes…') 32 | ff14 = FastForward( 33 | user_id=6, url='https://jessie-projects.s3.amazonaws.com/3b94ac3ab41d4ea893141b9fcd68efb5.mp4', caption='Stay hydrated') 34 | ff15 = FastForward( 35 | user_id=6, url='https://jessie-projects.s3.amazonaws.com/73ce990e98264130ac386b62ea0e6814.mp4', caption='When your #bereal notification goes off') 36 | ff16 = FastForward( 37 | user_id=7, url='https://jessie-projects.s3.amazonaws.com/ef22e170d33341f8987830d9c6bd00d0.mp4', caption='Who wants to ⚡️go⚡️to McDonald’s with me after the game today? #ad @mcdonalds') 38 | ff17 = FastForward( 39 | user_id=7, url='https://jessie-projects.s3.amazonaws.com/99066599783245de944a6f2d637cfb08.mp4', caption='#BinanceMan is back to ease you into your Web3 journey with @binance 🤲🏾') 40 | ff18 = FastForward( 41 | user_id=7, url='https://jessie-projects.s3.amazonaws.com/78c499e08ef441eca6fdcadd69f43cd4.mp4', caption='Singer vs Producer + Mom Part 4* 🤣 (🇺🇸 Jersey)what’s next… #learnfromkhaby #learnontiktok @TikTok @tiktok creators #drake') 42 | ff19 = FastForward( 43 | user_id=7, url='https://jessie-projects.s3.amazonaws.com/6d81c678cd09476a921fd9a0a2d19b23.mp4', caption='#KhabyChallenge!🤣 Let’s do it and mention me!😛I wanna see you all doing that #learnfromkhaby #learnontiktok @TikTok @tiktok creators @_christlike') 44 | ff20 = FastForward( 45 | user_id=7, url='https://jessie-projects.s3.amazonaws.com/eef8dcb166cb43e7b47aea7d2685e223.mp4', caption='Let’s see if you guys can DUET with me!! I love the new #Pixel7! I will repost all duets with me on my iG Stories🤯 @googlepixel #BroughttoyoubyGoogle #teampixel') 46 | ff21 = FastForward( 47 | user_id=8, url='https://jessie-projects.s3.amazonaws.com/b73bbf49e3464dc0b003e60e2d34a38c.mp4', caption='Here comes Dwanta Claus, right down Dwanta Claus laaaaane 🎶 🥃🎅🏾🛷 Your GREATEST CHEAT MEAL AWAITS - hit my bio NOW and enjoy 😈🍨 @Salt & Straw #teremana') 48 | ff22 = FastForward( 49 | user_id=8, url='https://jessie-projects.s3.amazonaws.com/e5e4468622c84b0ab2fd0f28d75c7757.mp4', caption='I’m the kind of guy who faces his demons… even the chocolate ones 🍫😈😂 Stealing Snickers bars + 7-11 + every day for a year = time to go back home and right the wrong 🤣🙋🏽‍♂️ Plus, it’s cheaper than a shrink 🤙🏾🥃😉') 50 | ff23 = FastForward( 51 | user_id=8, url='https://jessie-projects.s3.amazonaws.com/7ea3d33813fe4fc8877f46d7a09567d1.mp4', caption='COUNTDOWN 📅 IS OVER AND BLACK ADAM⚡️ IS IN THEATERS TONIGHT!!!! Get your tickets NOW & enjoy! And make sure you stay til the end and prepare for the eruption…. 🌋🤯😉') 52 | ff24 = FastForward( 53 | user_id=8, url='https://jessie-projects.s3.amazonaws.com/9f6d99e04deb4883b630d54ffe96d482.mp4', caption='Always good to see my brotha @imkevinhart aka #HonkyPete in the house - rocking his AMAZING mini-me #BlackAdam Halloween costume this year. I just get annoyed when he doesnt give me direct answers 😂👏🏾 GET YOUR BLACK ADAM ⚡️TICKETS NOW… In theaters OCTOBER 21st🌎') 54 | ff25 = FastForward( 55 | user_id=8, url='https://jessie-projects.s3.amazonaws.com/45bfbf8d8d3246a4a468570a580b6eb5.mp4', caption='Girl dads ROCK😉💪🏾 Her father was emotional as he handed me his beautiful baby. Whatever this moment meant to him, meant something special to me too. #BlackAdamWorldTour #MexicoCity') 56 | ff36 = FastForward( 57 | user_id=1, url='https://cdn.coverr.co/videos/coverr-a-girl-shooting-a-video-of-a-christmas-tree-at-the-fair-2532/1080p.mp4', caption='A girl shooting a video of a Christmas tree at the fair') 58 | ff37 = FastForward( 59 | user_id=1, url='https://cdn.coverr.co/videos/coverr-a-girl-smelling-a-christmas-tree-at-the-fair-1013/1080p.mp4', caption='A girl smelling a Christmas tree at the fair') 60 | ff38 = FastForward( 61 | user_id=1, url='https://cdn.coverr.co/videos/coverr-a-girl-looking-at-christmas-decorations-while-at-the-fair-3207/1080p.mp4', caption='A girl looking at Christmas decorations while at the fair') 62 | ff39 = FastForward( 63 | user_id=2, url='https://cdn.coverr.co/videos/coverr-christmas-decorations-4223/1080p.mp4', caption='Christmas decorations') 64 | ff40 = FastForward( 65 | user_id=2, url='https://cdn.coverr.co/videos/coverr-a-girl-taking-a-stroll-at-a-festive-fair-4511/1080p.mp4', caption='A girl taking a stroll at a festive fair') 66 | ff41 = FastForward( 67 | user_id=2, url='https://cdn.coverr.co/videos/coverr-christmas-stockings-5285/1080p.mp4', caption='Christmas stockings') 68 | ff42 = FastForward( 69 | user_id=3, url='https://cdn.coverr.co/videos/coverr-christmas-ornaments-8497/1080p.mp4', caption='Christmas ornaments') 70 | ff43 = FastForward( 71 | user_id=3, url='https://cdn.coverr.co/videos/coverr-a-girl-walking-around-at-the-fair-6858/1080p.mp4', caption='A girl walking around at the fair') 72 | ff44 = FastForward( 73 | user_id=3, url='https://cdn.coverr.co/videos/coverr-a-happy-girl-smiling-for-the-camera-8696/1080p.mp4', caption='A happy girl smiling for the camera') 74 | 75 | db.session.add(ff1) 76 | db.session.add(ff2) 77 | db.session.add(ff3) 78 | db.session.add(ff4) 79 | db.session.add(ff5) 80 | db.session.add(ff6) 81 | db.session.add(ff7) 82 | db.session.add(ff8) 83 | db.session.add(ff9) 84 | db.session.add(ff10) 85 | db.session.add(ff11) 86 | db.session.add(ff12) 87 | db.session.add(ff13) 88 | db.session.add(ff14) 89 | db.session.add(ff15) 90 | db.session.add(ff16) 91 | db.session.add(ff17) 92 | db.session.add(ff18) 93 | db.session.add(ff19) 94 | db.session.add(ff20) 95 | db.session.add(ff21) 96 | db.session.add(ff22) 97 | db.session.add(ff23) 98 | db.session.add(ff24) 99 | db.session.add(ff25) 100 | db.session.add(ff36) 101 | db.session.add(ff37) 102 | db.session.add(ff38) 103 | db.session.add(ff39) 104 | db.session.add(ff40) 105 | db.session.add(ff41) 106 | db.session.add(ff42) 107 | db.session.add(ff43) 108 | db.session.add(ff44) 109 | db.session.commit() 110 | 111 | 112 | # Uses a raw SQL query to TRUNCATE or DELETE the users table. SQLAlchemy doesn't 113 | # have a built in function to do this. With postgres in production TRUNCATE 114 | # removes all the data from the table, and RESET IDENTITY resets the auto 115 | # incrementing primary key, CASCADE deletes any dependent entities. With 116 | # sqlite3 in development you need to instead use DELETE to remove all data and 117 | # it will reset the primary keys for you as well. 118 | def undo_fast_forwards(): 119 | if environment == "production": 120 | db.session.execute(f"TRUNCATE table {SCHEMA}.fastForwards RESTART IDENTITY CASCADE;") 121 | else: 122 | db.session.execute("DELETE FROM fastForwards") 123 | 124 | db.session.commit() 125 | -------------------------------------------------------------------------------- /react-app/src/components/NavBar.css: -------------------------------------------------------------------------------- 1 | #home-logo { 2 | margin: 0; 3 | margin-left: 10%; 4 | padding: 10px 0; 5 | font-size: 24px; 6 | color: white; 7 | letter-spacing: 2px; 8 | } 9 | 10 | .logo-text { 11 | font-size: 24px; 12 | text-decoration: none; 13 | color: white; 14 | font-weight: bold; 15 | } 16 | 17 | #search-icon { 18 | color: rgb(118, 117, 117); 19 | font-size: 20px; 20 | padding-top: 10px; 21 | padding-bottom: 10px; 22 | padding-right: 10px; 23 | z-index: 50; 24 | } 25 | 26 | #search-icon-active { 27 | color: white; 28 | font-size: 20px; 29 | padding-top: 10px; 30 | padding-bottom: 10px; 31 | padding-right: 10px; 32 | z-index: 500; 33 | } 34 | 35 | .search-button { 36 | border: none; 37 | outline: none; 38 | opacity: 0; 39 | width: 0px; 40 | } 41 | 42 | #search-icon-active:hover { 43 | cursor: pointer; 44 | color: rgb(156, 155, 155); 45 | } 46 | 47 | .search-divider { 48 | width: 20px; 49 | rotate: 90deg; 50 | padding: 0; 51 | border-color: rgb(118, 117, 117); 52 | } 53 | 54 | .logo-container { 55 | display: flex; 56 | align-items: center; 57 | text-decoration: none; 58 | margin-left: 60%; 59 | gap: 5px; 60 | } 61 | 62 | .fast-forward-logo { 63 | display: block; 64 | } 65 | 66 | .navigation-items { 67 | display: flex; 68 | flex-direction: row; 69 | align-items: center; 70 | padding-bottom: 7px; 71 | box-shadow: rgb(255 255 255 / 12%) 0px 1px 1px; 72 | position: fixed; 73 | width: 100%; 74 | z-index: 500; 75 | background-color: rgb(18, 18, 18); 76 | top: 0; 77 | } 78 | 79 | .search-items { 80 | display: flex; 81 | align-items: center; 82 | margin-left: 23%; 83 | width: 340px; 84 | height: 45px; 85 | border: none; 86 | background: transparent; 87 | background-color: rgb(49, 49, 49); 88 | outline: none; 89 | padding: 0px; 90 | margin-top: 10px; 91 | border-radius: 33px; 92 | } 93 | 94 | .search-bar { 95 | font-weight: 400; 96 | font-size: 16px; 97 | line-height: 22px; 98 | border: none; 99 | background: transparent; 100 | outline: none; 101 | padding: 0px; 102 | width: 252px; 103 | margin-left: 5%; 104 | height: 40px; 105 | color: rgba(255, 255, 255, 0.9); 106 | caret-color: rgb(255, 59, 92); 107 | } 108 | 109 | .nav-buttons { 110 | display: flex; 111 | gap: 25px; 112 | margin-left: 25%; 113 | margin-top: 5px; 114 | width: 100%; 115 | } 116 | 117 | .upload-button { 118 | width: 110px; 119 | height: 36px; 120 | border: 1px solid rgba(22, 24, 35, 0.12); 121 | background-color: rgb(35, 35, 35); 122 | border-radius: 2px; 123 | } 124 | 125 | .upload { 126 | font-size: 16px; 127 | line-height: 24px; 128 | color: white; 129 | text-decoration: none; 130 | } 131 | 132 | #root>div>nav>div>div.nav-buttons>button.signIn { 133 | width: 110px; 134 | height: 36px; 135 | border: none; 136 | background-color: rgb(255, 59, 92); 137 | border-radius: 5px; 138 | font-weight: bold; 139 | text-decoration: none; 140 | color: white; 141 | font-weight: bold; 142 | font-size: 16px; 143 | line-height: 24px; 144 | } 145 | 146 | #root>div>nav>div>div.nav-buttons>button.signIn:hover { 147 | cursor: pointer; 148 | background-color: rgb(250, 41, 76); 149 | } 150 | 151 | .upload-button:hover { 152 | cursor: pointer; 153 | background-color: #141414; 154 | } 155 | 156 | #messages { 157 | color: white; 158 | font-size: 20px; 159 | padding-top: 5px; 160 | cursor: pointer; 161 | } 162 | 163 | .sideBar-items { 164 | display: flex; 165 | flex-direction: column; 166 | width: 350px; 167 | margin-left: 8%; 168 | margin-top: 5%; 169 | top: 0; 170 | position: fixed; 171 | overflow-y: scroll; 172 | scrollbar-color: grey; 173 | scrollbar-width: thin; 174 | height: 800px; 175 | } 176 | 177 | .sideBar-items::-webkit-scrollbar-thumb { 178 | width: 6px; 179 | height: 100%; 180 | border-radius: 3px; 181 | background: rgba(255, 255, 255, .08); 182 | } 183 | 184 | .sideBar-items::-webkit-scrollbar { 185 | width: 4px; 186 | } 187 | 188 | .sideBar-container-clicked { 189 | display: flex; 190 | padding-top: 10px; 191 | padding-bottom: 10px; 192 | padding-left: 8px; 193 | gap: 5px; 194 | font-size: 18px; 195 | line-height: 25px; 196 | text-decoration: none; 197 | color: rgb(255, 59, 92); 198 | ; 199 | font-weight: 400; 200 | display: flex; 201 | gap: 10px; 202 | border-radius: 4px; 203 | } 204 | 205 | .sideBar-container-clicked:hover { 206 | cursor: pointer; 207 | background-color: #2a2a2a; 208 | } 209 | 210 | .sideBar-container { 211 | display: flex; 212 | padding-top: 10px; 213 | padding-bottom: 10px; 214 | padding-left: 8px; 215 | gap: 5px; 216 | font-size: 18px; 217 | line-height: 25px; 218 | text-decoration: none; 219 | color: white; 220 | font-weight: 400; 221 | display: flex; 222 | gap: 10px; 223 | border-radius: 4px; 224 | } 225 | 226 | .sideBar-container:hover { 227 | cursor: pointer; 228 | background-color: #2a2a2a; 229 | } 230 | 231 | .sideBar-buttons { 232 | display: flex; 233 | flex-direction: column; 234 | gap: 10px; 235 | } 236 | 237 | #for-you-logo { 238 | font-size: 20px; 239 | } 240 | 241 | #following-logo { 242 | font-size: 20px; 243 | } 244 | 245 | .login-header { 246 | color: rgb(118, 117, 117); 247 | ; 248 | font-size: 16px; 249 | line-height: 22px; 250 | } 251 | 252 | .login-container-sidebar { 253 | padding-bottom: 25px; 254 | border-bottom: .8px solid rgba(66, 66, 66, .5); 255 | } 256 | 257 | .suggested-headline { 258 | font-size: 14px; 259 | line-height: 20px; 260 | margin-bottom: 8px; 261 | color: rgb(118, 117, 117); 262 | ; 263 | } 264 | 265 | .suggested-see-all { 266 | color: rgb(255, 59, 92); 267 | font-weight: 600; 268 | font-size: 14px; 269 | line-height: 20px; 270 | cursor: pointer; 271 | } 272 | 273 | .suggested-feed { 274 | border-bottom: .8px solid rgba(66, 66, 66, .5); 275 | } 276 | 277 | #root>div>div.sideBar-items>div.login-container-sidebar>button.signIn { 278 | font-size: 18px; 279 | line-height: 25px; 280 | color: rgba(255, 59, 92, 1); 281 | text-decoration: none; 282 | font-weight: 400; 283 | width: 100%; 284 | height: 48px; 285 | border-color: rgba(255, 59, 92, 1); 286 | background-color: rgba(255, 255, 255, .08); 287 | border-width: 1px; 288 | border-style: solid; 289 | border-radius: 4px; 290 | } 291 | 292 | #root>div>div.sideBar-items>div.login-container-sidebar>button.signIn:hover { 293 | cursor: pointer; 294 | background-color: rgb(36, 16, 16); 295 | } 296 | 297 | .navbar-profile { 298 | width: 30px; 299 | height: 30px; 300 | border-radius: 33px; 301 | cursor: pointer; 302 | } 303 | 304 | .dropdown { 305 | background-clip: padding-box; 306 | left: 0; 307 | list-style: none; 308 | margin-top: 2px; 309 | position: absolute; 310 | top: 100%; 311 | width: 200px; 312 | z-index: 100; 313 | margin-left: 75%; 314 | } 315 | 316 | .main-menu-inner { 317 | border-radius: 3px; 318 | background: rgb(49, 49, 49); 319 | color: white; 320 | } 321 | 322 | .main-menu-wrapper { 323 | min-width: 200px; 324 | padding: 24px 0; 325 | display: block; 326 | } 327 | 328 | .dropdown-info { 329 | display: flex; 330 | flex-direction: column; 331 | text-align: center; 332 | color: rgb(156, 155, 155); 333 | width: 200px; 334 | } 335 | 336 | .dropdown-links { 337 | display: flex; 338 | flex-direction: column; 339 | text-align: center; 340 | gap: 5px; 341 | } 342 | 343 | .info-item { 344 | top: 0; 345 | bottom: 0; 346 | margin-top: 0px; 347 | } 348 | 349 | .dropdown-divider { 350 | width: 70%; 351 | border-color: rgb(178, 178, 178); 352 | opacity: .4; 353 | } 354 | 355 | .dropdown-link-item { 356 | text-decoration: none; 357 | color: white; 358 | padding-top: 10px; 359 | padding-bottom: 10px; 360 | } 361 | 362 | .dropdown-link-item:hover { 363 | cursor: pointer; 364 | background-color: #2a2a2a; 365 | } 366 | 367 | .profile-navi { 368 | width: 45px; 369 | height: 45px; 370 | border-radius: 33px; 371 | } 372 | 373 | .search-results { 374 | display: flex; 375 | flex-direction: column; 376 | color: white; 377 | background-clip: padding-box; 378 | left: 0; 379 | top: 0; 380 | padding-top: 0px; 381 | list-style: none; 382 | margin-top: 2px; 383 | position: absolute; 384 | top: 100%; 385 | width: 250px; 386 | z-index: 100; 387 | margin-left: 34%; 388 | margin-bottom: 15px; 389 | background-color: rgb(49, 49, 49); 390 | border-radius: 7px; 391 | 392 | } 393 | 394 | .search-result-items { 395 | display: flex; 396 | } 397 | 398 | .search-results-header { 399 | color: rgb(156, 155, 155); 400 | padding-left: 15px; 401 | } 402 | 403 | .search-names { 404 | color: rgb(156, 155, 155); 405 | } 406 | 407 | .user-suggested-nav { 408 | display: flex; 409 | gap: 10px; 410 | color: rgb(156, 155, 155); 411 | font-size: 14px; 412 | margin-bottom: 10px; 413 | padding-top: 5px; 414 | padding-bottom: 5px; 415 | padding-left: 5px; 416 | width: 2401x; 417 | padding-left: 15px; 418 | } 419 | 420 | .user-suggested-nav:hover { 421 | background-color: #2a2a2a; 422 | } 423 | 424 | .user-header-navi { 425 | text-decoration: none; 426 | color: white; 427 | } 428 | 429 | .fixed-sideBar { 430 | color: rgb(156, 155, 155); 431 | } 432 | 433 | .socialIcon { 434 | width: 25px; 435 | } 436 | 437 | .socialsContainer { 438 | display: flex; 439 | gap: 5px; 440 | } 441 | 442 | .social-footer { 443 | font-size: 12px; 444 | } 445 | 446 | .sideBar-seperator { 447 | opacity: .2; 448 | border-color: grey; 449 | } 450 | 451 | @media only screen and (min-device-width: 320px) and (max-device-width: 480px) and (-webkit-min-device-pixel-ratio: 2) { 452 | .sideBar-items { 453 | width: 250px; 454 | } 455 | 456 | .nav-buttons { 457 | margin-left: 7%; 458 | } 459 | 460 | .search-bar { 461 | width: 200px; 462 | margin-left: 4%; 463 | } 464 | } 465 | 466 | 467 | @media screen 468 | and (min-device-width: 900px) 469 | and (max-device-width: 1600px) 470 | and (-webkit-min-device-pixel-ratio: 1){ 471 | .sideBar-items { 472 | width: 250px; 473 | } 474 | 475 | .nav-buttons { 476 | margin-left: 7%; 477 | } 478 | 479 | .search-bar { 480 | width: 200px; 481 | margin-left: 4%; 482 | } 483 | } 484 | -------------------------------------------------------------------------------- /react-app/src/components/FastForwardIndexItem.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { useLocation, useParams, NavLink, useHistory, Link } from "react-router-dom"; 4 | import * as fastForwardDetailsActions from "../store/fastForwardDetails"; 5 | import * as fastForwardActions from "../store/fastForward"; 6 | import * as followActions from '../store/follower' 7 | import * as likeActions from '../store/likePosts' 8 | // import * as followActions from '../../store/follower' 9 | import { getComments, deleteComment } from "../store/comment"; 10 | import CommentForm from "./CommentForm"; 11 | import CommentEditForm from "./CommentEditForm"; 12 | import './FastForwards.css' 13 | import CaptionEditForm from "./CaptionEditForm"; 14 | import FollowButton from "./FollowButton"; 15 | import { toast } from 'react-hot-toast'; 16 | 17 | const FastForwardIndexItem = () => { 18 | const fastForwardId = Number(useLocation().pathname.split("/")[2]); 19 | const user = useSelector((state) => state.session.user); 20 | const fastForwards = Object.values(useSelector((state) => state.fastForward)); 21 | const fastForward = fastForwards.filter(fastForward => fastForwardId === fastForward.id)[0] 22 | let followings = useSelector((state) => Object.values(state.follower.following)) 23 | followings = followings.map((user) => user.id) 24 | 25 | const [commentBody, setCommentBody] = useState(""); 26 | const [captionBody, setCaptionBody] = useState(""); 27 | const [showEdit, setShowEdit] = useState(false); 28 | const [showEdit2, setShowEdit2] = useState(false); 29 | const [editId, setEditId] = useState(-1); 30 | const [editId2, setEditId2] = useState(-1); 31 | const [following, setFollowing] = useState(followings.includes(fastForward?.user_id)) 32 | const [isLoaded, setIsLoaded] = useState(false); 33 | const { id } = useParams(); 34 | const history = useHistory(); 35 | const dispatch = useDispatch(); 36 | 37 | useEffect(() => { 38 | dispatch(fastForwardActions.fetchAllFastForwards()); 39 | }, [dispatch]); 40 | 41 | const deleteFastForward = async () => { 42 | await dispatch(fastForwardActions.fetchDeleteFastForward(fastForward.id)) 43 | .then(history.push(`/users/${user.id}`)) 44 | }; 45 | 46 | const handleDelete = async (commentId, fastForwardId) => { 47 | await dispatch(deleteComment(commentId, fastForwardId)) 48 | await dispatch(fastForwardActions.fetchAllFastForwards()) 49 | }; 50 | 51 | const handleFollow = (followerId, followedId) => { 52 | if (!following) { 53 | dispatch(followActions.follow(followerId, followedId)) 54 | .then(() => setFollowing(true)) 55 | } else { 56 | dispatch(followActions.unfollow(followerId, followedId)) 57 | .then(() => setFollowing(false)) 58 | } 59 | } 60 | 61 | const handleCopy = (fastForward) => { 62 | navigator.clipboard.writeText(fastForward.url) 63 | toast("URL copied to clipnoard") 64 | } 65 | 66 | const handleLike = async (fastForward) => { 67 | if(!user) return toast.error("Please log-in to like posts") 68 | const likes = fastForward?.LikePosts?.filter(like => like.user_id === user.id) 69 | if (!likes.length > 0) { 70 | await dispatch(likeActions.createLike(fastForward.id)) 71 | await dispatch(fastForwardActions.fetchAllFastForwards()) 72 | } 73 | else { 74 | await dispatch(likeActions.deleteLike(likes[0].id, fastForward.id)) 75 | await dispatch(fastForwardActions.fetchAllFastForwards()) 76 | } 77 | } 78 | 79 | useEffect(() => { 80 | if (user) { 81 | dispatch(followActions.followingList(user.id)) 82 | .then(() => setIsLoaded(true)) 83 | } 84 | }, [dispatch, isLoaded]); 85 | 86 | return ( 87 |
88 | 89 | 92 |
93 |
94 |
95 |
96 |
97 |
98 | Profile 103 |
104 |
105 |
106 | {fastForward?.User?.username} 107 |
{fastForward?.User?.first_name} {fastForward?.User.last_name}
108 |
109 |
110 | {fastForward?.caption} 111 |
112 | {fastForward?.User?.id === user?.id && 113 |
114 |
115 | Delete 116 |
117 |
{ 121 | if (editId2 === fastForward?.id) { 122 | setEditId2(-1); 123 | setEditId2(""); 124 | return; 125 | } 126 | setShowEdit(!showEdit) 127 | setEditId2(fastForward.id); 128 | setCaptionBody(fastForward.caption); 129 | }}> 130 | Edit 131 |
132 |
} 133 |
134 | {showEdit && ( 135 | 142 | )} 143 |
144 |
145 |
146 | {user &&
handleFollow(user.id, fastForward.User.id)}>{!following ? "Follow" : "Following"}
} 147 |
148 |
149 |
150 |
151 |
handleLike(fastForward)}> 152 | like.user_id === user.id).length > 0 ? "liked" : "un-liked"} class="fa-solid fa-heart"> 153 |
154 |
{fastForward?.LikePosts?.length}
155 |
156 |
157 |
158 | 159 |
160 |
161 | {fastForward?.Comments?.length} 162 |
163 |
164 |
165 |
166 |
167 | 172 | 173 |
handleCopy(fastForward)} className="share-item"> 174 | Copy link 175 |
176 |
177 |
178 |
179 |
180 | {fastForward?.Comments?.map((comment) => ( 181 |
182 |
183 | Profile 188 |
{comment.User.username}
189 |
190 |
{comment.body}
191 | {comment?.user_id === user?.id && ( 192 |
193 |
handleDelete(comment.id, fastForward.id)} 196 | > 197 |
Delete
198 |
199 |
{ 204 | if (editId === comment.id) { 205 | setEditId(-1); 206 | setEditId(""); 207 | return; 208 | } 209 | setEditId(comment.id); 210 | setCommentBody(comment.body); 211 | }} 212 | > 213 |
Edit
214 |
215 |
216 | )} 217 |
218 | {editId === comment.id && ( 219 | 227 | )} 228 |
229 |
230 | 231 | ))} 232 |
233 |
234 |
235 |
236 | 238 |
239 |
240 |
241 | ) 242 | } 243 | 244 | export default FastForwardIndexItem 245 | -------------------------------------------------------------------------------- /react-app/src/components/FastForwards.css: -------------------------------------------------------------------------------- 1 | #root > div > form > input[type=file] { 2 | margin-top: 25%; 3 | margin-left: 25%; 4 | color: white; 5 | } 6 | 7 | #root > div > div:nth-child(3) { 8 | padding-top: 5%; 9 | } 10 | 11 | .fast-forward-feed { 12 | margin-left: 45%; 13 | display: flex; 14 | flex-direction: column; 15 | gap: 25px; 16 | } 17 | 18 | .item-header3 { 19 | display: flex; 20 | gap: 20px; 21 | align-items: center; 22 | margin-left: 5%; 23 | padding-bottom: 1%; 24 | } 25 | 26 | .item-header { 27 | display: flex; 28 | gap: 20px; 29 | align-items: center; 30 | margin-left: 5%; 31 | } 32 | 33 | .item-wrapper { 34 | box-shadow: rgb(255 255 255 / 12%) 0px 1px 1px; 35 | padding-bottom: 2%; 36 | } 37 | 38 | .item-header2 { 39 | display: flex; 40 | gap: 10px; 41 | color: white; 42 | padding-bottom: 5px; 43 | } 44 | 45 | .profileImage { 46 | width: 50px; 47 | height: 50px; 48 | border-radius: 33px; 49 | } 50 | 51 | .caption { 52 | padding-bottom: 5px; 53 | color: white; 54 | text-decoration: none; 55 | width: 330px; 56 | display: block; 57 | } 58 | 59 | .video { 60 | margin-left: 9%; 61 | padding-top: 0; 62 | top: 0; 63 | background-color: black; 64 | background-size: cover; 65 | border-radius: 8px; 66 | cursor: pointer; 67 | } 68 | 69 | .video-username { 70 | font-weight: bold; 71 | font-size: 18px; 72 | text-decoration: none; 73 | color: white; 74 | } 75 | 76 | .video-username:hover { 77 | cursor: pointer; 78 | text-decoration: underline; 79 | } 80 | 81 | .video-name { 82 | font-size: 14px; 83 | margin-top: 4px; 84 | } 85 | 86 | #root > div > div.fastForward-wrapper { 87 | display: flex; 88 | align-items: center; 89 | height: 890px; 90 | width: 100%; 91 | } 92 | 93 | #root > div > div.fastForward-wrapper > video { 94 | background-color: black; 95 | } 96 | 97 | #root > div > div > div > div > div.item-header3 > div.right > div.caption-wrapper > a:hover { 98 | text-decoration: underline; 99 | } 100 | 101 | .video-comment { 102 | display: flex; 103 | gap: 10px; 104 | } 105 | 106 | #share-icon { 107 | font-size: 14px; 108 | color: rgba(255, 255, 255, 0.9); 109 | cursor: pointer; 110 | margin-left: 1px; 111 | } 112 | 113 | #liked-feed { 114 | font-size: 18px; 115 | color: rgb(255, 59, 92); 116 | cursor: pointer; 117 | margin-left: 1px; 118 | } 119 | 120 | #un-liked-feed { 121 | font-size: 18px; 122 | color: rgba(255, 255, 255, 0.9); 123 | cursor: pointer; 124 | margin-left: 1px; 125 | } 126 | 127 | #comment-icon { 128 | font-size: 18px; 129 | color: rgba(255, 255, 255, 0.9); 130 | cursor: pointer; 131 | margin-left: 1px; 132 | } 133 | 134 | .video-sidebar { 135 | display: flex; 136 | flex-direction: column; 137 | align-items: center; 138 | gap: 10px; 139 | justify-content: flex-end; 140 | margin-bottom: 2%; 141 | } 142 | 143 | .comments-sidebar { 144 | height: 700px; 145 | width: 100%; 146 | } 147 | 148 | .comment-counter { 149 | color: white; 150 | font-size: 12px; 151 | } 152 | 153 | .textarea-comments { 154 | position: absolute; 155 | border-top: .3px solid rgb(89, 88, 88); 156 | padding-top: 15px; 157 | width: 700px; 158 | } 159 | 160 | #form1 > label > input[type=text] { 161 | border: none; 162 | background-color: rgb(46, 46, 46); 163 | caret-color: rgb(255, 59, 92); 164 | color: rgba(255, 255, 255, 0.9); 165 | outline: none; 166 | width: 70%; 167 | height: 35px; 168 | padding-left: 15px; 169 | margin-left: 5%; 170 | border-radius: 7px; 171 | } 172 | 173 | 174 | .comments-wrapper { 175 | height: 100%; 176 | width: 700px; 177 | } 178 | 179 | .comment-button { 180 | margin-left: 5px; 181 | border: none; 182 | background-color: rgb(18, 18, 18); 183 | color: rgb(156, 155, 155); 184 | } 185 | 186 | .comment-button-active { 187 | margin-left: 5px; 188 | border: none; 189 | background-color: rgb(18, 18, 18); 190 | color: rgb(255, 59, 92); 191 | cursor: pointer; 192 | } 193 | 194 | #form1 > ul { 195 | width: 500px; 196 | } 197 | 198 | .wrapper-plus-summary { 199 | display: flex; 200 | flex-direction: column; 201 | justify-content: center; 202 | } 203 | 204 | .copy-wrapper { 205 | -webkit-box-pack: center; 206 | justify-content: center; 207 | -webkit-box-align: center; 208 | align-items: center; 209 | width: 22px; 210 | height: 22px; 211 | font-size: 0px; 212 | border-radius: 50%; 213 | background-color: rgba(255, 255, 255, 0.12); 214 | padding: 8px; 215 | margin-left: 5%; 216 | margin-top: 10%; 217 | cursor: pointer; 218 | } 219 | 220 | .like-wrapper-feed { 221 | -webkit-box-pack: center; 222 | justify-content: center; 223 | -webkit-box-align: center; 224 | align-items: center; 225 | width: 22px; 226 | height: 22px; 227 | font-size: 0px; 228 | border-radius: 50%; 229 | background-color: rgba(255, 255, 255, 0.12); 230 | padding: 8px; 231 | margin-left: 5%; 232 | margin-top: 10%; 233 | cursor: pointer; 234 | } 235 | 236 | .comment-wrapper { 237 | -webkit-box-pack: center; 238 | justify-content: center; 239 | -webkit-box-align: center; 240 | align-items: center; 241 | width: 22px; 242 | height: 22px; 243 | font-size: 0px; 244 | border-radius: 50%; 245 | background-color: rgba(255, 255, 255, 0.12); 246 | padding: 8.5px; 247 | margin-left: 5%; 248 | margin-top: 10%; 249 | cursor: pointer; 250 | } 251 | 252 | .comment-wrapper:hover { 253 | background-color: rgba(224, 214, 214, 0.12); 254 | } 255 | 256 | .like-wrapper-feed:hover { 257 | background-color: rgba(224, 214, 214, 0.12); 258 | } 259 | 260 | .copy-wrapper:hover { 261 | background-color: rgba(224, 214, 214, 0.12); 262 | } 263 | 264 | .comment-wrapper2 { 265 | display: flex; 266 | flex-direction: column; 267 | padding-top: 2%; 268 | } 269 | 270 | .profileImage2 { 271 | width: 28px; 272 | height: 28px; 273 | border-radius: 33px; 274 | } 275 | 276 | #root > div > div.fastForward-wrapper > div > div.comments-wrapper > div > div.scroll-body > div > div.item-header > div { 277 | color: white; 278 | font-size: 18px; 279 | } 280 | 281 | .comment-body { 282 | margin-left: 11.5%; 283 | margin-top: 5px; 284 | top: 0; 285 | color: rgba(255, 255, 255, 0.9); 286 | font-size: 16px; 287 | } 288 | 289 | .comment-buttons { 290 | margin-left: 11.5%; 291 | margin-top: 10px; 292 | display: flex; 293 | gap: 10px; 294 | color: rgba(255, 255, 255, 0.5);; 295 | font-size: 14px; 296 | cursor: pointer; 297 | } 298 | 299 | .scroll-body { 300 | overflow-y: scroll; 301 | max-height: 600px; 302 | } 303 | 304 | .edit-text { 305 | margin-left: 11.5%; 306 | resize: none; 307 | } 308 | 309 | .exit-redirect { 310 | position: absolute; 311 | left: 0; 312 | right: 0; 313 | padding-left: 15px; 314 | font-size: 34px; 315 | color: grey; 316 | margin-bottom: 840px; 317 | z-index: 50; 318 | } 319 | 320 | .exit-redirect:hover { 321 | opacity: .7; 322 | } 323 | 324 | .follow-user-button { 325 | border-width: 1px; 326 | border-style: solid; 327 | border-radius: 4px; 328 | color: rgb(255, 59, 92); 329 | border-color: rgb(255, 59, 92); 330 | background-color: rgba(255, 255, 255, 0.08); 331 | min-width: 106px; 332 | min-height: 28px; 333 | font-size: 16px; 334 | line-height: 22px; 335 | font-weight: 600; 336 | display: flex; 337 | position: relative; 338 | -webkit-box-align: center; 339 | align-items: center; 340 | -webkit-box-pack: center; 341 | justify-content: center; 342 | padding: 6px 8px; 343 | user-select: none; 344 | cursor: pointer; 345 | box-sizing: border-box; 346 | margin-left: 150px; 347 | } 348 | 349 | .follow-user-button:hover { 350 | cursor: pointer; 351 | background-color: rgb(36, 16, 16);} 352 | 353 | .following-user-button { 354 | border-width: 1px; 355 | border-style: solid; 356 | border-radius: 4px; 357 | color: rgba(255, 255, 255, 0.9); 358 | border: 1px solid rgba(22, 24, 35, 0.12); 359 | background-color: rgb(35, 35, 35); 360 | min-width: 106px; 361 | min-height: 28px; 362 | font-size: 16px; 363 | line-height: 22px; 364 | font-weight: 600; 365 | display: flex; 366 | position: relative; 367 | -webkit-box-align: center; 368 | align-items: center; 369 | -webkit-box-pack: center; 370 | justify-content: center; 371 | padding: 6px 8px; 372 | user-select: none; 373 | cursor: pointer; 374 | box-sizing: border-box; 375 | margin-left: 150px; 376 | } 377 | 378 | .following-user-button:hover { 379 | cursor: pointer; 380 | background-color: #141414; 381 | } 382 | 383 | .follow-feed { 384 | margin-left: 45%; 385 | margin-top: 4%; 386 | } 387 | 388 | .upload-headline-wrapper { 389 | display: flex; 390 | flex-direction: column; 391 | margin-top: 50px; 392 | margin-left: 5%; 393 | } 394 | 395 | .upload-title { 396 | color: white; 397 | } 398 | 399 | .upload-header-text { 400 | color: white; 401 | } 402 | 403 | .caption-label { 404 | color: white; 405 | } 406 | 407 | .upload-subtitle { 408 | color: rgb(169, 166, 166); 409 | margin-bottom: 45px; 410 | } 411 | 412 | .upload-page { 413 | margin-left: 150px; 414 | margin-right: 150px; 415 | margin-top: 10%; 416 | width: 1400px; 417 | height: 700px; 418 | background-color: rgb(37, 37, 37); 419 | box-shadow: rgb(0 0 0 / 6%) 0px 2px 8px; 420 | } 421 | 422 | .upload-block { 423 | border: 2px dashed rgb(169, 166, 166); 424 | width: 250px; 425 | height: 350px; 426 | margin-left: 50px; 427 | display: flex; 428 | flex-direction: column; 429 | align-items: center; 430 | } 431 | 432 | .upload-list { 433 | list-style: none; 434 | padding-left: 0px; 435 | } 436 | 437 | .upload-sub-text { 438 | font-size: 12px; 439 | top: 0; 440 | bottom: 0; 441 | color: rgb(169, 166, 166); 442 | } 443 | 444 | .upload-image { 445 | display: flex; 446 | color: rgb(255, 59, 92); 447 | font-size: 20px; 448 | margin-left: 27%; 449 | gap: 55px; 450 | align-items: center; 451 | } 452 | 453 | .upload-item { 454 | display: flex; 455 | gap: 15px; 456 | padding: 15px; 457 | padding-top: 5px; 458 | padding-left: 5px; 459 | border-radius: 7px; 460 | background-color: rgb(62, 62, 62); 461 | width: 250px; 462 | } 463 | 464 | #upload-item-1 { 465 | animation-name: myAnimation; 466 | animation-iteration-count: infinite; 467 | animation-duration: 15s; 468 | animation-delay: 1s; 469 | animation-fill-mode: forwards; 470 | } 471 | 472 | #upload-item-2 { 473 | animation-name: myAnimation; 474 | animation-iteration-count: infinite; 475 | animation-duration: 15s; 476 | animation-delay: 2s; 477 | animation-fill-mode: forwards; 478 | } 479 | 480 | #upload-item-3 { 481 | animation-name: myAnimation; 482 | animation-iteration-count: infinite; 483 | animation-duration: 15s; 484 | animation-delay: 3s; 485 | animation-fill-mode: forwards; 486 | } 487 | 488 | .upload-item-text { 489 | color: white; 490 | } 491 | 492 | #upload-number-1 { 493 | font-size: 60px; 494 | line-height: 10px; 495 | } 496 | 497 | #upload-number-2 { 498 | font-size: 60px; 499 | line-height: 10px; 500 | } 501 | 502 | #upload-number-3 { 503 | font-size: 60px; 504 | line-height: 10px; 505 | } 506 | 507 | #cloud-icon { 508 | font-size: 28px; 509 | color: grey; 510 | } 511 | 512 | .file-drop { 513 | margin-top: 35px; 514 | margin-left: 20px; 515 | color: white; 516 | width: 200px; 517 | overflow: hide; 518 | } 519 | 520 | .form-fields { 521 | display: flex; 522 | gap: 55px; 523 | } 524 | 525 | .caption-block { 526 | display: flex; 527 | align-items: center; 528 | gap: 10px; 529 | height: 35px; 530 | width: 650px; 531 | margin-top: 200px; 532 | margin-left: 8%; 533 | } 534 | 535 | .caption-input { 536 | border: none; 537 | background-color: rgb(62, 62, 62); 538 | caret-color: rgb(255, 59, 92); 539 | color: rgba(255, 255, 255, 0.9); 540 | outline: none; 541 | width: 100%; 542 | height: 35px; 543 | padding-left: 15px; 544 | } 545 | 546 | .submit-buttons { 547 | margin-left: 45%; 548 | position: relative; 549 | width: 150px; 550 | margin-bottom: 550px; 551 | display: flex; 552 | gap: 35px; 553 | } 554 | 555 | .discard-button { 556 | border-radius: 4px; 557 | border: .4px solid rgb(189, 188, 188); 558 | color: black; 559 | background-color: white; 560 | min-width: 180px; 561 | min-height: 46px; 562 | font-size: 16px; 563 | line-height: 22px; 564 | } 565 | 566 | .submit-button-upload { 567 | border-radius: 4px; 568 | border: none; 569 | color: rgb(255, 255, 255); 570 | background-color: rgb(255, 59, 92); 571 | min-width: 380px; 572 | min-height: 46px; 573 | font-size: 16px; 574 | line-height: 22px; 575 | } 576 | 577 | .submit-button-upload:hover { 578 | background-color: rgb(250, 41, 76); 579 | cursor: pointer; 580 | } 581 | 582 | .discard-button:hover { 583 | background-color: rgb(243, 243, 243); 584 | cursor: pointer; 585 | } 586 | 587 | #root > div > div.upload-page > form > div.submit-buttons > p { 588 | color: white; 589 | } 590 | 591 | .share-link { 592 | border-radius: 5px; 593 | overflow-x: hidden; 594 | overflow-y: hidden; 595 | margin-left: 5%; 596 | margin-top: 2%; 597 | width: 81%; 598 | height: 30px; 599 | display: flex; 600 | } 601 | 602 | .share-item-2 { 603 | width: 100%; 604 | text-overflow: ellipsis; 605 | overflow: hidden; 606 | white-space: nowrap; 607 | flex: 1 1 auto; 608 | padding: 7px 0px 5px 12px; 609 | background-color: rgba(255, 255, 255, 0.12); 610 | color: rgba(255, 255, 255, 0.75); 611 | font-size: 14px; 612 | line-height: 20px; 613 | } 614 | 615 | .share-line { 616 | display: flex; 617 | align-items: center; 618 | width: 100%; 619 | } 620 | 621 | .share-item { 622 | color: white; 623 | height: 24px; 624 | padding: 5px; 625 | padding-left: 15px; 626 | width: 16%; 627 | font-size: 14px; 628 | border: none; 629 | outline: none; 630 | background-color: rgba(157, 157, 157, 0.12); 631 | } 632 | 633 | .share-item:hover { 634 | background-color: rgb(27, 27, 27); 635 | cursor: pointer; 636 | } 637 | 638 | .signin-text-comments { 639 | color: rgb(255, 59, 92); 640 | font-weight: 600; 641 | border-radius: 2px; 642 | padding: 14px 16px; 643 | font-size: 16px; 644 | cursor: pointer; 645 | text-align: center; 646 | text-decoration: none; 647 | } 648 | 649 | .comment-signin-wrapper { 650 | padding-top: 15px; 651 | padding-bottom: 15px; 652 | width: 90%; 653 | margin-left: 25px; 654 | background: rgb(37, 37, 37); 655 | border-radius: 2px; 656 | } 657 | 658 | .follow-button-followed { 659 | border-width: 1px; 660 | border-style: solid; 661 | border-radius: 4px; 662 | color: rgba(255, 255, 255, 0.9); 663 | border-color: rgba(255, 255, 255, 0); 664 | background-color: rgb(35, 35, 35); 665 | min-width: 86px; 666 | min-height: 23px; 667 | font-size: 16px; 668 | line-height: 22px; 669 | font-weight: 100; 670 | cursor: pointer; 671 | margin-left: 75%; 672 | display: flex; 673 | align-items: center; 674 | justify-content: center; 675 | } 676 | 677 | .follow-button-unfollowed { 678 | border-width: 1px; 679 | border-style: solid; 680 | border-radius: 4px; 681 | color: rgba(255, 59, 92, 1); 682 | border-color: rgba(255, 59, 92, 1); 683 | background-color: rgba(255, 255, 255, .08); 684 | min-width: 86px; 685 | min-height: 23px; 686 | font-size: 16px; 687 | line-height: 22px; 688 | font-weight: 100; 689 | display: flex; 690 | align-items: center; 691 | justify-content: center; 692 | margin-left: 75%; 693 | cursor: pointer; 694 | } 695 | 696 | .follow-button-unfollowed:hover { 697 | background-color: rgb(36, 16, 16); 698 | } 699 | 700 | .follow-button-followed:hover { 701 | border: 1px solid white 702 | } 703 | 704 | .follow-button-followed-user { 705 | border-width: 1px; 706 | border-style: solid; 707 | border-radius: 4px; 708 | color: rgba(255, 255, 255, 0.9); 709 | border-color: rgba(255, 255, 255, 0); 710 | background-color: rgb(35, 35, 35); 711 | min-width: 198px; 712 | min-height: 36px; 713 | font-size: 16px; 714 | line-height: 22px; 715 | font-weight: 100; 716 | cursor: pointer; 717 | display: flex; 718 | align-items: center; 719 | justify-content: center; 720 | margin-top: 15px; 721 | } 722 | 723 | .follow-button-unfollowed-user { 724 | border-width: 1px; 725 | border-style: solid; 726 | border-radius: 4px; 727 | color: rgba(255, 255, 255, 0.9); 728 | border-color: rgba(255, 255, 255, 0); 729 | background-color: rgb(255, 59, 92); 730 | min-width: 198px; 731 | min-height: 36px; 732 | font-size: 16px; 733 | line-height: 22px; 734 | font-weight: 100; 735 | cursor: pointer; 736 | display: flex; 737 | align-items: center; 738 | justify-content: center; 739 | margin-top: 15px; 740 | } 741 | 742 | .summary-line { 743 | display: flex; 744 | margin-left: 5%; 745 | margin-top: 5%; 746 | gap: 4%; 747 | } 748 | 749 | .like-summary { 750 | display: flex; 751 | align-items: center; 752 | gap: 5px; 753 | } 754 | 755 | .like-wrapper { 756 | display: flex; 757 | -webkit-box-pack: center; 758 | justify-content: center; 759 | -webkit-box-align: center; 760 | align-items: center; 761 | width: 22px; 762 | height: 22px; 763 | font-size: 0px; 764 | border-radius: 50%; 765 | background-color: rgba(255, 255, 255, 0.12); 766 | padding: 6px; 767 | margin-left: 5%; 768 | margin-top: 10%; 769 | } 770 | 771 | .comment-wrapper-2 { 772 | display: flex; 773 | -webkit-box-pack: center; 774 | justify-content: center; 775 | -webkit-box-align: center; 776 | align-items: center; 777 | width: 22px; 778 | height: 22px; 779 | font-size: 0px; 780 | border-radius: 50%; 781 | background-color: rgba(255, 255, 255, 0.12); 782 | padding: 6px; 783 | margin-left: 5%; 784 | margin-top: 10%; 785 | } 786 | 787 | .like-wrapper:hover { 788 | background-color: rgba(224, 214, 214, 0.12); 789 | cursor: pointer; 790 | } 791 | 792 | #liked { 793 | font-size: 18px; 794 | color: rgb(255, 59, 92); 795 | cursor: pointer; 796 | } 797 | 798 | #un-liked { 799 | font-size: 18px; 800 | color: rgba(255, 255, 255, 0.9); 801 | cursor: pointer; 802 | } 803 | 804 | #share-icon { 805 | font-size: 18px; 806 | color: rgba(255, 255, 255, 0.9); 807 | cursor: pointer; 808 | margin-left: 2px; 809 | } 810 | 811 | #comment { 812 | font-size: 18px; 813 | color: rgba(255, 255, 255, 0.9); 814 | } 815 | 816 | .like-number { 817 | color: rgba(255, 255, 255, .75); 818 | font-size: 12px; 819 | line-height: 17px; 820 | text-align: center; 821 | margin-top: 10%; 822 | } 823 | 824 | @keyframes myAnimation { 825 | 0% { 826 | opacity: 1; 827 | } 828 | 829 | 50% { 830 | opacity: 0; 831 | } 832 | 833 | 100% { 834 | opacity: 1; 835 | } 836 | } 837 | 838 | @media only screen and (min-device-width: 320px) and (max-device-width: 480px) and (-webkit-min-device-pixel-ratio: 2) { 839 | .fast-forward-feed { 840 | margin-left: 35%; 841 | } 842 | } 843 | --------------------------------------------------------------------------------