├── .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 |
29 |
30 |
{children}
31 |
,
32 | modalNode
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/react-app/src/components/LoginFormModal/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Modal } from "../../context/Modal";
3 | import LoginForm from "./LoginForm";
4 |
5 |
6 | function LoginFormModal({ nav }) {
7 | const [showModal, setShowModal] = useState(false);
8 |
9 | return (
10 | <>
11 | {!nav && (
12 | setShowModal(true)}
16 | >
17 | Sign In
18 |
19 | )}
20 | {nav && (
21 | setShowModal(true)}
25 | >
26 | Start Reading
27 |
28 | )}
29 | {showModal && (
30 | setShowModal(false)}>
31 |
32 |
33 | )}
34 | >
35 | );
36 | }
37 |
38 | export default LoginFormModal;
39 |
--------------------------------------------------------------------------------
/migrations/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # template used to generate migration files
5 | file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(slug)s
6 |
7 | # set to 'true' to run the environment during
8 | # the 'revision' command, regardless of autogenerate
9 | # revision_environment = false
10 |
11 |
12 | # Logging configuration
13 | [loggers]
14 | keys = root,sqlalchemy,alembic
15 |
16 | [handlers]
17 | keys = console
18 |
19 | [formatters]
20 | keys = generic
21 |
22 | [logger_root]
23 | level = WARN
24 | handlers = console
25 | qualname =
26 |
27 | [logger_sqlalchemy]
28 | level = WARN
29 | handlers =
30 | qualname = sqlalchemy.engine
31 |
32 | [logger_alembic]
33 | level = INFO
34 | handlers =
35 | qualname = alembic
36 |
37 | [handler_console]
38 | class = StreamHandler
39 | args = (sys.stderr,)
40 | level = NOTSET
41 | formatter = generic
42 |
43 | [formatter_generic]
44 | format = %(levelname)-5.5s [%(name)s] %(message)s
45 | datefmt = %H:%M:%S
--------------------------------------------------------------------------------
/app/forms/login_form.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField
3 | from wtforms.validators import DataRequired, Email, ValidationError
4 | from app.models import User
5 |
6 |
7 | def user_exists(form, field):
8 | # Checking if user exists
9 | email = field.data
10 | user = User.query.filter(User.email == email).first()
11 | if not user:
12 | raise ValidationError('Email provided not found.')
13 |
14 |
15 | def password_matches(form, field):
16 | # Checking if password matches
17 | password = field.data
18 | email = form.data['email']
19 | user = User.query.filter(User.email == email).first()
20 | if not user:
21 | raise ValidationError('No such user exists.')
22 | if not user.check_password(password):
23 | raise ValidationError('Password was incorrect.')
24 |
25 |
26 | class LoginForm(FlaskForm):
27 | email = StringField('email', validators=[DataRequired(), user_exists])
28 | password = StringField('password', validators=[
29 | DataRequired(), password_matches])
--------------------------------------------------------------------------------
/app/api/like_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, request
2 | from flask_login import login_required, current_user
3 | from app.models import LikePost
4 | from app.models import db, User
5 |
6 |
7 | likes_routes = Blueprint('likes', __name__)
8 |
9 | def validation_errors_to_error_messages(validation_errors):
10 | """
11 | Simple function that turns the WTForms validation errors into a simple list
12 | """
13 | errorMessages = []
14 | for field in validation_errors:
15 | for error in validation_errors[field]:
16 | errorMessages.append(f'{error}')
17 | return errorMessages
18 |
19 | @likes_routes.route('/', methods=['DELETE'])
20 | @login_required
21 | def remove_like(id):
22 | """
23 | Query for all likes for a story and returns them in a list of dictionaries
24 | """
25 | like = LikePost.query.get(id)
26 | print(like)
27 | if current_user.id == like.user_id:
28 | db.session.delete(like)
29 | db.session.commit()
30 | return {'message': 'Deleted'}
31 | return {'errors': ['Unauthorized']}
32 |
--------------------------------------------------------------------------------
/app/seeds/__init__.py:
--------------------------------------------------------------------------------
1 | from flask.cli import AppGroup
2 | from .users import seed_users, undo_users
3 | from .fast_forwards import seed_fast_forwards, undo_fast_forwards
4 |
5 | from app.models.db import db, environment, SCHEMA
6 |
7 | # Creates a seed group to hold our commands
8 | # So we can type `flask seed --help`
9 | seed_commands = AppGroup('seed')
10 |
11 |
12 | # Creates the `flask seed all` command
13 | @seed_commands.command('all')
14 | def seed():
15 | if environment == 'production':
16 | # Before seeding in production, you want to run the seed undo
17 | # command, which will truncate all tables prefixed with
18 | # the schema name (see comment in users.py undo_users function).
19 | # Make sure to add all your other model's undo functions below
20 | undo_users()
21 | seed_users()
22 | # Add other seed functions here
23 | seed_fast_forwards()
24 |
25 |
26 | # Creates the `flask seed undo` command
27 | @seed_commands.command('undo')
28 | def undo():
29 | undo_users()
30 | undo_fast_forwards()
31 | # Add other undo functions here
32 |
--------------------------------------------------------------------------------
/react-app/src/components/UsersList2.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { NavLink } from 'react-router-dom';
3 | import './User.css'
4 |
5 | function UsersList2() {
6 | const [users, setUsers] = useState([]);
7 |
8 | useEffect(() => {
9 | async function fetchData() {
10 | const response = await fetch('/api/users/');
11 | const responseData = await response.json();
12 | setUsers(responseData.users);
13 | }
14 | fetchData();
15 | }, []);
16 |
17 | const userComponents = users?.map((user) => {
18 | return (
19 |
20 |
21 |
22 | {user.username}
23 | {user?.first_name}
24 | {user?.last_name}
25 |
26 |
27 | );
28 | });
29 |
30 | return (
31 | <>
32 |
33 | >
34 | );
35 | }
36 |
37 | export default UsersList2;
38 |
--------------------------------------------------------------------------------
/react-app/src/components/UsersList.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { NavLink } from 'react-router-dom';
3 | import './User.css'
4 |
5 | function UsersList() {
6 | const [users, setUsers] = useState([]);
7 |
8 | useEffect(() => {
9 | async function fetchData() {
10 | const response = await fetch('/api/users/');
11 | const responseData = await response.json();
12 | setUsers(responseData.users);
13 | }
14 | fetchData();
15 | }, []);
16 |
17 | const userComponents = users.slice(0,5)?.map((user) => {
18 | return (
19 |
20 |
21 |
22 | {user.username}
23 | {user?.first_name}
24 | {user?.last_name}
25 |
26 |
27 | );
28 | });
29 |
30 | return (
31 | <>
32 |
33 | >
34 | );
35 | }
36 |
37 | export default UsersList;
38 |
--------------------------------------------------------------------------------
/react-app/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import session from './session'
4 | import fastForward from './fastForward'
5 | import fastForwardDetails from './fastForwardDetails'
6 | import commentReducer from './comment'
7 | import follower from './follower'
8 | import postLikeReducer from './likePosts';
9 |
10 | const rootReducer = combineReducers({
11 | session,
12 | fastForward,
13 | fastForwardDetails,
14 | comment: commentReducer,
15 | follower,
16 | postLikes: postLikeReducer,
17 | });
18 |
19 | let enhancer;
20 |
21 | if (process.env.NODE_ENV === 'production') {
22 | enhancer = applyMiddleware(thunk);
23 | } else {
24 | const logger = require('redux-logger').default;
25 | const composeEnhancers =
26 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
27 | enhancer = composeEnhancers(applyMiddleware(thunk, logger));
28 | }
29 |
30 | const configureStore = (preloadedState) => {
31 | return createStore(rootReducer, preloadedState, enhancer);
32 | };
33 |
34 | export default configureStore;
35 |
--------------------------------------------------------------------------------
/app/models/comment.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 | from .db import db, add_prefix_for_prod, environment, SCHEMA
3 |
4 | class Comment(db.Model):
5 | __tablename__ = 'comments'
6 | if environment == "production":
7 | __table_args__ = {'schema': SCHEMA}
8 | id = db.Column(db.Integer, primary_key=True)
9 | body = db.Column(db.String)
10 | user_id = db.Column(db.Integer, db.ForeignKey(add_prefix_for_prod("users.id")))
11 | fast_forward_id = db.Column(db.Integer, db.ForeignKey(add_prefix_for_prod("fastForwards.id")))
12 | user = db.relationship("User", backref="comments")
13 | fast_forwards = db.relationship("FastForward", backref="comments")
14 |
15 | # liked_comment_user = db.relationship(
16 | # "User",
17 | # secondary=like_comment,
18 | # lazy='dynamic',
19 | # back_populates = 'liked_comment')
20 |
21 | def to_dict(self):
22 | return {
23 | 'id': self.id,
24 | 'body': self.body,
25 | 'user_id': self.user_id,
26 | 'fast_forward_id': self.fast_forward_id,
27 | 'User': self.user.to_dict()
28 | }
29 |
--------------------------------------------------------------------------------
/react-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.14.1",
7 | "@testing-library/react": "^11.2.7",
8 | "@testing-library/user-event": "^12.8.3",
9 | "http-proxy-middleware": "^1.0.5",
10 | "react": "^17.0.2",
11 | "react-dom": "^17.0.2",
12 | "react-hot-toast": "^2.4.0",
13 | "react-redux": "^7.2.4",
14 | "react-router-dom": "^5.2.0",
15 | "react-scripts": "^4.0.3",
16 | "redux": "^4.1.0",
17 | "redux-logger": "^3.0.6",
18 | "redux-thunk": "^2.3.0"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "CI=false && react-scripts build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": "react-app"
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.3%",
32 | "not ie 11",
33 | "not dead",
34 | "not op_mini all"
35 | ],
36 | "development": [
37 | "last 1 chrome version",
38 | "last 1 firefox version",
39 | "last 1 safari version"
40 | ]
41 | },
42 | "proxy": "http://localhost:5000"
43 | }
44 |
--------------------------------------------------------------------------------
/app/aws.py:
--------------------------------------------------------------------------------
1 | import boto3
2 | import botocore
3 | import os
4 | import uuid
5 |
6 | BUCKET_NAME = os.environ.get("S3_BUCKET")
7 | S3_LOCATION = f"https://{BUCKET_NAME}.s3.amazonaws.com/"
8 | ALLOWED_EXTENSIONS = {"mp4", "WebM", "pdf", "png", "jpg", "jpeg", "gif"}
9 |
10 | s3 = boto3.client(
11 | "s3",
12 | aws_access_key_id=os.environ.get("S3_KEY"),
13 | aws_secret_access_key=os.environ.get("S3_SECRET")
14 | )
15 |
16 |
17 | def allowed_file(filename):
18 | return "." in filename and \
19 | filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
20 |
21 |
22 | def get_unique_filename(filename):
23 | ext = filename.rsplit(".", 1)[1].lower()
24 | unique_filename = uuid.uuid4().hex
25 | return f"{unique_filename}.{ext}"
26 |
27 |
28 | def upload_file_to_s3(file, acl="public-read"):
29 | try:
30 | s3.upload_fileobj(
31 | file,
32 | BUCKET_NAME,
33 | file.filename,
34 | ExtraArgs={
35 | "ACL": acl,
36 | "ContentType": file.content_type
37 | }
38 | )
39 | except Exception as e:
40 | # in case the our s3 upload fails
41 | return {"errors": str(e)}
42 |
43 | return {"url": f"{S3_LOCATION}{file.filename}"}
44 |
--------------------------------------------------------------------------------
/react-app/src/store/fastForwardDetails.js:
--------------------------------------------------------------------------------
1 | const GET_DETAILS = "fastForwardDetails/GET_DETAILS";
2 | const DELETE_DETAILS = "fastForwardDetails/DELETE_DETAILS"
3 | const EDIT_DETAILS = "fastForwardDetails/EDIT_DETAILS";
4 |
5 |
6 |
7 | const getFastForwardDetails = (fastForward) => ({
8 | type: GET_DETAILS,
9 | payload: fastForward,
10 | });
11 |
12 | export const deleteFastForwardDetails = () => ({
13 | type: DELETE_DETAILS
14 | });
15 |
16 | export const editFastForwardDetails = (fastForward) => ({
17 | type: EDIT_DETAILS,
18 | payload: fastForward
19 | });
20 |
21 |
22 | export const fetchFastForwardDetails = (id) => async (dispatch) => {
23 | const response = await fetch(`/api/fastForwards/${id}`);
24 | if (response.ok) {
25 | const fastForward = await response.json();
26 | dispatch(getFastForwardDetails(fastForward));
27 | return response;
28 | }
29 | };
30 |
31 | const initialState = {};
32 |
33 | export default function reducer(state = initialState, action) {
34 | let newState;
35 | switch (action.type) {
36 | case GET_DETAILS:
37 | newState = action.payload;
38 | return newState;
39 | case DELETE_DETAILS:
40 | newState = {}
41 | return newState
42 | default:
43 | return state;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/api/user_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify
2 | from flask_login import login_required
3 | from app.models import User
4 |
5 | user_routes = Blueprint('users', __name__)
6 |
7 |
8 | @user_routes.route('/')
9 | def users():
10 | """
11 | Query for all users and returns them in a list of user dictionaries
12 | """
13 | users = User.query.all()
14 | return {'users': [user.to_dict() for user in users]}
15 |
16 |
17 | @user_routes.route('/')
18 | def user(id):
19 | """
20 | Query for a user by id and returns that user in a dictionary
21 | """
22 | user = User.query.get(id)
23 | return user.to_dict()
24 |
25 |
26 | # ================================users followers and following routes ==================================
27 |
28 | # get all users I am following
29 | @user_routes.route("//following")
30 | def following(id):
31 | user = User.query.get(id)
32 | following = user.following.all()
33 | users = {}
34 | for i in range(len(following)):
35 | users[user.following[i].id]=user.following[i].to_dict()
36 | return users
37 |
38 | # get all users who follow me
39 | @user_routes.route("//followers")
40 | def followers(id):
41 | user = User.query.get(id)
42 | followers = user.followers.all()
43 | users = {}
44 | for i in range(len(followers)):
45 | users[users.following[i].id] = user.followers[i].to_dict()
46 | return users
47 |
--------------------------------------------------------------------------------
/app/models/fast_forward.py:
--------------------------------------------------------------------------------
1 | from .db import db, environment, SCHEMA, add_prefix_for_prod
2 | from werkzeug.security import generate_password_hash, check_password_hash
3 | from flask_login import UserMixin
4 |
5 |
6 | class FastForward(db.Model):
7 | __tablename__ = 'fastForwards'
8 | if environment == "production":
9 | __table_args__ = {'schema': SCHEMA}
10 | id = db.Column(db.Integer, primary_key=True)
11 | url = db.Column(db.String)
12 | caption = db.Column(db.String)
13 | user_id = db.Column(db.Integer, db.ForeignKey(add_prefix_for_prod("users.id")))
14 | user = db.relationship("User", lazy='joined', backref="fastForwards")
15 | comment = db.relationship("Comment", cascade="all,delete", backref="fastForwards")
16 | like_post = db.relationship("LikePost", cascade="all,delete", backref="fastForwards")
17 |
18 | # liked_fast_forward_user = db.relationship(
19 | # "User",
20 | # secondary=like_fast_forward,
21 | # lazy='dynamic',
22 | # back_populates = 'liked')
23 |
24 | def to_dict(self):
25 | return {
26 | 'id': self.id,
27 | 'url': self.url,
28 | 'caption': self.caption,
29 | 'user_id': self.user_id,
30 | 'User': self.user.to_dict(),
31 | 'Comments': [comment.to_dict() for comment in self.comments],
32 | 'LikePosts':[like_post.to_dict() for like_post in self.like_post]
33 | }
34 |
--------------------------------------------------------------------------------
/app/api/follower_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, request
2 | from flask_login import login_required
3 | from app.models import db, User
4 | import json
5 |
6 | followers_routes = Blueprint("followers", __name__)
7 |
8 | # I am able to follow other users
9 | @followers_routes.route("", methods=["POST"])
10 | @login_required
11 | def follow():
12 | # pass in follower and followed id's from frontend
13 | # use those parameters on lines 27 and 28 instead
14 | req_body = request.json
15 | user_followed = User.query.get(req_body["followed_id"])
16 | user_follower = User.query.get(req_body["follower_id"])
17 | user_follower.following.append(user_followed)
18 | print("another weird data thing", user_follower)
19 | db.session.commit()
20 | updated_following = user_follower.following.all()
21 | users = {}
22 | for i in range(len(updated_following)):
23 | users[updated_following[i].id]=updated_following[i].to_dict()
24 | return users
25 |
26 | # I am able to unfollow other users
27 | @followers_routes.route("", methods=["DELETE"])
28 | @login_required
29 | def unfollow():
30 | req_body = json.loads(request.data)
31 | user_follower = User.query.get(req_body['follower_id'])
32 | user_followed = User.query.get(req_body["followed_id"])
33 | user_follower.following.remove(user_followed)
34 | db.session.commit()
35 | updated_following = user_follower.following.all()
36 | print('updated list of who I follow', updated_following)
37 | users = {}
38 | for i in range(len(updated_following)):
39 | users[updated_following[i].id]=updated_following[i].to_dict()
40 | return users
41 |
--------------------------------------------------------------------------------
/react-app/src/components/FollowList.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { NavLink } from 'react-router-dom';
3 | import { useSelector, useDispatch } from "react-redux";
4 | import * as followActions from '../store/follower'
5 | import './User.css'
6 |
7 | function FollowingList() {
8 |
9 | const [follows, setFollows] = useState([]);
10 | const user = useSelector((state) => state.session.user);
11 | const [isLoaded, setIsLoaded] = useState(false);
12 | const dispatch = useDispatch();
13 |
14 |
15 | useEffect(() => {
16 | if (user) {
17 | dispatch(followActions.followingList(user.id))
18 | .then(() => setIsLoaded(true))
19 | }
20 | }, [dispatch, isLoaded]);
21 |
22 | useEffect(() => {
23 | async function fetchData() {
24 | const response = await fetch(`/api/users/${user?.id}/following`);
25 | const responseData = await response.json();
26 | setFollows(Object.values(responseData));
27 | console.log(follows)
28 | }
29 | fetchData();
30 | }, []);
31 |
32 | const userComponents = follows?.map((user) => {
33 | return (
34 |
35 |
36 |
37 | {user.username}
38 | {user?.first_name}
39 | {user?.last_name}
40 |
41 |
42 | );
43 | });
44 |
45 | return (
46 | <>
47 |
48 | >
49 | );
50 | }
51 |
52 | export default FollowingList;
53 |
--------------------------------------------------------------------------------
/app/forms/signup_form.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField
3 | from wtforms.validators import DataRequired, Email, ValidationError
4 | from app.models import User
5 |
6 |
7 | def user_exists(form, field):
8 | # Checking if user exists
9 | email = field.data
10 | user = User.query.filter(User.email == email).first()
11 | if user:
12 | raise ValidationError('Email address is already in use.')
13 |
14 |
15 | def username_exists(form, field):
16 | # Checking if username is already in use
17 | username = field.data
18 | user = User.query.filter(User.username == username).first()
19 | if user:
20 | raise ValidationError('Username is already in use.')
21 |
22 |
23 | def image_url_is_valid(form, field):
24 | image_url = field.data
25 | print(image_url)
26 | if not (image_url.endswith("jpg") or image_url.endswith("png") or image_url.endswith("jpeg") or image_url.endswith("gif")):
27 | raise ValidationError('Please input a valid URL')
28 |
29 |
30 | class SignUpForm(FlaskForm):
31 | first_name = StringField('firstName', validators=[DataRequired()])
32 | last_name = StringField('lastName', validators=[DataRequired()])
33 | bio = StringField('bio', validators=[DataRequired()])
34 | image_url = StringField('imageUrl', validators=[
35 | DataRequired(), image_url_is_valid])
36 | username = StringField('username', validators=[
37 | DataRequired(), username_exists])
38 | email = StringField('email', validators=[
39 | DataRequired(), Email(), user_exists])
40 | password = StringField('password', validators=[DataRequired()])
41 |
--------------------------------------------------------------------------------
/react-app/src/components/auth/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { Redirect } from 'react-router-dom';
4 | import { login } from '../../store/session';
5 |
6 | const LoginForm = () => {
7 | const [errors, setErrors] = useState([]);
8 | const [email, setEmail] = useState('');
9 | const [password, setPassword] = useState('');
10 | const user = useSelector(state => state.session.user);
11 | const dispatch = useDispatch();
12 |
13 | const onLogin = async (e) => {
14 | e.preventDefault();
15 | const data = await dispatch(login(email, password))
16 | if (data) {
17 | setErrors(data);
18 | }
19 | };
20 |
21 | const updateEmail = (e) => {
22 | setEmail(e.target.value);
23 | };
24 |
25 | const updatePassword = (e) => {
26 | setPassword(e.target.value);
27 | };
28 |
29 | if (user) {
30 | return ;
31 | }
32 |
33 | return (
34 |
62 | );
63 | };
64 |
65 | export default LoginForm;
66 |
--------------------------------------------------------------------------------
/app/api/comment_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, request
2 | from flask_login import login_required, current_user
3 | from app.models import Comment
4 | from app.models import db, User
5 | from app.forms import CommentForm
6 |
7 | comments_routes = Blueprint('comments', __name__)
8 |
9 | def validation_errors_to_error_messages(validation_errors):
10 | """
11 | Simple function that turns the WTForms validation errors into a simple list
12 | """
13 | errorMessages = []
14 | for field in validation_errors:
15 | for error in validation_errors[field]:
16 | errorMessages.append(f'{error}')
17 | return errorMessages
18 |
19 |
20 | @comments_routes.route('/', methods=['PUT'])
21 | @login_required
22 | def fix_comment(id):
23 | """
24 | Query for a single comment and edit that comment
25 | """
26 | comment = Comment.query.get(id)
27 | if current_user.id == comment.user_id:
28 | form = CommentForm()
29 | form['csrf_token'].data = request.cookies['csrf_token']
30 | print(form.data)
31 | if form.validate_on_submit():
32 | comment.body = form.data['body']
33 | db.session.add(comment)
34 | db.session.commit()
35 | return comment.to_dict()
36 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401
37 | return {'errors': ['Unauthorized']}
38 |
39 | @comments_routes.route('/', methods=['DELETE'])
40 | @login_required
41 | def remove_comment(id):
42 | """
43 | Query for all comments for a story and returns them in a list of dictionaries
44 | """
45 | comment = Comment.query.get(id)
46 | print(current_user.id == comment.user_id)
47 | if current_user.id == comment.user_id:
48 | db.session.delete(comment)
49 | db.session.commit()
50 | return {'message': 'Deleted'}
51 | return {'errors': ['Unauthorized']}
52 |
--------------------------------------------------------------------------------
/react-app/src/components/FollowButton.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useSelector, useDispatch } from "react-redux";
3 | import { NavLink, useHistory, useLocation } 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 * as followActions from '../store/follower'
9 | import './FastForwards.css'
10 |
11 |
12 |
13 | const FollowButton = ({fastForward}) => {
14 |
15 | let followings = useSelector((state) => Object.values(state.follower.following))
16 | const [following, setFollowing] = useState(followings.includes(fastForward.user_id))
17 | const dispatch = useDispatch();
18 | const user = useSelector((state) => state.session.user);
19 |
20 |
21 | useEffect(() => {
22 | if (user) {
23 | setFollowing(followings.includes(fastForward.user_id));
24 | }
25 | }, [dispatch, followings]);
26 |
27 | const handleFollow = (followerId, followedId) => {
28 | if (!following) {
29 | dispatch(followActions.follow(followerId, followedId))
30 | .then(() => setFollowing(true))
31 | console.log("this is whether or not the follow button worked", "followed")
32 | } else {
33 | dispatch(followActions.unfollow(followerId, followedId))
34 | .then(() => setFollowing(false))
35 | console.log("this is whether or not the follow button worked", "unfollowed")
36 | }
37 | }
38 |
39 | return (
40 | handleFollow(user.id, fastForward.User.id)}>{!following ? "Follow" : "Following"}
41 | )
42 | }
43 |
44 | export default FollowButton;
45 |
--------------------------------------------------------------------------------
/react-app/src/components/CaptionEditForm.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { useHistory, useLocation, useParams } from "react-router-dom";
4 | import * as fastForwardActions from "../store/fastForward";
5 |
6 | function CaptionEditForm({setCaptionBody, captionBody, setShowEdit, fastForwardId}) {
7 | console.log(fastForwardId, "this is the fastForwardId")
8 | const userId = useSelector((state) => state.session.user.id);
9 | const [validationErrors, setValidationErrors] = useState([]);
10 | const [hasSubmitted, setHasSubmitted] = useState(false);
11 | const dispatch = useDispatch()
12 |
13 | useEffect(() => {
14 | if (!captionBody) {
15 | setValidationErrors([]);
16 | return;
17 | }
18 | console.log("uE running");
19 | const errors = [];
20 | if (!captionBody.length) errors.push("Please enter your comment");
21 | }, [captionBody]);
22 |
23 | const onSubmit = async (e) => {
24 | // Prevent the default form behavior so the page doesn't reload.
25 | e.preventDefault();
26 |
27 | setHasSubmitted(true);
28 | if (validationErrors.length) return alert(`Cannot Submit`);
29 |
30 | // Create a new object for the caption form information.
31 | const captionForm = {caption: captionBody};
32 |
33 |
34 | await dispatch(fastForwardActions.fetchEditFastForward(fastForwardId, captionForm))
35 | await dispatch(fastForwardActions.fetchAllFastForwards())
36 | setShowEdit(false)
37 |
38 |
39 | // Reset the form state.
40 | setCaptionBody("");
41 | setValidationErrors([]);
42 | setHasSubmitted(false);
43 | };
44 |
45 | return (
46 |
64 | );
65 | }
66 |
67 | export default CaptionEditForm;
68 |
--------------------------------------------------------------------------------
/react-app/src/components/CommentEditForm.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { Dispatch } from "react";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { useHistory, useLocation, useParams } from "react-router-dom";
5 | import { editComment } from "../store/comment";
6 | import * as fastForwardActions from "../store/fastForward";
7 |
8 | function CommentEditForm({comment, setCommentBody, commentBody, setEditId, fastForwardId}) {
9 |
10 | const userId = useSelector((state) => state.session.user.id);
11 | const [validationErrors, setValidationErrors] = useState([]);
12 | const [hasSubmitted, setHasSubmitted] = useState(false);
13 | const dispatch = useDispatch()
14 | const history = useHistory()
15 | const { id } = useParams()
16 |
17 | useEffect(() => {
18 | if (!commentBody) {
19 | setValidationErrors([]);
20 | return;
21 | }
22 | console.log("uE running");
23 | const errors = [];
24 | if (!commentBody.length) errors.push("Please enter your comment");
25 | }, [commentBody]);
26 |
27 | const onSubmit = async (e) => {
28 | // Prevent the default form behavior so the page doesn't reload.
29 | e.preventDefault();
30 |
31 | setHasSubmitted(true);
32 | if (validationErrors.length) return alert(`Cannot Submit`);
33 |
34 | // Create a new object for the song form information.
35 | const commentForm = {body: commentBody};
36 |
37 |
38 | await dispatch(editComment(comment.id, commentForm, fastForwardId))
39 | await dispatch(fastForwardActions.fetchAllFastForwards())
40 | setEditId(-1)
41 |
42 |
43 | // Reset the form state.
44 | setCommentBody("");
45 | setValidationErrors([]);
46 | setHasSubmitted(false);
47 | };
48 |
49 | return (
50 |
51 |
52 | {validationErrors.map((error, idx) => (
53 | {error}
54 | ))}
55 |
56 |
57 | setCommentBody(e.target.value)}
63 | required
64 | />
65 |
66 | Respond
67 |
68 | );
69 | }
70 |
71 | export default CommentEditForm;
72 |
--------------------------------------------------------------------------------
/react-app/src/components/CommentForm.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { Dispatch } from "react";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { NavLink, useHistory, useLocation } from "react-router-dom";
5 | import { createComment } from "../store/comment";
6 | import * as fastForwardActions from "../store/fastForward";
7 | import LoginFormModal from "./LoginFormModal";
8 | import SignupFormModal from "./SingUpFormModal";
9 | import '../components/LoginFormModal/LoginForm.css'
10 |
11 | function CommentForm(fastForwardId) {
12 | const user = useSelector((state) => state.session.user);
13 | const fastId = (Object.values(fastForwardId)[0])
14 | const [body, setBody] = useState("");
15 | const [validationErrors, setValidationErrors] = useState([]);
16 | const [hasSubmitted, setHasSubmitted] = useState(false);
17 | const dispatch = useDispatch()
18 | const history = useHistory()
19 |
20 | useEffect(() => {
21 | if (!body) {
22 | setValidationErrors([]);
23 | return;
24 | }
25 | const errors = [];
26 | if (!body.length) errors.push("Please enter your comment");
27 | }, [body]);
28 |
29 | const onSubmit = async (e) => {
30 | // Prevent the default form behavior so the page doesn't reload.
31 | e.preventDefault();
32 | setHasSubmitted(true);
33 |
34 | // Create a new object for the song form information.
35 | const commentForm = { body };
36 |
37 | await dispatch(createComment(fastId, commentForm))
38 | await dispatch(fastForwardActions.fetchAllFastForwards())
39 |
40 | // Reset the form state.
41 | setBody("");
42 | setValidationErrors([]);
43 | setHasSubmitted(false);
44 | };
45 |
46 | return (
47 |
48 |
49 | {validationErrors.map((error, idx) => (
50 | {error}
51 | ))}
52 |
53 | {user &&
54 | setBody(e.target.value)}
58 | required={true}
59 | />
60 | }
61 | {user && 300 ? 'comment-button' : 'comment-button-active'} disabled={!body || body.length > 300} type="submit">Respond }
62 | {!user && Please log in to comment
}
63 |
64 | );
65 | }
66 |
67 | export default CommentForm;
68 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Fast-Forward
2 |
3 | Fast-Forward is a social media platform, inspired by TikTok, where a user can upload videos to their personal profiles or interact with content from other creators! Create your own account or log in with the "Demo User" option.
4 |
5 | Live Link: https://fastforward-en-bfes.onrender.com
6 |
7 | ## Wiki Links
8 |
9 | - [Features](https://github.com/Jessie-Baron/Fast-Forward/wiki/Features-List)
10 | - [Wire Frames](https://github.com/Jessie-Baron/Fast-Forward/wiki/Wireframes)
11 | - [User Stories](https://github.com/Jessie-Baron/Fast-Forward/wiki/User-Stories)
12 | - [Schema](https://github.com/Jessie-Baron/Fast-Forward/wiki/DB-Schema)
13 |
14 | ## Technologies Used
15 |
16 | 
17 | 
18 | 
19 | 
20 | 
21 | 
22 | 
23 | 
24 | 
25 | 
26 |
27 | ## Splash Page
28 | 
29 |
30 | ## Log-in
31 | 
32 |
33 | ## Sign-up
34 | 
35 |
36 | ## Profile-Page
37 | 
38 |
39 | ## Future Features
40 |
41 | - Verfied Status
42 | - Direct Messaging
43 | - Video Tags and Categories
44 | - Live Feature
45 |
--------------------------------------------------------------------------------
/app/api/auth_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify, session, request
2 | from app.models import User, db
3 | from app.forms import LoginForm
4 | from app.forms import SignUpForm
5 | from flask_login import current_user, login_user, logout_user, login_required
6 |
7 | auth_routes = Blueprint('auth', __name__)
8 |
9 |
10 | def validation_errors_to_error_messages(validation_errors):
11 | """
12 | Simple function that turns the WTForms validation errors into a simple list
13 | """
14 | errorMessages = []
15 | for field in validation_errors:
16 | for error in validation_errors[field]:
17 | errorMessages.append(f'{error}')
18 | return errorMessages
19 |
20 |
21 | @auth_routes.route('/')
22 | def authenticate():
23 | """
24 | Authenticates a user.
25 | """
26 | if current_user.is_authenticated:
27 | return current_user.to_dict()
28 | return {'errors': ['Unauthorized']}
29 |
30 |
31 | @auth_routes.route('/login', methods=['POST'])
32 | def login():
33 | """
34 | Logs a user in
35 | """
36 | form = LoginForm()
37 | # Get the csrf_token from the request cookie and put it into the
38 | # form manually to validate_on_submit can be used
39 | form['csrf_token'].data = request.cookies['csrf_token']
40 | if form.validate_on_submit():
41 | # Add the user to the session, we are logged in!
42 | user = User.query.filter(User.email == form.data['email']).first()
43 | login_user(user)
44 | return user.to_dict()
45 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401
46 |
47 |
48 | @auth_routes.route('/logout')
49 | def logout():
50 | """
51 | Logs a user out
52 | """
53 | logout_user()
54 | return {'message': 'User logged out'}
55 |
56 |
57 | @auth_routes.route('/signup', methods=['POST'])
58 | def sign_up():
59 | """
60 | Creates a new user and logs them in
61 | """
62 | form = SignUpForm()
63 | form['csrf_token'].data = request.cookies['csrf_token']
64 | if form.validate_on_submit():
65 | user = User(
66 | first_name=form.data['first_name'],
67 | last_name=form.data['last_name'],
68 | bio=form.data['bio'],
69 | image_url=form.data['image_url'],
70 | username=form.data['username'],
71 | email=form.data['email'],
72 | password=form.data['password']
73 | )
74 | db.session.add(user)
75 | db.session.commit()
76 | login_user(user)
77 | return user.to_dict()
78 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401
79 |
80 |
81 | @auth_routes.route('/unauthorized')
82 | def unauthorized():
83 | """
84 | Returns unauthorized JSON when flask-login authentication fails
85 | """
86 | return {'errors': ['Unauthorized']}, 401
87 |
--------------------------------------------------------------------------------
/app/models/user.py:
--------------------------------------------------------------------------------
1 | from .db import db, environment, SCHEMA, add_prefix_for_prod
2 | from werkzeug.security import generate_password_hash, check_password_hash
3 | from flask_login import UserMixin
4 | from .follow import follows
5 |
6 | # like_fast_forward = db.Table(
7 | # "like_fast_forward",
8 | # db.Column("user_id", db.Integer, db.ForeignKey(add_prefix_for_prod("users.id")), primary_key=True),
9 | # db.Column("fast_forward_id", db.Integer, db.ForeignKey(add_prefix_for_prod("fastForwards.id")), primary_key=True)
10 | # )
11 |
12 | # like_comment = db.Table(
13 | # "like_comment",
14 | # db.Column("user_id", db.Integer, db.ForeignKey(add_prefix_for_prod("users.id")), primary_key=True),
15 | # db.Column("comment_id", db.Integer, db.ForeignKey(add_prefix_for_prod("comments.id")), primary_key=True)
16 | # )
17 |
18 | class User(db.Model, UserMixin):
19 | __tablename__ = 'users'
20 |
21 | if environment == "production":
22 | __table_args__ = {'schema': SCHEMA}
23 |
24 | id = db.Column(db.Integer, primary_key=True)
25 | first_name = db.Column(db.String(40), nullable=False, unique=True)
26 | last_name = db.Column(db.String(40), nullable=False, unique=True)
27 | bio = db.Column(db.String(255), nullable=True, unique=True)
28 | image_url = db.Column(db.String(255), nullable=False, unique=True)
29 | username = db.Column(db.String(40), nullable=False, unique=True)
30 | email = db.Column(db.String(255), nullable=False, unique=True)
31 | hashed_password = db.Column(db.String(255), nullable=False)
32 |
33 | followers = db.relationship(
34 | "User",
35 | secondary=follows,
36 | primaryjoin=(follows.c.followed_id == id),
37 | secondaryjoin=(follows.c.follower_id == id),
38 | backref=db.backref("following", lazy="dynamic"),
39 | lazy="dynamic",
40 | )
41 |
42 | # liked_comment = db.relationship(
43 | # "Comment",
44 | # secondary=like_comment,
45 | # primaryjoin=(like_fast_forward.c.user_id == id),
46 | # secondaryjoin=(like_fast_forward.c.fast_forward_id == id),
47 | # lazy='dynamic',
48 | # back_populates = "liked_comment_user"
49 | # )
50 |
51 | @property
52 | def password(self):
53 | return self.hashed_password
54 |
55 | @password.setter
56 | def password(self, password):
57 | self.hashed_password = generate_password_hash(password)
58 |
59 | def check_password(self, password):
60 | return check_password_hash(self.password, password)
61 |
62 | def to_dict(self):
63 | return {
64 | 'id': self.id,
65 | 'first_name': self.first_name,
66 | 'last_name': self.last_name,
67 | 'bio': self.bio,
68 | 'image_url': self.image_url,
69 | 'username': self.username,
70 | 'email': self.email,
71 | }
72 |
--------------------------------------------------------------------------------
/react-app/src/components/User.css:
--------------------------------------------------------------------------------
1 | #root > div > div.sideBar-items > div.suggested-feed > ul > div > img {
2 | width: 30px;
3 | border-radius: 33px;
4 | height: 30px;
5 | }
6 |
7 | #root > div > div.sideBar-items > div.suggested-feed > ul {
8 | padding-left: 0;
9 | margin-left: 0;
10 | left: 0;
11 | }
12 |
13 | .user-suggested {
14 | display: flex;
15 | gap: 10px;
16 | color: rgb(156, 155, 155);
17 | font-size: 14px;
18 | margin-bottom: 10px;
19 | padding-top: 5px;
20 | padding-bottom: 5px;
21 | padding-left: 5px;
22 | border-radius: 7px;
23 | }
24 |
25 | .user-suggested:hover {
26 | background-color: #2a2a2a;
27 | }
28 |
29 | #root > div > div.sideBar-items > div.suggested-feed > ul > div > div > a {
30 | display: flex;
31 | gap: 10px;
32 | color: rgba(255, 255, 255, .75);
33 | font-weight: 600;
34 | font-size: 18px;
35 | text-decoration: none;
36 | }
37 |
38 | .suggested-text {
39 | display: flex;
40 | flex-direction: column;
41 | }
42 | .user-page-headline {
43 | margin-left: 35%;
44 | display: flex;
45 | gap: 20px;
46 | align-items: center;
47 | }
48 |
49 | .user-profile-image {
50 | width: 100px;
51 | height: 100px;
52 | margin-top: 25%;
53 | border-radius: 53px;
54 | }
55 |
56 | .profile-title {
57 | font-weight: 700;
58 | font-size: 32px;
59 | line-height: 38px;
60 | margin-bottom: 0;
61 | color: rgba(255, 255, 255, 0.9);
62 | }
63 |
64 | .profile-subtitle {
65 | color: rgba(255, 255, 255, 0.9);
66 | font-weight: 600;
67 | font-size: 18px;
68 | line-height: 25px;
69 | }
70 |
71 | .user-followers {
72 | margin-left: 36%;
73 | margin-top: 15px;
74 | display: flex;
75 | gap: 50px;
76 | font-weight: 400;
77 | font-size: 16px;
78 | line-height: 22px;
79 | color: rgba(255, 255, 255, 0.75);
80 | }
81 |
82 | .user-followers-count {
83 | color: rgba(255, 255, 255, 0.9);
84 | font-weight: 600;
85 | font-size: 18px;
86 | line-height: 25px;
87 | }
88 |
89 | .video-user-wrapper {
90 | display: grid;
91 | width: 700px;
92 | grid-template-columns: repeat(4, 1fr);
93 | gap: 10px;
94 | margin-left: 33%;
95 | margin-top: 25px;
96 | }
97 |
98 | .user-bio {
99 | margin-left: 36%;
100 | margin-top: 1%;
101 | font-weight: 400;
102 | font-size: 16px;
103 | line-height: 22px;
104 | color: rgba(255, 255, 255, 0.75);
105 | }
106 |
107 | .caption-user {
108 | margin-left: 10%;
109 | margin-top: 3%;
110 | color: rgba(255, 255, 255, 0.75);
111 | max-width: 200px;
112 | max-height: 30px;
113 | overflow: hidden;
114 | white-space: nowrap;
115 | text-overflow: ellipsis;
116 | text-decoration: none;
117 | display: block;
118 | }
119 |
--------------------------------------------------------------------------------
/react-app/src/store/session.js:
--------------------------------------------------------------------------------
1 | // constants
2 | const SET_USER = 'session/SET_USER';
3 | const REMOVE_USER = 'session/REMOVE_USER';
4 |
5 | const setUser = (user) => ({
6 | type: SET_USER,
7 | payload: user
8 | });
9 |
10 | const removeUser = () => ({
11 | type: REMOVE_USER,
12 | })
13 |
14 | const initialState = { user: null };
15 |
16 | export const authenticate = () => async (dispatch) => {
17 | const response = await fetch('/api/auth/', {
18 | headers: {
19 | 'Content-Type': 'application/json'
20 | }
21 | });
22 | if (response.ok) {
23 | const data = await response.json();
24 | if (data.errors) {
25 | return;
26 | }
27 |
28 | dispatch(setUser(data));
29 | }
30 | }
31 |
32 | export const login = (email, password) => async (dispatch) => {
33 | const response = await fetch('/api/auth/login', {
34 | method: 'POST',
35 | headers: {
36 | 'Content-Type': 'application/json'
37 | },
38 | body: JSON.stringify({
39 | email,
40 | password
41 | })
42 | });
43 |
44 |
45 | if (response.ok) {
46 | const data = await response.json();
47 | dispatch(setUser(data))
48 | return null;
49 | } else if (response.status < 500) {
50 | const data = await response.json();
51 | if (data.errors) {
52 | return data.errors;
53 | }
54 | } else {
55 | return ['An error occurred. Please try again.']
56 | }
57 |
58 | }
59 |
60 | export const logout = () => async (dispatch) => {
61 | const response = await fetch('/api/auth/logout', {
62 | headers: {
63 | 'Content-Type': 'application/json',
64 | }
65 | });
66 |
67 | if (response.ok) {
68 | dispatch(removeUser());
69 | }
70 | };
71 |
72 |
73 | export const signUp = (payload) => async (dispatch) => {
74 | const {firstName, lastName, bio, imageUrl, username, email, password} = payload
75 | const response = await fetch('/api/auth/signup',
76 | {
77 | method: 'POST',
78 | headers: {
79 | 'Content-Type': 'application/json',
80 | },
81 | body: JSON.stringify({
82 | first_name: firstName,
83 | last_name: lastName,
84 | bio,
85 | image_url: imageUrl,
86 | username,
87 | email,
88 | password,
89 | }),
90 | });
91 |
92 | if (response.ok) {
93 | const data = await response.json();
94 | console.log(data)
95 | dispatch(setUser(data))
96 | return null;
97 | } else if (response.status < 500) {
98 | const data = await response.json();
99 | if (data.errors) {
100 | return data.errors;
101 | }
102 | } else {
103 | return ['An error occurred. Please try again.']
104 | }
105 | }
106 |
107 | export default function reducer(state = initialState, action) {
108 | switch (action.type) {
109 | case SET_USER:
110 | return { user: action.payload }
111 | case REMOVE_USER:
112 | return { user: null }
113 | default:
114 | return state;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/app/seeds/users.py:
--------------------------------------------------------------------------------
1 | from app.models import db, User, environment, SCHEMA
2 |
3 |
4 | # Adds a demo user, you can add other users here if you want
5 | def seed_users():
6 | demo = User(
7 | first_name='Demo', last_name='User', username='Demo', bio='This is a test User', image_url='https://images.pexels.com/photos/771742/pexels-photo-771742.jpeg', email='demo@aa.io', password='password')
8 | marnie = User(
9 | first_name='Marnie', last_name='Rodriguez', username='marnie', bio='virgo <3', image_url='https://i.etsystatic.com/36532523/r/il/97ae46/4078306713/il_340x270.4078306713_n74s.jpg', email='marnie@aa.io', password='password')
10 | bobbie = User(
11 | first_name='Bobbie', last_name='Smiths', image_url='https://static-cse.canva.com/blob/951359/1600w-YTfEMXMuMCs.jpg', bio='Creator from South Georgia! Roll Tide!', username='bobbie', email='bobbie@aa.io', password='password')
12 | Bella = User(
13 | first_name='Bella', last_name='Poarch', image_url='https://editors.dexerto.com/wp-content/uploads/2021/01/BellaPoarchTop5.jpg', bio='No bio yet.', username='bellapoarch', email='bella@aa.io', password='password')
14 | Will = User(
15 | first_name='Will', last_name='Smith', image_url='https://media.wired.com/photos/5d960eba01e4a4000826137c/master/pass/Culture_Monitor_WillSmith-465783654.jpg', username='willsmith', email='will@aa.io', password='password')
16 | Zach = User(
17 | first_name='Zach', last_name='King', image_url='https://i.guim.co.uk/img/media/4ea36f9ca7d8900fde5957a15d64ecef7180364b/0_1343_4480_2688/master/4480.jpg?width=1200&height=1200&quality=85&auto=format&fit=crop&s=f90a12b25e5d0f30ccbbce7431422d57', username='zachking', email='zach@aa.io', password='password')
18 | Khaby = User(
19 | first_name='Khabane', last_name='Lame', image_url='https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQDOE1dBmDsBMGigsXAgt8Udrc4e7oKJoik6A&usqp=CAU', username='khaby.lame', email='khaby@aa.io', password='password')
20 | Rock = User(
21 | first_name='The', last_name='Rock', image_url='https://parade.com/.image/ar_1:1%2Cc_fill%2Ccs_srgb%2Cfl_progressive%2Cq_auto:good%2Cw_1200/MTk0NDIzNDY0NzQyODg4OTY1/black-adam-madrid-premiere.jpg', username='therock', email='rock@aa.io', password='password')
22 |
23 |
24 |
25 | db.session.add(demo)
26 | db.session.add(marnie)
27 | db.session.add(bobbie)
28 | db.session.add(Bella)
29 | db.session.add(Will)
30 | db.session.add(Zach)
31 | db.session.add(Khaby)
32 | db.session.add(Rock)
33 | db.session.commit()
34 |
35 |
36 | # Uses a raw SQL query to TRUNCATE or DELETE the users table. SQLAlchemy doesn't
37 | # have a built in function to do this. With postgres in production TRUNCATE
38 | # removes all the data from the table, and RESET IDENTITY resets the auto
39 | # incrementing primary key, CASCADE deletes any dependent entities. With
40 | # sqlite3 in development you need to instead use DELETE to remove all data and
41 | # it will reset the primary keys for you as well.
42 | def undo_users():
43 | if environment == "production":
44 | db.session.execute(f"TRUNCATE table {SCHEMA}.users RESTART IDENTITY CASCADE;")
45 | else:
46 | db.session.execute("DELETE FROM users")
47 |
48 | db.session.commit()
49 |
--------------------------------------------------------------------------------
/migrations/versions/20230203_213751_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 349289ca782f
4 | Revises:
5 | Create Date: 2023-02-03 21:37:51.037885
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '349289ca782f'
14 | down_revision = None
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_table('users',
22 | sa.Column('id', sa.Integer(), nullable=False),
23 | sa.Column('first_name', sa.String(length=40), nullable=False),
24 | sa.Column('last_name', sa.String(length=40), nullable=False),
25 | sa.Column('bio', sa.String(length=255), nullable=True),
26 | sa.Column('image_url', sa.String(length=255), nullable=False),
27 | sa.Column('username', sa.String(length=40), nullable=False),
28 | sa.Column('email', sa.String(length=255), nullable=False),
29 | sa.Column('hashed_password', sa.String(length=255), nullable=False),
30 | sa.PrimaryKeyConstraint('id'),
31 | sa.UniqueConstraint('bio'),
32 | sa.UniqueConstraint('email'),
33 | sa.UniqueConstraint('first_name'),
34 | sa.UniqueConstraint('image_url'),
35 | sa.UniqueConstraint('last_name'),
36 | sa.UniqueConstraint('username')
37 | )
38 | op.create_table('fastForwards',
39 | sa.Column('id', sa.Integer(), nullable=False),
40 | sa.Column('url', sa.String(), nullable=True),
41 | sa.Column('caption', sa.String(), nullable=True),
42 | sa.Column('user_id', sa.Integer(), nullable=True),
43 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
44 | sa.PrimaryKeyConstraint('id')
45 | )
46 | op.create_table('follows',
47 | sa.Column('follower_id', sa.Integer(), nullable=False),
48 | sa.Column('followed_id', sa.Integer(), nullable=False),
49 | sa.ForeignKeyConstraint(['followed_id'], ['users.id'], ),
50 | sa.ForeignKeyConstraint(['follower_id'], ['users.id'], ),
51 | sa.PrimaryKeyConstraint('follower_id', 'followed_id')
52 | )
53 | op.create_table('comments',
54 | sa.Column('id', sa.Integer(), nullable=False),
55 | sa.Column('body', sa.String(), nullable=True),
56 | sa.Column('user_id', sa.Integer(), nullable=True),
57 | sa.Column('fast_forward_id', sa.Integer(), nullable=True),
58 | sa.ForeignKeyConstraint(['fast_forward_id'], ['fastForwards.id'], ),
59 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
60 | sa.PrimaryKeyConstraint('id')
61 | )
62 | op.create_table('likePosts',
63 | sa.Column('id', sa.Integer(), nullable=False),
64 | sa.Column('user_id', sa.Integer(), nullable=True),
65 | sa.Column('fast_forward_id', sa.Integer(), nullable=True),
66 | sa.ForeignKeyConstraint(['fast_forward_id'], ['fastForwards.id'], ),
67 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
68 | sa.PrimaryKeyConstraint('id')
69 | )
70 | # ### end Alembic commands ###
71 |
72 |
73 | def downgrade():
74 | # ### commands auto generated by Alembic - please adjust! ###
75 | op.drop_table('likePosts')
76 | op.drop_table('comments')
77 | op.drop_table('follows')
78 | op.drop_table('fastForwards')
79 | op.drop_table('users')
80 | # ### end Alembic commands ###
--------------------------------------------------------------------------------
/react-app/src/components/SingUpFormModal/SignupForm.css:
--------------------------------------------------------------------------------
1 | .signup-form {
2 | width: 483px;
3 | height: 650px;
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 | overflow-y: scroll;
13 | }
14 |
15 | .signup-scroll-content {
16 | height: 650px;
17 | overflow-y: scroll;
18 | padding-left: 15px;
19 | padding-right: 15px;
20 | }
21 |
22 | .signup-text-link {
23 | font-weight: 600;
24 | font-size: 15px;
25 | line-height: 18px;
26 | color: rgb(255, 59, 92);
27 | cursor: pointer;
28 | }
29 |
30 |
31 | .signup-form::-webkit-scrollbar {
32 | width: 8px;
33 | }
34 |
35 | .signup-form::-webkit-scrollbar-thumb {
36 | width: 6px;
37 | height: 100%;
38 | border-radius: 3px;
39 | background: rgba(255, 255, 255, .08);
40 | }
41 |
42 | .signup-scroll-content::-webkit-scrollbar-thumb {
43 | width: 6px;
44 | height: 100%;
45 | border-radius: 3px;
46 | background: rgba(255, 255, 255, .08);
47 | }
48 |
49 | .signup-scroll-content::-webkit-scrollbar {
50 | width: 8px;
51 | }
52 |
53 | .signup-input {
54 | width: 285px;
55 | color: rgba(255, 255, 255, 0.9);
56 | font-weight: 600;
57 | border: 1px solid rgba(255, 255, 255, 0);
58 | font-size: 15px;
59 | padding: 0px 12px;
60 | height: 44px;
61 | background: rgb(46, 46, 46);
62 | margin-bottom: 15px;
63 | display: flex;
64 | outline: none;
65 | }
66 |
67 | #signup-textarea {
68 | height: 100px;
69 | color: rgb(46, 46, 46);
70 | }
71 |
72 | .signup-button {
73 | border-radius: 4px;
74 | border: none;
75 | color: rgb(255, 255, 255);
76 | background-color: rgb(255, 59, 92);
77 | min-width: 320px;
78 | min-height: 46px;
79 | font-size: 16px;
80 | line-height: 22px;
81 | margin-top: 20px;
82 | }
83 |
84 | .signup-button:hover {
85 | background-color: rgb(250, 41, 76);
86 | cursor: pointer;
87 | }
88 |
89 | #lastname-input {
90 | margin-top: 0px;
91 | }
92 |
93 | #modal-content > form > div:nth-child(1) {
94 | color: rgb(201, 0, 0);
95 | font-size: 14px;
96 | }
97 |
98 | #modal-content > form > div.signup-scroll-content > div:nth-child(8) {
99 | padding-bottom: 105px;
100 | }
101 |
102 | #modal-content > form > div.signup-scroll-content > div:nth-child(9) {
103 | height: 75px;
104 | }
105 |
106 | .submit-button-signup {
107 | border-radius: 4px;
108 | border: none;
109 | color: rgb(255, 255, 255);
110 | background-color: rgb(255, 59, 92);
111 | min-width: 340px;
112 | min-height: 46px;
113 | font-size: 16px;
114 | line-height: 22px;
115 | cursor: pointer;
116 | }
117 |
118 | .submit-buttons-signup {
119 | margin-bottom: 25px;
120 | margin-top: 10px;
121 | }
122 |
123 | .upload-signup-header {
124 | color: white;
125 | }
126 |
--------------------------------------------------------------------------------
/react-app/src/store/fastForward.js:
--------------------------------------------------------------------------------
1 | const POST_FAST_FORWARD = "fastForwards/POST_FAST_FORWARD";
2 | const EDIT_FAST_FORWARD = "fastForwards/EDIT_FAST_FORWARD";
3 | const GET_FAST_FORWARD = "fastForwards/GET_FAST_FORWARD";
4 | const DELETE_FAST_FORWARD = "fastForwards/DELETE_FAST_FORWARD"
5 |
6 | const postFastForward = (fastForward) => ({
7 | type: POST_FAST_FORWARD,
8 | payload: fastForward,
9 | });
10 |
11 | const editFastForward = (fastForward) => ({
12 | type: EDIT_FAST_FORWARD,
13 | payload: fastForward
14 | });
15 |
16 | const getFastForwards = (fastForwards) => ({
17 | type: GET_FAST_FORWARD,
18 | payload: fastForwards,
19 | });
20 |
21 | const deleteFastForward = (id) => ({
22 | type: DELETE_FAST_FORWARD,
23 | payload: id
24 | });
25 |
26 | export const fetchAllFastForwards = () => async (dispatch) => {
27 | const response = await fetch("/api/fastForwards");
28 | if (response.ok) {
29 | const fastForwards = await response.json();
30 | dispatch(getFastForwards(fastForwards));
31 | return fastForwards;
32 | }
33 | };
34 |
35 | export const fetchPostFastForward = (fastForward) => async (dispatch) => {
36 | const { caption, url } = fastForward;
37 | const response = await fetch("/api/fastForwards", {
38 | method: "POST",
39 | headers: {
40 | "Content-Type": "application/json",
41 | },
42 | body: JSON.stringify(fastForward),
43 | });
44 | if (response.ok) {
45 | const fastForward = await response.json();
46 | dispatch(postFastForward(fastForward));
47 | return response;
48 | }
49 | };
50 |
51 | export const fetchEditFastForward = (fastForwardId, payload) => async (dispatch) => {
52 | console.log(fastForwardId)
53 | // const formData = new FormData();
54 | // formData.append("title", newTitle);
55 | // formData.append("body", newBody);
56 | const res = await fetch(`/api/fastForwards/${fastForwardId}`, {
57 | method: "PUT",
58 | headers: {
59 | "Content-Type": "application/json"
60 | },
61 | body: JSON.stringify(payload)
62 | });
63 | if (res.ok) {
64 | const data = await res.json()
65 | dispatch(editFastForward(data))
66 | return data
67 | }
68 | }
69 |
70 | export const fetchDeleteFastForward = (id) => async (dispatch) => {
71 | const response = await fetch(`/api/fastForwards/${id}`, {
72 | method: "DELETE",
73 | });
74 | console.log(response)
75 | if (response.ok) {
76 | dispatch(deleteFastForward(id))
77 | return response
78 | }
79 | }
80 |
81 | const initialState = {};
82 |
83 | export default function reducer(state = initialState, action) {
84 | let newState;
85 | switch (action.type) {
86 | case GET_FAST_FORWARD:
87 | newState = action.payload;
88 | return newState;
89 | case POST_FAST_FORWARD:
90 | newState = Object.assign({}, state);
91 | newState[action.payload.id] = action.payload;
92 | return newState;
93 | case EDIT_FAST_FORWARD:
94 | newState = Object.assign({}, state);
95 | newState[action.payload.id] = action.payload;
96 | return newState;
97 | case DELETE_FAST_FORWARD:
98 | newState = Object.assign({}, state);
99 | delete newState[action.payload.id];
100 | return newState;
101 | default:
102 | return state;
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/react-app/src/store/follower.js:
--------------------------------------------------------------------------------
1 | const LOAD_FOLLOWING = 'followers/LOAD_FOLLOWING'
2 | const LOAD_FOLLOWERS = 'followers/LOAD_FOLLOWERS'
3 | const ADD_FOLLOWING = 'followers/ADD_FOLLOWING'
4 | const REMOVE_FOLLOWING = 'followers/REMOVE_FOLLOWING'
5 |
6 | const loadFollowing = (listOfFollowing) => ({
7 | type: LOAD_FOLLOWING,
8 | payload: listOfFollowing
9 | })
10 |
11 | const addFollowing = (userData) => ({
12 | type: ADD_FOLLOWING,
13 | payload: userData
14 | })
15 |
16 | const removeFollowing = (id) => ({
17 | type: REMOVE_FOLLOWING,
18 | payload: id
19 | })
20 |
21 | export const followingList = (id) => async (dispatch) => {
22 | const res = await fetch(`/api/users/${id}/following`)
23 | if (res.ok) {
24 | const list = await res.json()
25 | console.log('list', list)
26 | dispatch(loadFollowing(list))
27 | return list
28 | }
29 | }
30 |
31 | export const follow = (follower_id, followed_id) => async (dispatch) => {
32 | const res = await fetch('/api/followers', {
33 | method: 'POST',
34 | headers: {
35 | 'Content-Type': 'application/json'
36 | },
37 | body: JSON.stringify({
38 | follower_id,
39 | followed_id
40 | })
41 | })
42 |
43 | if (res.ok) {
44 | const followData = await res.json()
45 | console.log('followData', followData[followed_id])
46 | dispatch(addFollowing(followData[followed_id]))
47 | return followData
48 | } else if (res.status < 500) {
49 | const data = await res.json()
50 | if (data.errors) {
51 | return data.errors
52 | }
53 | } else {
54 | return ['An error occured. Please try again.']
55 | }
56 | }
57 |
58 | export const unfollow = (follower_id, followed_id) => async (dispatch) => {
59 | const res = await fetch('/api/followers', {
60 | method: 'DELETE',
61 | body: JSON.stringify({
62 | follower_id,
63 | followed_id
64 | })
65 | })
66 |
67 | if (res.ok) {
68 | const followData = await res.json()
69 | dispatch(removeFollowing(followData))
70 | return followData
71 | } else if (res.status < 500) {
72 | const data = await res.json()
73 | if (data.errors) {
74 | return data.errors
75 | }
76 | } else {
77 | return ['An error occured. Please try again.']
78 | }
79 | }
80 |
81 | const initialState = {
82 | followers: {},
83 | following: {}
84 | }
85 |
86 | export default function followReducer(state = initialState, action) {
87 | let newState;
88 | switch (action.type) {
89 | case LOAD_FOLLOWING:
90 | newState = Object.assign({}, state);
91 | newState.following = { ...action.payload };
92 | return newState;
93 | case ADD_FOLLOWING:
94 | newState = Object.assign({}, state);
95 | newState.following[action.payload.id] = { ...action.payload }
96 | return newState
97 | case REMOVE_FOLLOWING:
98 | return {
99 | ...state,
100 | following: { ...action.payload }
101 | }
102 | default:
103 | return state;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/react-app/src/components/FollowFeed.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { NavLink } from 'react-router-dom';
3 | import { useSelector, useDispatch } from "react-redux";
4 | import './User.css'
5 |
6 | function FollowFeed() {
7 |
8 | const [follows, setFollows] = useState([]);
9 | const user = useSelector((state) => state.session.user);
10 | const fastForwardsObj = useSelector(state => state.fastForward)
11 | const fastForwards = Object.values(fastForwardsObj)
12 |
13 | useEffect(() => {
14 | async function fetchData() {
15 | const response = await fetch(`/api/users/${user?.id}/following`);
16 | const responseData = await response.json();
17 | setFollows(Object.values(responseData));
18 | }
19 | fetchData();
20 | }, []);
21 |
22 | const filtered = []
23 |
24 | for (let i = 0; i < fastForwards.length; i++) {
25 | let fastForwardId = fastForwards[i].user_id
26 | for (let j = 0; j < follows.length; j++)
27 | if (fastForwardId === follows[j].id) filtered.push(fastForwards[i])
28 | }
29 |
30 | const userComponents = filtered?.map((fastForward) => {
31 | return (
32 |
33 |
34 |
35 |
40 |
41 |
42 |
43 |
{fastForward.User.username}
44 |
{fastForward.User.first_name} {fastForward.User.last_name}
45 |
46 |
47 | {fastForward.caption}
48 |
49 |
50 |
51 |
52 |
event.target.play()} onMouseOut={event => event.target.pause()} width="350" height="600" border-radius='8'>
53 |
54 |
55 | {user &&
56 |
57 |
58 |
59 |
60 |
{fastForward?.Comments?.length}
61 |
}
62 |
63 |
64 |
65 |
66 | );
67 | });
68 |
69 | return (
70 | <>
71 |
72 | >
73 | );
74 | }
75 |
76 | export default FollowFeed;
77 |
--------------------------------------------------------------------------------
/react-app/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { BrowserRouter, Route, Switch } from 'react-router-dom';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import LoginForm from './components/auth/LoginForm';
5 | import SignUpForm from './components/auth/SignUpForm';
6 | import NavBar from './components/NavBar';
7 | import ProtectedRoute from './components/auth/ProtectedRoute';
8 | import UsersList from './components/UsersList';
9 | import User from './components/User';
10 | import { authenticate } from './store/session';
11 | import SideBar from './components/SideBar';
12 | import SideBar2 from './components/sideBar2';
13 | import FastUpload from './components/FastUpload';
14 | import { ModalProvider } from "./context/Modal";
15 | import FastForwards from './components/FastForward';
16 | import FastForwardIndexItem from './components/FastForwardIndexItem'
17 | import * as followActions from './store/follower'
18 | import FollowFeed from './components/FollowFeed';
19 | import toast, { Toaster } from 'react-hot-toast';
20 | import TopCreators from './components/TopCreators';
21 |
22 | function App() {
23 |
24 | let followings = useSelector((state) => Object.values(state.follower.following))
25 | followings = followings.map((user) => user.id)
26 | const dispatch = useDispatch();
27 | const user = useSelector((state) => state.session.user);
28 |
29 | const [loaded, setLoaded] = useState(false);
30 | const [isLoaded, setIsLoaded] = useState(false);
31 |
32 | useEffect(() => {
33 | if (user) {
34 | dispatch(followActions.followingList(user.id))
35 | .then(() => setIsLoaded(true))
36 | }
37 | }, [dispatch, isLoaded]);
38 |
39 |
40 | useEffect(() => {
41 | (async () => {
42 | await dispatch(authenticate());
43 | setLoaded(true);
44 | })();
45 | }, [dispatch]);
46 |
47 | if (!loaded) {
48 | return null;
49 | }
50 |
51 | return (
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | {followings && }
84 | {!followings.length && }
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | );
95 | }
96 |
97 | export default App;
98 |
--------------------------------------------------------------------------------
/migrations/env.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 |
3 | import logging
4 | from logging.config import fileConfig
5 |
6 | from sqlalchemy import engine_from_config
7 | from sqlalchemy import pool
8 |
9 | from alembic import context
10 |
11 | import os
12 | environment = os.getenv("FLASK_ENV")
13 | SCHEMA = os.environ.get("SCHEMA")
14 |
15 |
16 | # this is the Alembic Config object, which provides
17 | # access to the values within the .ini file in use.
18 | config = context.config
19 |
20 | # Interpret the config file for Python logging.
21 | # This line sets up loggers basically.
22 | fileConfig(config.config_file_name)
23 | logger = logging.getLogger('alembic.env')
24 |
25 | # add your model's MetaData object here
26 | # for 'autogenerate' support
27 | # from myapp import mymodel
28 | # target_metadata = mymodel.Base.metadata
29 | from flask import current_app
30 | config.set_main_option(
31 | 'sqlalchemy.url',
32 | str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))
33 | target_metadata = current_app.extensions['migrate'].db.metadata
34 |
35 | # other values from the config, defined by the needs of env.py,
36 | # can be acquired:
37 | # my_important_option = config.get_main_option("my_important_option")
38 | # ... etc.
39 |
40 |
41 | def run_migrations_offline():
42 | """Run migrations in 'offline' mode.
43 |
44 | This configures the context with just a URL
45 | and not an Engine, though an Engine is acceptable
46 | here as well. By skipping the Engine creation
47 | we don't even need a DBAPI to be available.
48 |
49 | Calls to context.execute() here emit the given string to the
50 | script output.
51 |
52 | """
53 | url = config.get_main_option("sqlalchemy.url")
54 | context.configure(
55 | url=url, target_metadata=target_metadata, literal_binds=True
56 | )
57 |
58 | with context.begin_transaction():
59 | context.run_migrations()
60 |
61 |
62 | def run_migrations_online():
63 | """Run migrations in 'online' mode.
64 |
65 | In this scenario we need to create an Engine
66 | and associate a connection with the context.
67 |
68 | """
69 |
70 | # this callback is used to prevent an auto-migration from being generated
71 | # when there are no changes to the schema
72 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
73 | def process_revision_directives(context, revision, directives):
74 | if getattr(config.cmd_opts, 'autogenerate', False):
75 | script = directives[0]
76 | if script.upgrade_ops.is_empty():
77 | directives[:] = []
78 | logger.info('No changes in schema detected.')
79 |
80 | connectable = engine_from_config(
81 | config.get_section(config.config_ini_section),
82 | prefix='sqlalchemy.',
83 | poolclass=pool.NullPool,
84 | )
85 |
86 | with connectable.connect() as connection:
87 | context.configure(
88 | connection=connection,
89 | target_metadata=target_metadata,
90 | process_revision_directives=process_revision_directives,
91 | **current_app.extensions['migrate'].configure_args
92 | )
93 | # Create a schema (only in production)
94 | if environment == "production":
95 | connection.execute(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
96 |
97 | # Set search path to your schema (only in production)
98 | with context.begin_transaction():
99 | if environment == "production":
100 | context.execute(f"SET search_path TO {SCHEMA}")
101 | context.run_migrations()
102 |
103 | if context.is_offline_mode():
104 | run_migrations_offline()
105 | else:
106 | run_migrations_online()
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | from flask import Flask, request, redirect
3 | from flask_cors import CORS
4 | from flask_migrate import Migrate
5 | from flask_wtf.csrf import generate_csrf
6 | from flask_login import LoginManager
7 | from .models import db, User
8 | from .api.follower_routes import followers_routes
9 | from .api.user_routes import user_routes
10 | from .api.auth_routes import auth_routes
11 | from .api.fast_forward_routes import fast_forward_routes
12 | from .api.comment_routes import comments_routes
13 | from .api.clip_routes import clip_routes
14 | from .api.like_routes import likes_routes
15 | from .seeds import seed_commands
16 | from .config import Config
17 |
18 |
19 | app = Flask(__name__, static_folder="../react-app/build", static_url_path="/")
20 |
21 | # Setup login manager
22 | login = LoginManager(app)
23 | login.login_view = "auth.unauthorized"
24 |
25 |
26 | @login.user_loader
27 | def load_user(id):
28 | print("Load User is running")
29 | return User.query.get(int(id))
30 |
31 |
32 | # Tell flask about our seed commands
33 | app.cli.add_command(seed_commands)
34 |
35 | app.config.from_object(Config)
36 |
37 | app.register_blueprint(user_routes, url_prefix='/api/users')
38 | app.register_blueprint(auth_routes, url_prefix='/api/auth')
39 | app.register_blueprint(fast_forward_routes, url_prefix='/api/fastForwards')
40 | app.register_blueprint(followers_routes, url_prefix="/api/followers")
41 | app.register_blueprint(likes_routes, url_prefix="/api/likes")
42 | app.register_blueprint(comments_routes, url_prefix="/api/comments")
43 | app.register_blueprint(clip_routes, url_prefix="/api/clips")
44 |
45 | db.init_app(app)
46 | Migrate(app, db)
47 |
48 | # Application Security
49 | CORS(app)
50 |
51 |
52 | # Since we are deploying with Docker and Flask,
53 | # we won't be using a buildpack when we deploy to Heroku.
54 | # Therefore, we need to make sure that in production any
55 | # request made over http is redirected to https.
56 | # Well.........
57 | @app.before_request
58 | def https_redirect():
59 | if os.environ.get("FLASK_ENV") == "production":
60 | if request.headers.get("X-Forwarded-Proto") == "http":
61 | url = request.url.replace("http://", "https://", 1)
62 | code = 301
63 | return redirect(url, code=code)
64 |
65 |
66 | @app.after_request
67 | def inject_csrf_token(response):
68 | response.set_cookie(
69 | "csrf_token",
70 | generate_csrf(),
71 | secure=True if os.environ.get("FLASK_ENV") == "production" else False,
72 | samesite="Strict" if os.environ.get("FLASK_ENV") == "production" else None,
73 | httponly=True,
74 | )
75 | return response
76 |
77 |
78 |
79 |
80 | @app.route("/api/docs")
81 | def api_help():
82 | """
83 | Returns all API routes and their doc strings
84 | """
85 | acceptable_methods = ["GET", "POST", "PUT", "PATCH", "DELETE"]
86 | route_list = {
87 | rule.rule: [
88 | [method for method in rule.methods if method in acceptable_methods],
89 | app.view_functions[rule.endpoint].__doc__,
90 | ]
91 | for rule in app.url_map.iter_rules()
92 | if rule.endpoint != "static"
93 | }
94 | return route_list
95 |
96 | @app.route("/", defaults={"path": ""})
97 | @app.route("/")
98 | def react_root(path):
99 | """
100 | This route will direct to the public directory in our
101 | react builds in the production environment for favicon
102 | or index.html requests
103 | """
104 | if path == "favicon.ico":
105 | return app.send_from_directory("public", "favicon.ico")
106 | return app.send_static_file("index.html")
107 |
108 |
109 | @app.errorhandler(404)
110 | def not_found(e):
111 | return app.send_static_file('index.html')
112 |
--------------------------------------------------------------------------------
/react-app/src/components/TopCreators.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 { toast } from 'react-hot-toast';
7 | import CommentForm from "./CommentForm";
8 | import CommentEditForm from "./CommentEditForm";
9 | import * as followActions from '../store/follower'
10 | import './FastForwards.css'
11 |
12 | const TopCreators = () => {
13 | // const user = useSelector((state) => state.session.user);
14 | const history = useHistory();
15 | const fastForwards = Object.values(useSelector((state) => state.fastForward));
16 | const sample = fastForwards.slice(13, 40)
17 | console.log(sample)
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 |
26 | useEffect(() => {
27 | dispatch(fastForwardActions.fetchAllFastForwards())
28 | toast("You don't seem to be following anyone. Here are some of our top creators!")
29 | }, [dispatch]);
30 |
31 | const openMenu = () => {
32 | if (!showMenu) setShowMenu(true);
33 | if (showMenu) setShowMenu(false);
34 | };
35 |
36 | const handleDelete = async (commentId, fastForwardId) => {
37 | await dispatch(deleteComment(commentId, fastForwardId))
38 | await dispatch(fastForwardActions.fetchAllFastForwards())
39 | };
40 |
41 | return (
42 |
43 |
{sample?.map((fastForward) => (
44 |
45 |
46 |
47 |
52 |
53 |
54 |
55 |
{fastForward.User.username}
56 |
{fastForward.User.first_name} {fastForward.User.last_name}
57 |
58 |
59 | {fastForward.caption}
60 |
61 |
62 |
63 |
64 |
event.target.play()} onMouseOut={event => event.target.pause()} width="350" height="600" border-radius='8'>
65 |
66 | {user &&
67 |
68 |
69 |
70 |
71 |
{fastForward?.Comments?.length}
72 |
}
73 |
74 |
75 |
76 |
))}
77 |
78 | );
79 | };
80 |
81 | export default TopCreators;
82 |
--------------------------------------------------------------------------------
/react-app/src/components/auth/SignUpForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux'
3 | import { Redirect } from 'react-router-dom';
4 | import { signUp } from '../../store/session';
5 |
6 | const SignUpForm = () => {
7 | const [errors, setErrors] = useState([]);
8 | const [username, setUsername] = useState('');
9 | const [firstName, setFirstName] = useState('');
10 | const [lastName, setLastName] = useState('');
11 | const [imageUrl, setImageUrl] = useState('');
12 | const [bio, setBio] = useState('');
13 | const [email, setEmail] = useState('');
14 | const [password, setPassword] = useState('');
15 | const [repeatPassword, setRepeatPassword] = useState('');
16 | const user = useSelector(state => state.session.user);
17 | const dispatch = useDispatch();
18 |
19 | const onSignUp = async (e) => {
20 | e.preventDefault();
21 | if (password === repeatPassword) {
22 | const data = await dispatch(signUp(firstName, lastName, bio, imageUrl, username, email, password));
23 | if (data) {
24 | setErrors(data)
25 | }
26 | }
27 | };
28 |
29 | const updatedFirstName = (e) => {
30 | setFirstName(e.target.value);
31 | };
32 | const updatedLastName = (e) => {
33 | setLastName(e.target.value);
34 | };
35 | const updatedBio = (e) => {
36 | setBio(e.target.value);
37 | };
38 | const updatedImageUrl = (e) => {
39 | setImageUrl(e.target.value);
40 | };
41 |
42 | const updateUsername = (e) => {
43 | setUsername(e.target.value);
44 | };
45 |
46 | const updateEmail = (e) => {
47 | setEmail(e.target.value);
48 | };
49 |
50 | const updatePassword = (e) => {
51 | setPassword(e.target.value);
52 | };
53 |
54 | const updateRepeatPassword = (e) => {
55 | setRepeatPassword(e.target.value);
56 | };
57 |
58 | if (user) {
59 | return ;
60 | }
61 |
62 | return (
63 |
64 |
65 | {errors.map((error, ind) => (
66 |
{error}
67 | ))}
68 |
69 |
70 | First Name
71 |
77 |
78 |
79 | Last Name
80 |
86 |
87 |
88 | Tell us about yourself!
89 |
95 |
96 |
97 | Upload a Profile Picture!
98 |
104 |
105 |
106 | User Name
107 |
113 |
114 |
115 | Email
116 |
122 |
123 |
124 | Password
125 |
131 |
132 |
133 | Repeat Password
134 |
141 |
142 | Sign Up
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 |
87 | {/*
88 |
89 | Continue with FaceBook
90 |
91 |
92 |
93 | Continue with Google
94 |
95 |
96 |
97 | Continue with Instagram
98 | */}
99 |
100 |
101 | Login
102 |
103 |
104 | Demo
105 |
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 |
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 | event.target.play()} onMouseOut={event => event.target.pause()} width="200" height="300" border-radius='8'>
126 |
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 |
68 |
69 |
70 |
71 |
72 | FastForward
73 |
74 |
75 |
76 |
77 | {query &&
Accounts }
78 | {query ? usersNav.filter(user => {
79 | if (query === '') {
80 | return user
81 | } else if (user.username.toLowerCase().includes(query.toLocaleLowerCase())) {
82 | return user
83 | }
84 | }).map((user, index) => (
85 |
86 |
87 |
88 |
searchClick2(user.id)}>{user.username}
89 | {user?.first_name}
90 | {user?.last_name}
91 |
92 |
93 | )) : null}
94 |
95 | setQuery(event.target.value)}>
96 |
97 |
98 |
99 | searchClick()}>
100 |
101 |
102 |
103 |
+ Upload
104 | {!user &&
}
105 | {/* {user &&
} */}
106 | {user &&
}
107 |
108 | {showMenu &&
109 |
110 |
111 |
112 |
113 |
114 |
115 |
{user.username}
116 |
{user.first_name} {user.last_name}
117 |
{user.email}
118 |
119 |
120 |
121 |
122 | View Profile
123 |
124 |
125 | Top Creators
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 | }
136 |
137 |
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 | Caption
143 | setCaption(e.target.value)}
148 | required
149 | />
150 |
151 |
152 |
153 | {!hasSubmitted &&
Submit }
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 |
212 |
213 | Submit
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 |
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 |
event.target.play()} onMouseOut={event => event.target.pause()} width="350" height="600" border-radius='8'>
103 |
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 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
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 |
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 |
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 |
--------------------------------------------------------------------------------