├── .flaskenv ├── react-app ├── public │ ├── index.css │ ├── favicon.ico │ └── index.html ├── src │ ├── components │ │ ├── Comments │ │ │ └── comment.js │ │ ├── Homepage │ │ │ ├── .index.js.swp │ │ │ ├── homepage.css │ │ │ └── index.js │ │ ├── auth │ │ │ ├── logout.css │ │ │ ├── ProtectedRoute.js │ │ │ ├── LogoutButton.js │ │ │ ├── DemoLogin.js │ │ │ ├── login-signup.css │ │ │ ├── LoginForm.js │ │ │ └── SignUpForm.js │ │ ├── Following │ │ │ ├── following.css │ │ │ └── following.js │ │ ├── UsersList.js │ │ ├── Navbar.css │ │ ├── EditCaption │ │ │ ├── editcaption.css │ │ │ └── index.js │ │ ├── PostForm │ │ │ ├── postForm.css │ │ │ └── index.js │ │ ├── PostDetails │ │ │ ├── postdetails.css │ │ │ └── index.js │ │ ├── NavBar.js │ │ ├── UserProfile │ │ │ ├── user.css │ │ │ └── User.js │ │ └── splash-page │ │ │ ├── SplashPage.css │ │ │ └── SplashPage.js │ ├── index.css │ ├── index.js │ ├── Followers │ │ ├── followers.css │ │ └── followers.js │ ├── store │ │ ├── index.js │ │ ├── user.js │ │ ├── session.js │ │ └── post.js │ └── App.js ├── .env.example ├── .gitignore ├── README.md └── package.json ├── dev-requirements.txt ├── migrations ├── README ├── script.py.mako ├── alembic.ini └── env.py ├── migrations_old_version ├── README ├── script.py.mako ├── alembic.ini ├── versions │ ├── 20201120_150602_create_users_table.py │ ├── 20210816_113319_user_py.py │ └── 20210816_160610_comment_post.py └── env.py ├── artygram.png ├── app ├── models │ ├── db.py │ ├── __init__.py │ ├── like.py │ ├── comment.py │ ├── post.py │ └── user.py ├── forms │ ├── __init__.py │ ├── post_form.py │ ├── signup_form.py │ └── login_form.py ├── config.py ├── seeds │ ├── __init__.py │ ├── posts.py │ └── users.py ├── api │ ├── user_routes.py │ ├── auth_routes.py │ └── post_routes.py └── __init__.py ├── artygram_homeView.png ├── package.json ├── .dockerignore ├── .gitignore ├── .env.example ├── requirements.txt ├── Dockerfile ├── Pipfile ├── README.md ├── setup.md └── Pipfile.lock /.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=app -------------------------------------------------------------------------------- /react-app/public/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | psycopg2-binary==2.8.6 2 | -------------------------------------------------------------------------------- /react-app/src/components/Comments/comment.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations_old_version/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /react-app/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_BASE_URL=http://localhost:5000 2 | -------------------------------------------------------------------------------- /artygram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lemlooma/Artygram/HEAD/artygram.png -------------------------------------------------------------------------------- /app/models/db.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | 4 | db = SQLAlchemy() 5 | -------------------------------------------------------------------------------- /artygram_homeView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lemlooma/Artygram/HEAD/artygram_homeView.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react-particles-js": "^3.5.3" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | react-app/node_modules 2 | .venv 3 | Pipfile 4 | Pipfile.lock 5 | .env 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /app/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from .login_form import LoginForm 2 | from .signup_form import SignUpForm 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | __pycache__/ 3 | *.py[cod] 4 | .venv 5 | .DS_Store 6 | .vscode/ 7 | .node_modules/ 8 | -------------------------------------------------------------------------------- /react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lemlooma/Artygram/HEAD/react-app/public/favicon.ico -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from .user import User 3 | from .post import Post 4 | from .comment import Comment 5 | -------------------------------------------------------------------------------- /react-app/src/components/Homepage/.index.js.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lemlooma/Artygram/HEAD/react-app/src/components/Homepage/.index.js.swp -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | FLASK_APP=app 2 | FLASK_ENV=development 3 | SECRET_KEY=lkasjdf09ajsdkfljalsiorj12n3490re9485309irefvn,u90818734902139489230 4 | DATABASE_URL=postgresql://starter_app_dev@localhost/starter_db 5 | -------------------------------------------------------------------------------- /react-app/src/components/auth/logout.css: -------------------------------------------------------------------------------- 1 | #logOutButton { 2 | padding-left: 17px; 3 | /* padding-bottom: 15px; */ 4 | font-size: 28px; 5 | } 6 | 7 | #logOutButton:hover { 8 | cursor: pointer; 9 | color: rgb(249, 95, 167); 10 | } 11 | -------------------------------------------------------------------------------- /react-app/src/index.css: -------------------------------------------------------------------------------- 1 | /* TODO Add site wide styles */ 2 | 3 | 4 | body, html{ 5 | position: relative; 6 | min-height: 100vh; 7 | width: 100%; 8 | height:100%; 9 | background-repeat: no-repeat; 10 | background-attachment: fixed; 11 | margin: auto 12 | 13 | } 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/models/like.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from datetime import datetime 3 | 4 | likes = db.Table( 5 | "likes", 6 | db.Model.metadata, 7 | db.Column("userId", db.Integer, db.ForeignKey("users.id")), 8 | db.Column("postId", db.Integer, db.ForeignKey("posts.id")), 9 | db.Column("timestamp", db.DateTime, default=datetime.now) 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /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* 24 | -------------------------------------------------------------------------------- /app/forms/post_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, IntegerField, DateField 3 | from wtforms.validators import DataRequired, ValidationError 4 | 5 | 6 | class CreatePostForm(FlaskForm): 7 | caption = StringField('Caption', validators=[DataRequired()]) 8 | pic_url = StringField('Picture Url', validators=[DataRequired()]) 9 | 10 | def updateCaption(self, newCaption): 11 | self.caption = newCaption 12 | -------------------------------------------------------------------------------- /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 | 9 | const store = configureStore(); 10 | 11 | ReactDOM.render( 12 | 13 | 14 | 15 | 16 | , 17 | document.getElementById('root') 18 | ); 19 | -------------------------------------------------------------------------------- /react-app/src/Followers/followers.css: -------------------------------------------------------------------------------- 1 | .followingPageBody { 2 | 3 | padding-top: 100px; 4 | } 5 | 6 | .profilePic { 7 | width: 100px; 8 | height: 100px; 9 | border-radius: 50%; 10 | 11 | } 12 | 13 | .followersDetailContainer { 14 | 15 | display: flex; 16 | flex-direction: row; 17 | height: 150px; 18 | 19 | 20 | } 21 | 22 | .followersDetail { 23 | text-decoration: none; 24 | color: black; 25 | padding-top: 40px; 26 | padding-left: 30px; 27 | font-size: 20px; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /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. While is development, run this application from this location using `npm start`. 4 | 5 | 6 | No environment variables are needed to run this application in development, but be sure to set the REACT_APP_BASE_URL environment variable in heroku! 7 | 8 | This app will be automatically built when you deploy to heroku, please see the `heroku-postbuild` script in your `express.js` applications `package.json` to see how this works. 9 | -------------------------------------------------------------------------------- /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 11 | SQLALCHEMY_DATABASE_URI = os.environ.get( 12 | 'DATABASE_URL').replace('postgres://', 'postgresql://') 13 | SQLALCHEMY_ECHO = True 14 | 15 | 16 | -------------------------------------------------------------------------------- /react-app/src/components/Following/following.css: -------------------------------------------------------------------------------- 1 | .followingPageBody { 2 | 3 | padding-top: 100px; 4 | } 5 | 6 | .profilePic { 7 | width: 100px; 8 | height: 100px; 9 | border-radius: 50%; 10 | 11 | } 12 | 13 | .followersDetailContainer { 14 | 15 | display: flex; 16 | flex-direction: row; 17 | justify-content: flex-start; 18 | height: 150px; 19 | padding-top: 40px; 20 | padding-left: 48%; 21 | 22 | 23 | } 24 | 25 | .followersDetail { 26 | text-decoration: none; 27 | color: black; 28 | font-size: 20px; 29 | 30 | } 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /react-app/src/components/auth/LogoutButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch} from 'react-redux'; 3 | import { useHistory } from 'react-router-dom'; 4 | import { logout } from '../../store/session'; 5 | import "./logout.css"; 6 | 7 | const LogoutButton = () => { 8 | const history = useHistory() 9 | const dispatch = useDispatch() 10 | const onLogout = async (e) => { 11 | await dispatch(logout()); 12 | history.push("/splash") 13 | }; 14 | 15 | return 16 | }; 17 | 18 | export default LogoutButton; 19 | -------------------------------------------------------------------------------- /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"} 25 | -------------------------------------------------------------------------------- /migrations_old_version/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"} 25 | -------------------------------------------------------------------------------- /react-app/src/components/auth/DemoLogin.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { loginDemo } from '../../store/session'; 4 | import { useHistory } from "react-router-dom"; 5 | 6 | 7 | const DemoLoginButton = () => { 8 | const dispatch = useDispatch() 9 | const history = useHistory() 10 | const onDemoLogin = async (e) => { 11 | await dispatch(loginDemo()); 12 | history.push("/") 13 | }; 14 | 15 | return ( 16 | 19 | ); 20 | }; 21 | 22 | export default DemoLoginButton; -------------------------------------------------------------------------------- /app/seeds/__init__.py: -------------------------------------------------------------------------------- 1 | from flask.cli import AppGroup 2 | from .users import seed_users, undo_users 3 | from .posts import seed_posts, undo_posts 4 | 5 | # Creates a seed group to hold our commands 6 | # So we can type `flask seed --help` 7 | seed_commands = AppGroup('seed') 8 | 9 | 10 | # Creates the `flask seed all` command 11 | @seed_commands.command('all') 12 | def seed(): 13 | seed_users() 14 | seed_posts() 15 | # Add other seed functions here 16 | 17 | 18 | # Creates the `flask seed undo` command 19 | @seed_commands.command('undo') 20 | def undo(): 21 | undo_users() 22 | # undo_posts() 23 | # Add other undo functions here 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 AS build-stage 2 | 3 | WORKDIR /react-app 4 | COPY react-app/. . 5 | 6 | # You have to set this because it should be set during build time. 7 | ENV REACT_APP_BASE_URL=https://artygram.herokuapp.com 8 | 9 | # Build our React App 10 | RUN npm install 11 | RUN npm run build 12 | 13 | FROM python:3.9 14 | 15 | # Setup Flask environment 16 | ENV FLASK_APP=app 17 | ENV FLASK_ENV=production 18 | ENV SQLALCHEMY_ECHO=True 19 | 20 | EXPOSE 8000 21 | 22 | WORKDIR /var/www 23 | COPY . . 24 | COPY --from=build-stage /react-app/build/* app/static/ 25 | 26 | # Install Python Dependencies 27 | RUN pip install -r requirements.txt 28 | RUN pip install psycopg2 29 | 30 | # Run flask environment 31 | CMD gunicorn app:app 32 | -------------------------------------------------------------------------------- /react-app/src/components/UsersList.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | function UsersList() { 5 | const [users, setUsers] = useState([]); 6 | 7 | useEffect(() => { 8 | async function fetchData() { 9 | const response = await fetch('/api/users/'); 10 | const responseData = await response.json(); 11 | setUsers(responseData.users); 12 | } 13 | fetchData(); 14 | }, []); 15 | 16 | const userComponents = users.map((user) => { 17 | return ( 18 |
  • 19 | {user.username} 20 |
  • 21 | ); 22 | }); 23 | 24 | return ( 25 | <> 26 |

    User List:

    27 | 28 | 29 | ); 30 | } 31 | 32 | export default UsersList; 33 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [[source]] 7 | url = "https://pypi.org/simple" 8 | verify_ssl = true 9 | 10 | [packages] 11 | alembic = "==1.6.5" 12 | click = "==7.1.2" 13 | Flask-Cors = "==3.0.8" 14 | Flask-Login = "==0.5.0" 15 | Flask-Migrate = "==3.0.1" 16 | Flask-SQLAlchemy = "==2.5.1" 17 | Flask-WTF = "==0.15.1" 18 | Flask = "==2.0.1" 19 | greenlet = "==1.1.0" 20 | gunicorn = "==20.1.0" 21 | itsdangerous = "==2.0.1" 22 | Jinja2 = "==3.0.1" 23 | Mako = "==1.1.4" 24 | MarkupSafe = "==2.0.1" 25 | python-dateutil = "==2.8.1" 26 | python-dotenv = "==0.14.0" 27 | python-editor = "==1.0.4" 28 | six = "==1.15.0" 29 | SQLAlchemy = "==1.4.19" 30 | Werkzeug = "==2.0.1" 31 | WTForms = "==2.3.3" 32 | 33 | [dev-packages] 34 | psycopg2-binary = "==2.8.6" 35 | 36 | [requires] 37 | python_version = "3.9" 38 | -------------------------------------------------------------------------------- /app/models/comment.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from datetime import datetime 3 | 4 | 5 | class Comment(db.Model): 6 | __tablename__ = 'comments' 7 | 8 | id = db.Column(db.Integer, primary_key=True) 9 | caption = db.Column(db.String(255), nullable=False) 10 | post_id = db.Column(db.Integer, db.ForeignKey("posts.id") ) 11 | user_id = db.Column(db.Integer, db.ForeignKey('users.id')) 12 | timestamp = db.Column(db.DateTime, default=datetime.now) 13 | 14 | 15 | users = db.relationship('User', back_populates="comments") 16 | 17 | posts = db.relationship('Post', back_populates='comments') 18 | 19 | def to_dict(self): 20 | return { 21 | "id": self.id, 22 | "caption": self.comment, 23 | "postId": self.postId, 24 | "userId": self.userId, 25 | "timestamp": self.timestamp 26 | } -------------------------------------------------------------------------------- /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 posts from './post' 5 | import users from './user' 6 | 7 | 8 | 9 | const rootReducer = combineReducers({ 10 | session, 11 | posts, 12 | users 13 | }); 14 | 15 | 16 | let enhancer; 17 | 18 | if (process.env.NODE_ENV === 'production') { 19 | enhancer = applyMiddleware(thunk); 20 | } else { 21 | const logger = require('redux-logger').default; 22 | const composeEnhancers = 23 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 24 | enhancer = composeEnhancers(applyMiddleware(thunk, logger)); 25 | } 26 | 27 | const configureStore = (preloadedState) => { 28 | return createStore(rootReducer, preloadedState, enhancer); 29 | }; 30 | 31 | export default configureStore; 32 | -------------------------------------------------------------------------------- /react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Artygram 19 | 24 | 25 | 26 | 27 | 28 |
    29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /migrations_old_version/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 46 | -------------------------------------------------------------------------------- /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 | class SignUpForm(FlaskForm): 24 | username = StringField( 25 | 'username', validators=[DataRequired(), username_exists]) 26 | email = StringField('email', validators=[DataRequired(), user_exists]) 27 | password = StringField('password', validators=[DataRequired()]) 28 | profile_pic = StringField('profile_pic') 29 | -------------------------------------------------------------------------------- /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]) 30 | -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(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,flask_migrate 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 | [logger_flask_migrate] 38 | level = INFO 39 | handlers = 40 | qualname = flask_migrate 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /react-app/src/components/Navbar.css: -------------------------------------------------------------------------------- 1 | .entire__navabar{ 2 | position: fixed; 3 | top:0; 4 | left:0; 5 | width:100%; 6 | } 7 | 8 | .navbar__container{ 9 | background-color: white; 10 | border-bottom: 1px solid rgba(128, 128, 128, 0.384); 11 | height: 70px; 12 | display: flex; 13 | justify-content: space-evenly; 14 | align-items: center; 15 | top: 0px; 16 | width: 100%; 17 | 18 | } 19 | 20 | .artygram_logo { 21 | width: 150px; 22 | margin-right: 10px; 23 | } 24 | 25 | .addpic{ 26 | width: 40px; 27 | margin-right: 15px; 28 | } 29 | 30 | .addpicHover:hover{ 31 | cursor: pointer; 32 | 33 | } 34 | 35 | .right-navbar{ 36 | display: flex; 37 | flex-direction: row; 38 | align-items: center; 39 | justify-content: space-between; 40 | width: 150px; 41 | } 42 | 43 | .navbarProPic{ 44 | width: 26px; 45 | border-radius: 50% ; 46 | height: 28px; 47 | margin-top: 7px; 48 | } 49 | .search{ 50 | width:300px; 51 | height: 25px; 52 | border-radius: 8px; 53 | text-align: center; 54 | } 55 | 56 | .logout__container{ 57 | display: flex; 58 | justify-content: center; 59 | align-items: center; 60 | } 61 | -------------------------------------------------------------------------------- /migrations_old_version/versions/20201120_150602_create_users_table.py: -------------------------------------------------------------------------------- 1 | """create_users_table 2 | 3 | Revision ID: ffdc0a98111c 4 | Revises: 5 | Create Date: 2020-11-20 15:06:02.230689 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'ffdc0a98111c' 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('username', sa.String(length=40), nullable=False), 24 | sa.Column('email', sa.String(length=255), nullable=False), 25 | sa.Column('hashed_password', sa.String(length=255), nullable=False), 26 | sa.PrimaryKeyConstraint('id'), 27 | sa.UniqueConstraint('email'), 28 | sa.UniqueConstraint('username') 29 | ) 30 | # ### end Alembic commands ###qqqqqqqqq 31 | 32 | 33 | def downgrade(): 34 | # ### commands auto generated by Alembic - please adjust! ### 35 | op.drop_table('users') 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /react-app/src/store/user.js: -------------------------------------------------------------------------------- 1 | const GET_FOLLOWING = "followers/GET_FOLLOWING"; 2 | const GET_USERS = "/users/GET_USERS"; 3 | 4 | const getFollowing = (user) => ({ 5 | type: GET_FOLLOWING, 6 | payload: user, 7 | }); 8 | 9 | const getUser = (users) => ({ 10 | type: GET_USERS, 11 | payload: users, 12 | }); 13 | 14 | // export const getAllFollowing = (id) => async (dispatch) => { 15 | // const response = await fetch(`/api/users/${id}/following`); 16 | 17 | // if (response.ok) { 18 | // const following = await response.json(); 19 | // dispatch(getFollowing(following)); 20 | // } 21 | // }; 22 | 23 | export const getAllUsers = () => async (dispatch) => { 24 | const response = await fetch("/api/users/all"); 25 | 26 | if (response.ok) { 27 | const users = await response.json(); 28 | dispatch(getUser(users)); 29 | } 30 | }; 31 | 32 | const initialState = {}; 33 | 34 | export default function reducer(state = initialState, action) { 35 | switch (action.type) { 36 | case GET_FOLLOWING: { 37 | return { user: action.payload }; 38 | } 39 | case GET_USERS: { 40 | return { ...state, ...action.payload }; 41 | } 42 | 43 | default: 44 | return state; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /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-particles-js": "^3.5.3", 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 | "tsparticles": "^1.33.3" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.3%", 33 | "not ie 11", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "proxy": "http://localhost:5000" 44 | } 45 | -------------------------------------------------------------------------------- /react-app/src/components/EditCaption/editcaption.css: -------------------------------------------------------------------------------- 1 | .update-button{ 2 | margin-top: 20px; 3 | background-color: #8dda98; 4 | 5 | border: 0px; 6 | height: 33.33px; 7 | /* letter-spacing: 0.1rem; */ 8 | color: #f5f7f9; 9 | border-radius: 3px; 10 | font-size: 0.8rem; 11 | font-weight: bold; 12 | transition: background-color 0.5s; 13 | transition-timing-function: ease-in-out; 14 | 15 | } 16 | .update-button:hover { 17 | background-color: #42c462; 18 | cursor: pointer; 19 | } 20 | 21 | .cancel-edit-button{ 22 | margin-top: 20px; 23 | 24 | background-color: #f78e8e; 25 | 26 | border: 0px; 27 | height: 33.33px; 28 | /* letter-spacing: 0.1rem; */ 29 | color: #f5f7f9; 30 | border-radius: 3px; 31 | font-size: 0.8rem; 32 | font-weight: bold; 33 | transition: background-color 0.5s; 34 | transition-timing-function: ease-in-out; 35 | 36 | } 37 | 38 | .cancel-edit-button:hover { 39 | background-color: rgb(241, 20, 20); 40 | cursor: pointer; 41 | } 42 | 43 | .edit-input-form{ 44 | width: 300px; 45 | background-color: #fafafa; 46 | height: 33.33px; 47 | border-radius: 3px; 48 | border: 1px solid #dbdbdb; 49 | padding: 0 10px; 50 | color: #000; 51 | } -------------------------------------------------------------------------------- /migrations_old_version/versions/20210816_113319_user_py.py: -------------------------------------------------------------------------------- 1 | """user.py 2 | 3 | Revision ID: db418e582c48 4 | Revises: ffdc0a98111c 5 | Create Date: 2021-08-16 11:33:19.556111 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'db418e582c48' 14 | down_revision = 'ffdc0a98111c' 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('followers', 22 | sa.Column('followerId', sa.Integer(), nullable=True), 23 | sa.Column('followingId', sa.Integer(), nullable=True), 24 | sa.Column('timestamp', sa.DateTime(), nullable=True), 25 | sa.ForeignKeyConstraint(['followerId'], ['users.id'], ), 26 | sa.ForeignKeyConstraint(['followingId'], ['users.id'], ) 27 | ) 28 | op.add_column('users', sa.Column('bio', sa.String(length=255), nullable=True)) 29 | op.add_column('users', sa.Column('profile_pic', sa.String(length=255), nullable=True)) 30 | # ### end Alembic commands ### 31 | 32 | 33 | def downgrade(): 34 | # ### commands auto generated by Alembic - please adjust! ### 35 | op.drop_column('users', 'profile_pic') 36 | op.drop_column('users', 'bio') 37 | op.drop_table('followers') 38 | # ### end Alembic commands ### 39 | -------------------------------------------------------------------------------- /react-app/src/components/Homepage/homepage.css: -------------------------------------------------------------------------------- 1 | .photo-feed__container{ 2 | display: flex; 3 | flex-direction: column; 4 | margin-top: 25px; 5 | align-items: center; 6 | } 7 | 8 | .single-post__container{ 9 | display: flex; 10 | flex-direction: column; 11 | margin-bottom: 25px; 12 | padding-bottom: 15px; 13 | width: 600px; 14 | box-sizing: border-box; 15 | border: 1px solid #dbdbdb; 16 | } 17 | 18 | .icon-username__container{ 19 | display: flex; 20 | flex-direction: row; 21 | align-items: center; 22 | padding: 10px; 23 | } 24 | 25 | .post-icon{ 26 | margin-right: 10px; 27 | border-radius: 60%; 28 | width: 50px; 29 | height: 50px; 30 | } 31 | 32 | .post-img{ 33 | width: 600px 34 | } 35 | 36 | .liked{ 37 | color: red; 38 | font-size: 20px; 39 | } 40 | 41 | .unliked{ 42 | font-size: 20px; 43 | } 44 | 45 | .fa-heart:hover{ 46 | cursor: pointer; 47 | } 48 | 49 | .likebutton{ 50 | border: none; 51 | background-color: white; 52 | padding-bottom: 2px; 53 | padding-top: 2px; 54 | 55 | } 56 | 57 | .post-username { 58 | text-decoration: none; 59 | color: black; 60 | } 61 | 62 | .caption-photofeed{ 63 | font-weight: bold; 64 | padding-bottom: 10px; 65 | padding-top: 10px; 66 | } 67 | 68 | .like-button-container{ 69 | padding: 4px; 70 | } 71 | 72 | .photofeed-details-container{ 73 | padding: 10px; 74 | } 75 | 76 | .timestamp{ 77 | font-size: 13px; 78 | padding-top: 20px; 79 | color:grey 80 | } -------------------------------------------------------------------------------- /react-app/src/components/EditCaption/index.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { updateCaption } from "../../store/post"; 3 | import { useDispatch } from 'react-redux' 4 | import "./editcaption.css" 5 | const EditCaption = ({ post, hideForm }) => { 6 | // const [editCaption, setEditCaption] = useState(post.caption) 7 | const [editCaption, setEditCaption] = useState(post.caption) 8 | const dispatch = useDispatch() 9 | 10 | const handleSubmit = async (e) => { 11 | 12 | e.preventDefault() 13 | 14 | const payload = { 15 | id: post.id, 16 | caption: editCaption, 17 | pic_url: post.pic_url, 18 | user_id: post.user_id 19 | 20 | } 21 | 22 | await dispatch(updateCaption(payload)) 23 | 24 | hideForm() 25 | } 26 | 27 | 28 | return ( 29 | <> 30 |
    31 | setEditCaption(e.target.value)} 37 | > 38 | 39 | 42 | 45 | 46 |
    47 | 48 | ); 49 | } 50 | 51 | export default EditCaption; 52 | -------------------------------------------------------------------------------- /app/models/post.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from flask import jsonify 3 | from datetime import datetime 4 | from .like import likes 5 | from .user import User 6 | 7 | 8 | class Post(db.Model): 9 | __tablename__ = 'posts' 10 | 11 | id = db.Column(db.Integer, primary_key=True) 12 | caption = db.Column(db.String(100), nullable=False) 13 | pic_url = db.Column(db.String(255), nullable=False) 14 | user_id = db.Column(db.Integer, db.ForeignKey('users.id')) 15 | timestamp = db.Column(db.DateTime, default=datetime.now, ) 16 | 17 | users = db.relationship('User', back_populates="posts") 18 | 19 | comments = db.relationship("Comment", back_populates="posts") 20 | 21 | postLikes = db.relationship( 22 | "User", secondary=likes, back_populates="userLikes") 23 | 24 | def to_dict(self): 25 | user = User.query.filter(User.id == self.user_id).first() 26 | 27 | return { 28 | 'id': self.id, 29 | 'caption': self.caption, 30 | 'pic_url': self.pic_url, 31 | "user_id": self.user_id, 32 | "timestamp": self.timestamp, 33 | "user": user.to_dict(), 34 | "comments": self.comments, 35 | "postlikes": [user.id for user in self.postLikes], 36 | "likesnum": len(self.postLikes), 37 | "commentsnum": len(self.comments) 38 | } 39 | 40 | def to_dict_associations(self): 41 | return { 42 | "comments": self.comments, 43 | "postlikes": self.postLikes, 44 | "likesnum": len(self.postLikes), 45 | "commentsnum": len(self.comments) 46 | } 47 | -------------------------------------------------------------------------------- /react-app/src/components/PostForm/postForm.css: -------------------------------------------------------------------------------- 1 | .post-form__container { 2 | margin: 100px auto 0px auto; 3 | padding: 30px 10px; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: center; 8 | background-color: #ffffff; 9 | width: 400px; 10 | border: 1px solid #dbdbdb; 11 | border-radius: 3px; 12 | } 13 | 14 | .post-form { 15 | display: flex; 16 | flex-direction: column; 17 | } 18 | 19 | .textarea { 20 | height: 200px; 21 | width: 300px; 22 | background-color: #fafafa; 23 | border-radius: 3px; 24 | border: 1px solid #dbdbdb; 25 | padding: 5px 10px; 26 | color: #000; 27 | } 28 | 29 | .create-button { 30 | margin-top: 20px; 31 | background-color: #b2dffc; 32 | 33 | border: 0px; 34 | height: 33.33px; 35 | /* letter-spacing: 0.1rem; */ 36 | color: #f5f7f9; 37 | border-radius: 3px; 38 | font-size: 0.8rem; 39 | font-weight: bold; 40 | transition: background-color 0.5s; 41 | transition-timing-function: ease-in-out; 42 | } 43 | 44 | .create-button:hover { 45 | background-color: #0095f6; 46 | 47 | cursor: pointer; 48 | } 49 | 50 | .cancel-button{ 51 | margin-top: 20px; 52 | background-color: rgba(255, 0, 0, 0.555); 53 | 54 | border: 0px; 55 | height: 33.33px; 56 | /* letter-spacing: 0.1rem; */ 57 | color: #f5f7f9; 58 | border-radius: 3px; 59 | font-size: 0.8rem; 60 | font-weight: bold; 61 | transition: background-color 0.5s; 62 | transition-timing-function: ease-in-out; 63 | } 64 | 65 | .cancel-button:hover { 66 | background-color: red; 67 | 68 | cursor: pointer; 69 | } 70 | -------------------------------------------------------------------------------- /react-app/src/components/Following/following.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { NavLink, useParams } from "react-router-dom"; 4 | import { getAllUsers } from "../../store/user"; 5 | import "./following.css"; 6 | 7 | function Following() { 8 | const { userId } = useParams(); 9 | 10 | // const user = useSelector((state) => state.session.user); 11 | const dispatch = useDispatch(); 12 | 13 | const allUser = useSelector((state) => state.users); 14 | useEffect(() => { 15 | dispatch(getAllUsers()); 16 | }, [dispatch]); 17 | 18 | return ( 19 |
    20 | {allUser.users?.map((user) => 21 | user.follows.map((following) => 22 | +userId === user.id ? ( 23 | <> 24 |
    25 | {allUser.users?.map((user) => 26 | user.id === following.id ? ( 27 |
    28 | 29 | {user.id}/ 30 | 31 |
    32 | ) : null 33 | )} 34 | 35 |
    36 | 37 |
    {following.username}
    38 |
    39 |
    40 |
    41 | 42 | ) : null 43 | ) 44 | )} 45 |
    46 | ); 47 | } 48 | 49 | export default Following; 50 | -------------------------------------------------------------------------------- /react-app/src/Followers/followers.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { NavLink, useParams } from "react-router-dom"; 4 | import { getAllUsers } from "../store/user"; 5 | 6 | // import PostForm from '../PostForm'; 7 | import "./followers.css"; 8 | 9 | function Followers() { 10 | const { userId } = useParams(); 11 | // const user = useSelector((state) => state.session.user); 12 | const dispatch = useDispatch(); 13 | 14 | const allUser = useSelector((state) => state.users); 15 | 16 | 17 | useEffect(() => { 18 | dispatch(getAllUsers()); 19 | }, [dispatch]); 20 | 21 | return ( 22 |
    23 | {allUser.users?.map((user) => 24 | user.follow_by.map((followBy) => 25 | +userId === user.id ? ( 26 | <> 27 |
    28 | {allUser.users?.map((user) => 29 | user.id === followBy.id ? ( 30 |
    31 | 32 | {user.id} 33 | 34 |
    35 | ) : null 36 | )} 37 | 38 |
    39 | 40 |
    {followBy.username}
    41 |
    42 |
    43 |
    44 | 45 | ) : null 46 | ) 47 | )} 48 |
    49 | ); 50 | } 51 | 52 | export default Followers; 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Artygram 2 | 3 | Wiki: https://github.com/lemlooma/Artygram/wiki 4 | 5 | Live Demo: https://artygram.herokuapp.com/ 6 | 7 | Artygram is an Instrgram clone targeted towards artist. On Artygram you can browse artist's profile pages and view their work 8 | 9 | ## Technologies 10 | * 11 | 12 | * Python 13 | 14 | * 15 | 16 | * 17 | 18 | * Flask 19 | 20 | 21 | * 22 | 23 | * 24 | 25 | ## Welcome View 26 | 27 | ![welcome](artygram.png) 28 | 29 | ## Home View 30 | 31 | ![homepage](artygram_homeView.png) 32 | 33 | ## Technical Details 34 | 35 | Artygram's backend is built using Flask and frontend is built using React-Redux 36 | 37 | ## Features 38 | 39 | - Signup, Login and Demo a user 40 | - Post Image/Edit Image/Delete Image 41 | - Like/Unlike Posts 42 | - Follow/Unfollow Users 43 | - Photo Feed 44 | - User Profiles 45 | -------------------------------------------------------------------------------- /app/api/user_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify 2 | from flask_login import login_required, current_user 3 | from app.models import User 4 | from ..models.db import db 5 | 6 | 7 | user_routes = Blueprint('users', __name__) 8 | 9 | 10 | @user_routes.route('/all') 11 | @login_required 12 | def users(): 13 | users = User.query.all() 14 | return {'users': [user.to_dict() for user in users]} 15 | 16 | 17 | @user_routes.route('/') 18 | @login_required 19 | def user(id): 20 | user = User.query.get_or_404(id) 21 | 22 | return user.to_dict() 23 | 24 | 25 | @user_routes.route('//following') 26 | @login_required 27 | def following(id): 28 | user = User.query.get(id) 29 | return user.to_dict() 30 | 31 | 32 | @user_routes.route('//followers') 33 | @login_required 34 | def followers(id): 35 | user = User.query.get(id) 36 | return user.to_dict() 37 | 38 | 39 | @user_routes.route('//follow') 40 | @login_required 41 | def likeOnPost(id): 42 | loggedUser = current_user 43 | otherUser = User.query.get(id) 44 | 45 | # print('this is the post!!!!!!!!!!!', dir(post.postLikes)) 46 | # post.postLikes.append(int(user.id)) 47 | 48 | # post.postLikes is a list contains the User object. not the user.id 49 | # this is getting all the id in the post.postLikes. 50 | allUsersId = [user.id for user in loggedUser.follows] 51 | 52 | if otherUser.id in allUsersId: 53 | # have to remove the whole user object. 54 | loggedUser.follows.remove(otherUser) 55 | else: 56 | # this has to add the user object. instead of just the user.id 57 | loggedUser.follows.append(otherUser) 58 | 59 | db.session.commit() 60 | # print('this is the post!!!!!!!!!!!', post.postLikes) 61 | return {'loggedUser': loggedUser.to_dict(), 'otherUser': otherUser.to_dict()} 62 | 63 | -------------------------------------------------------------------------------- /migrations_old_version/versions/20210816_160610_comment_post.py: -------------------------------------------------------------------------------- 1 | """comment/post 2 | 3 | Revision ID: 25f84b89a7fd 4 | Revises: db418e582c48 5 | Create Date: 2021-08-16 16:06:10.882821 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '25f84b89a7fd' 14 | down_revision = 'db418e582c48' 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('posts', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('caption', sa.String(length=100), nullable=False), 24 | sa.Column('pic_url', sa.String(length=255), nullable=False), 25 | sa.Column('user_id', sa.Integer(), nullable=True), 26 | sa.Column('timestamp', sa.DateTime(), nullable=True), 27 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 28 | sa.PrimaryKeyConstraint('id') 29 | ) 30 | op.create_table('comments', 31 | sa.Column('id', sa.Integer(), nullable=False), 32 | sa.Column('caption', sa.String(length=255), nullable=False), 33 | sa.Column('post_id', sa.Integer(), nullable=True), 34 | sa.Column('user_id', sa.Integer(), nullable=True), 35 | sa.Column('timestamp', sa.DateTime(), nullable=True), 36 | sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ), 37 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 38 | sa.PrimaryKeyConstraint('id') 39 | ) 40 | op.create_table('likes', 41 | sa.Column('userId', sa.Integer(), nullable=True), 42 | sa.Column('postId', sa.Integer(), nullable=True), 43 | sa.Column('timestamp', sa.DateTime(), nullable=True), 44 | sa.ForeignKeyConstraint(['postId'], ['posts.id'], ), 45 | sa.ForeignKeyConstraint(['userId'], ['users.id'], ) 46 | ) 47 | # ### end Alembic commands ### 48 | 49 | 50 | def downgrade(): 51 | # ### commands auto generated by Alembic - please adjust! ### 52 | op.drop_table('likes') 53 | op.drop_table('comments') 54 | op.drop_table('posts') 55 | # ### end Alembic commands ### 56 | -------------------------------------------------------------------------------- /app/seeds/posts.py: -------------------------------------------------------------------------------- 1 | from app.models import db, Post 2 | 3 | 4 | # Adds a demo user, you can add other users here if you want 5 | def seed_posts(): 6 | post1 = Post(caption="dummy caption", pic_url="https://lh6.ggpht.com/HlgucZ0ylJAfZgusynnUwxNIgIp5htNhShF559x3dRXiuy_UdP3UQVLYW6c=s1200", user_id= 2 ) 7 | 8 | post2 = Post(caption="dummy caption", pic_url="https://www.tate.org.uk/art/images/work/N/N05/N05976_9.jpg", user_id=2) 9 | 10 | post3 = Post(caption="dummy caption", pic_url="https://www.homestratosphere.com/wp-content/uploads/2019/07/Cubism-art-833x1024.jpg", user_id= 1 ) 11 | 12 | post4 = Post(caption="dummy caption", pic_url="https://media.vanityfair.com/photos/5e8f9f875752fb00088317c4/16:9/w_1280,c_limit/The-Art-of-Making-Art-About-a-Plague.jpg", user_id= 1 ) 13 | 14 | post5 = Post(caption="dummy caption", pic_url="https://www.killyourdarlings.com.au/wp-content/uploads/2020/07/horse-1.jpg", user_id= 3 ) 15 | 16 | post6 = Post(caption="dummy caption", pic_url="https://images.artsonia.com/art/93030215.jpg", user_id= 3 ) 17 | 18 | post7= Post(caption="dummy caption", pic_url="https://media.timeout.com/images/105590782/750/422/image.jpg", user_id= 10 ) 19 | post8 = Post(caption="dummy caption", pic_url="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRR0CmwogoC8hHJaqWkdtG1K34toqYMgV84cw&usqp=CAU", user_id= 10 ) 20 | 21 | post9 = Post(caption="dummy caption", pic_url="https://d1zdxptf8tk3f9.cloudfront.net/ckeditor_assets/pictures/2528/content_mr-tt-628115-unsplash.jpg", user_id= 7 ) 22 | 23 | post10 = Post(caption="dummy caption", pic_url="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSe_ZcKVdiu1-7eLtbes7mnF5mNvACzbBSZhg&usqp=CAU", user_id= 7) 24 | 25 | db.session.add(post1) 26 | db.session.add(post2) 27 | db.session.add(post3) 28 | db.session.add(post4) 29 | db.session.add(post5) 30 | db.session.add(post6) 31 | db.session.add(post7) 32 | db.session.add(post8) 33 | db.session.add(post9) 34 | db.session.add(post10) 35 | 36 | db.session.commit() 37 | 38 | 39 | 40 | def undo_posts(): 41 | db.session.execute('TRUNCATE posts;') 42 | db.session.commit() -------------------------------------------------------------------------------- /react-app/src/components/PostDetails/postdetails.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .post-detail__container{ 4 | display: flex; 5 | flex-direction: column; 6 | margin-bottom: 25px; 7 | padding-bottom: 15px; 8 | width: 600px; 9 | box-sizing: border-box; 10 | border: 1px solid #dbdbdb; 11 | } 12 | 13 | .icon-username__container{ 14 | display: flex; 15 | flex-direction: row; 16 | align-items: center; 17 | padding: 10px; 18 | } 19 | 20 | .post-icon{ 21 | margin-right: 10px; 22 | border-radius: 60%; 23 | width: 50px; 24 | height: 50px; 25 | } 26 | 27 | .liked{ 28 | color: red; 29 | font-size: 20px; 30 | } 31 | 32 | .unliked{ 33 | font-size: 20px; 34 | } 35 | 36 | .fa-heart:hover{ 37 | cursor: pointer; 38 | } 39 | 40 | .likebutton{ 41 | border: 0px solid white; 42 | background-color: white; 43 | } 44 | 45 | .div-in-post{ 46 | padding: 9px; 47 | 48 | 49 | } 50 | 51 | .post-comment__div{ 52 | display: flex; 53 | flex-direction: row; 54 | justify-content: space-between; 55 | align-items: center; 56 | } 57 | 58 | 59 | .post-comment__button{ 60 | background-color: white; 61 | border: 0px; 62 | background-color: #0c8fe7; 63 | color: #f5f7f9; 64 | border-radius: 3px; 65 | font-size: 0.8rem; 66 | font-weight: bold; 67 | padding: 8px; 68 | } 69 | .post-comment__button:hover{ 70 | cursor: pointer; 71 | } 72 | 73 | .like-delete__container{ 74 | display: flex; 75 | flex-direction: row; 76 | align-items: center; 77 | } 78 | .caption-button__container{ 79 | display: flex; 80 | flex-direction: row; 81 | align-items: center; 82 | justify-content: space-between; 83 | } 84 | 85 | .edit-post{ 86 | font-size: 20px; 87 | } 88 | .edit-post:hover{ 89 | color: green; 90 | } 91 | .delete-post{ 92 | padding-left: 10px; 93 | font-size: 20px; 94 | } 95 | .delete-post:hover{ 96 | color:blue; 97 | } 98 | 99 | 100 | .timestamp-postdetails{ 101 | font-size: 13px; 102 | padding-top: 10px; 103 | color:grey; 104 | padding-left: 10px 105 | } 106 | 107 | .comment-msg{ 108 | color: grey; 109 | 110 | } -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, render_template, request, session, redirect 3 | from flask_cors import CORS 4 | from flask_migrate import Migrate 5 | from flask_wtf.csrf import CSRFProtect, generate_csrf 6 | from flask_login import LoginManager 7 | 8 | from .models import db, User 9 | from .api.user_routes import user_routes 10 | from .api.auth_routes import auth_routes 11 | from .api.post_routes import post_routes 12 | 13 | from .seeds import seed_commands 14 | 15 | from .config import Config 16 | 17 | app = Flask(__name__) 18 | app.config.from_object(Config) 19 | db.init_app(app) 20 | # Setup login manager 21 | login = LoginManager(app) 22 | login.login_view = 'auth.unauthorized' 23 | 24 | 25 | @login.user_loader 26 | def load_user(id): 27 | return User.query.get(int(id)) 28 | 29 | 30 | # Tell flask about our seed commands 31 | app.cli.add_command(seed_commands) 32 | 33 | app.config.from_object(Config) 34 | app.register_blueprint(user_routes, url_prefix='/api/users') 35 | app.register_blueprint(auth_routes, url_prefix='/api/auth') 36 | app.register_blueprint(post_routes, url_prefix='/api/posts') 37 | db.init_app(app) 38 | Migrate(app, db) 39 | 40 | # Application Security 41 | CORS(app) 42 | 43 | 44 | # Since we are deploying with Docker and Flask, 45 | # we won't be using a buildpack when we deploy to Heroku. 46 | # Therefore, we need to make sure that in production any 47 | # request made over http is redirected to https. 48 | # Well......... 49 | @app.before_request 50 | def https_redirect(): 51 | if os.environ.get('FLASK_ENV') == 'production': 52 | if request.headers.get('X-Forwarded-Proto') == 'http': 53 | url = request.url.replace('http://', 'https://', 1) 54 | code = 301 55 | return redirect(url, code=code) 56 | 57 | 58 | @app.after_request 59 | def inject_csrf_token(response): 60 | response.set_cookie( 61 | 'csrf_token', 62 | generate_csrf(), 63 | secure=True if os.environ.get('FLASK_ENV') == 'production' else False, 64 | samesite='Strict' if os.environ.get( 65 | 'FLASK_ENV') == 'production' else None, 66 | httponly=True) 67 | return response 68 | 69 | 70 | @app.route('/', defaults={'path': ''}) 71 | @app.route('/') 72 | def react_root(path): 73 | if path == 'favicon.ico': 74 | return app.send_static_file('favicon.ico') 75 | return app.send_static_file('index.html') 76 | -------------------------------------------------------------------------------- /react-app/src/components/NavBar.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import React from "react"; 3 | import { NavLink } from "react-router-dom"; 4 | import LogoutButton from "./auth/LogoutButton"; 5 | import "./Navbar.css"; 6 | 7 | const NavBar = ({ userdata, setAuthenticated }) => { 8 | const user = useSelector(state => state.session.user) 9 | 10 | // console.log(user?.profile_pic) 11 | let navbar_pfp; 12 | 13 | if (user?.profile_pic) { 14 | navbar_pfp = user?.profile_pic 15 | } 16 | else { 17 | navbar_pfp = "https://www.tenforums.com/geek/gars/images/2/types/thumb_15951118880user.png" 18 | } 19 | 20 | const artygram = "https://i.imgur.com/t3Mtt7E.png"; 21 | const addpic = "https://i.imgur.com/3yiJpcr.png"; 22 | 23 | 24 | // const settingsButton = (e) => { 25 | // e.preventDefault(); 26 | // history.push(`/users/${userdata.username}/edit`); 27 | // }; 28 | 29 | // const profileButton = (e) => { 30 | // e.preventDefault(); 31 | // history.push(`/users/${userdata.username}`); 32 | // }; 33 | 34 | 35 | return ( 36 |
    37 | 38 | {user ? 39 |
    40 | 41 |
    42 | 43 | {`artygram`} 44 | 45 |
    46 | 47 | {/*
    48 | 49 |
    */} 50 | 51 |
    52 |
    53 | 54 | {`addpic`} 55 | 56 |
    57 |
    58 | 59 | {user?.profile_pic === null || user?.profile_pic.includes("jpeg") || user?.profile_pic.includes("jpg") || user?.profile_pic.includes("png") || user?.profile_pic.includes("image") ? : } 60 | {/* {user.id} */} 61 | 62 |
    63 |
    64 | 65 |
    66 |
    67 | 68 |
    69 | : ''} 70 |
    71 | ); 72 | }; 73 | export default NavBar; 74 | -------------------------------------------------------------------------------- /react-app/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { BrowserRouter, Route, Switch } from 'react-router-dom'; 3 | import { useDispatch } 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/UserProfile/User'; 10 | import { authenticate } from './store/session'; 11 | import SplashPage from "./components/splash-page/SplashPage"; 12 | import HomePage from './components/Homepage'; 13 | import PostDetails from './components/PostDetails'; 14 | import PostForm from './components/PostForm'; 15 | import Following from './components/Following/following'; 16 | import Followers from './Followers/followers'; 17 | 18 | function App() { 19 | const [loaded, setLoaded] = useState(false); 20 | const [authenticated, setAuthenticated] = useState(false); 21 | const dispatch = useDispatch(); 22 | 23 | useEffect(() => { 24 | (async() => { 25 | await dispatch(authenticate()); 26 | setLoaded(true); 27 | })(); 28 | }, [dispatch]); 29 | 30 | if (!loaded) { 31 | return null; 32 | } 33 | 34 | return ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 49 | 50 | {/* 51 | 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 | export default App; 80 | -------------------------------------------------------------------------------- /app/models/user.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from werkzeug.security import generate_password_hash, check_password_hash 3 | from flask_login import UserMixin 4 | from datetime import datetime 5 | from .like import likes 6 | 7 | Followers = db.Table( 8 | "followers", 9 | db.Column("followerId", db.Integer, db.ForeignKey("users.id")), 10 | db.Column("followingId", db.Integer, db.ForeignKey("users.id")), 11 | db.Column("timestamp", db.DateTime, default=datetime.now) 12 | ) 13 | 14 | 15 | class User(db.Model, UserMixin): 16 | __tablename__ = 'users' 17 | 18 | id = db.Column(db.Integer, primary_key=True) 19 | username = db.Column(db.String(40), nullable=False, unique=True) 20 | email = db.Column(db.String(255), nullable=False, unique=True) 21 | bio = db.Column(db.String(255)) 22 | profile_pic = db.Column(db.String(255)) 23 | hashed_password = db.Column(db.String(255), nullable=False) 24 | 25 | follows = db.relationship( 26 | 'User', 27 | secondary=Followers, 28 | primaryjoin=(Followers.c.followerId == id), 29 | secondaryjoin=(Followers.c.followingId == id), 30 | backref=db.backref('follow_by', lazy='dynamic'), 31 | lazy='dynamic' 32 | ) 33 | 34 | # followers = db.relationship( 35 | # 'User', 36 | # secondary=followers, 37 | # primaryjoin=(followers.c.followingId == id), 38 | # secondaryjoin=(followers.c.followerId == id), 39 | # backref=db.backref('follows', lazy='dynamic'), 40 | # lazy='dynamic' 41 | # ) 42 | 43 | # follows = db.relationship('User', secondary=Followers, primaryjoin=(Followers.c.followerId == id),secondaryjoin=(Followers.c.followingId == id), back_populates='follow_by') 44 | # follow_by = db.relationship('User', secondary=Followers, primaryjoin=(Followers.c.followingId == id),secondaryjoin=(Followers.c.followerId == id), back_populates='follows') 45 | 46 | posts = db.relationship('Post', back_populates="users") 47 | comments = db.relationship('Comment', back_populates="users") 48 | userLikes = db.relationship("Post", secondary=likes, back_populates="postLikes") 49 | 50 | @property 51 | def password(self): 52 | return self.hashed_password 53 | 54 | @password.setter 55 | def password(self, password): 56 | self.hashed_password = generate_password_hash(password) 57 | 58 | def check_password(self, password): 59 | return check_password_hash(self.password, password) 60 | 61 | def to_dict(self): 62 | # print(self.followers) 63 | 64 | return { 65 | 'id': self.id, 66 | 'username': self.username, 67 | 'email': self.email, 68 | "bio": self.bio, 69 | "profile_pic": self.profile_pic, 70 | "follows": [{'id': user.id, 'username': user.username} for user in self.follows], 71 | "follow_by": [{'id': user.id, 'username': user.username} for user in self.follow_by] 72 | } 73 | -------------------------------------------------------------------------------- /react-app/src/components/auth/login-signup.css: -------------------------------------------------------------------------------- 1 | .login-form__container { 2 | margin: 100px auto 0px auto; 3 | padding: 30px 10px; 4 | 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | background-color: #ffffff; 10 | width: 400px; 11 | border: 1px solid #dbdbdb; 12 | border-radius: 3px; 13 | } 14 | 15 | .form-title { 16 | display: inline; 17 | font-weight: bold; 18 | font-size: 2.6rem; 19 | margin-bottom: 20px; 20 | margin-top: 10px; 21 | font-family: cursive; 22 | letter-spacing: 2px; 23 | } 24 | 25 | .sign-up__container { 26 | margin: 10px auto 0px auto; 27 | padding:20px 10px; 28 | display: flex; 29 | flex-direction: column; 30 | align-items: center; 31 | justify-content: center; 32 | background-color: #ffffff; 33 | width: 400px; 34 | border: 1px solid #dbdbdb; 35 | border-radius: 3px; 36 | } 37 | 38 | .login-form { 39 | display: flex; 40 | flex-direction: column; 41 | } 42 | 43 | .input__container { 44 | display: flex; 45 | flex-direction: column; 46 | margin-bottom: 10px; 47 | } 48 | 49 | .input { 50 | width: 300px; 51 | background-color: #fafafa; 52 | height: 33.33px; 53 | border-radius: 3px; 54 | border: 1px solid #dbdbdb; 55 | padding: 0 10px; 56 | color: #000; 57 | } 58 | 59 | .log-in-button { 60 | margin-top: 20px; 61 | background-color: #b2dffc; 62 | 63 | border: 0px; 64 | height: 33.33px; 65 | /* letter-spacing: 0.1rem; */ 66 | color: #f5f7f9; 67 | border-radius: 3px; 68 | font-size: 0.8rem; 69 | font-weight: bold; 70 | transition: background-color 0.5s; 71 | transition-timing-function: ease-in-out; 72 | } 73 | 74 | .log-in-button:hover { 75 | background-color: #0095f6; 76 | 77 | cursor: pointer; 78 | } 79 | 80 | .divider__container { 81 | width: 400px; 82 | margin-top: 20px; 83 | margin-bottom: -10px; 84 | } 85 | 86 | .divider { 87 | border-top: 1px solid #9ea5ad; 88 | display: block; 89 | line-height: 1px; 90 | 91 | margin: 15px 20px; 92 | position: relative; 93 | text-align: center; 94 | } 95 | 96 | .divider-title { 97 | background-color: #fff; 98 | font-size: 12px; 99 | letter-spacing: 1px; 100 | padding: 0 20px; 101 | color: #9ea5ad; 102 | text-transform: uppercase; 103 | } 104 | 105 | .demo-login__container { 106 | display: flex; 107 | flex-direction: column; 108 | } 109 | 110 | 111 | 112 | .sign-up-text { 113 | font-size: 0.9rem; 114 | } 115 | 116 | .sign-up-link { 117 | text-decoration: none; 118 | color: #0095f6; 119 | font-weight: 350; 120 | } 121 | 122 | .party { 123 | position: fixed; 124 | right: 0; 125 | bottom: 0; 126 | min-width: 100%; 127 | min-height: 100%; 128 | z-index: -1; 129 | } 130 | 131 | .errors{ 132 | 133 | padding-bottom: 10px; 134 | color:rgb(255, 93, 93); 135 | font-weight: bold; 136 | } 137 | -------------------------------------------------------------------------------- /react-app/src/components/PostForm/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { Redirect, useHistory } from "react-router-dom"; 4 | import { createPost } from "../../store/post"; 5 | import "./postForm.css"; 6 | import Particles from "react-particles-js"; 7 | 8 | const PostForm = () => { 9 | const [errors, setErrors] = useState([]); 10 | const [caption, setCaption] = useState(""); 11 | const [pic_url, setPic_Url] = useState(""); 12 | 13 | const user = useSelector((state) => state.session.user); 14 | 15 | const dispatch = useDispatch(); 16 | const history = useHistory(); 17 | 18 | if (!user) { 19 | return ; 20 | } 21 | 22 | const handleSubmit = async (e) => { 23 | e.preventDefault(); 24 | const post = await dispatch(createPost(caption, pic_url)); 25 | // console.log(post) 26 | if(post.id){ 27 | history.push("/"); 28 | } 29 | }; 30 | 31 | const cancel = () => { 32 | history.push("/"); 33 | }; 34 | 35 | return ( 36 |
    37 | 38 |

    Upload Post

    39 |
    40 |
    41 | {errors.map((error) => ( 42 |
  • {error}
  • 43 | ))} 44 |
    45 |
    46 | {/* */} 47 | setPic_Url(e.target.value)} 53 | value={pic_url} 54 | required 55 | > 56 |
    57 |
    58 | {/* */} 59 | 68 |
    69 | 72 |
    73 | 74 |
    75 | 78 |
    79 | 96 |
    97 | ); 98 | }; 99 | 100 | export default PostForm; 101 | -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from flask import current_app 7 | 8 | from alembic import context 9 | 10 | # this is the Alembic Config object, which provides 11 | # access to the values within the .ini file in use. 12 | config = context.config 13 | 14 | # Interpret the config file for Python logging. 15 | # This line sets up loggers basically. 16 | fileConfig(config.config_file_name) 17 | logger = logging.getLogger('alembic.env') 18 | 19 | # add your model's MetaData object here 20 | # for 'autogenerate' support 21 | # from myapp import mymodel 22 | # target_metadata = mymodel.Base.metadata 23 | config.set_main_option( 24 | 'sqlalchemy.url', 25 | str(current_app.extensions['migrate'].db.get_engine().url).replace( 26 | '%', '%%')) 27 | target_metadata = current_app.extensions['migrate'].db.metadata 28 | 29 | # other values from the config, defined by the needs of env.py, 30 | # can be acquired: 31 | # my_important_option = config.get_main_option("my_important_option") 32 | # ... etc. 33 | 34 | 35 | def run_migrations_offline(): 36 | """Run migrations in 'offline' mode. 37 | 38 | This configures the context with just a URL 39 | and not an Engine, though an Engine is acceptable 40 | here as well. By skipping the Engine creation 41 | we don't even need a DBAPI to be available. 42 | 43 | Calls to context.execute() here emit the given string to the 44 | script output. 45 | 46 | """ 47 | url = config.get_main_option("sqlalchemy.url") 48 | context.configure( 49 | url=url, target_metadata=target_metadata, literal_binds=True 50 | ) 51 | 52 | with context.begin_transaction(): 53 | context.run_migrations() 54 | 55 | 56 | def run_migrations_online(): 57 | """Run migrations in 'online' mode. 58 | 59 | In this scenario we need to create an Engine 60 | and associate a connection with the context. 61 | 62 | """ 63 | 64 | # this callback is used to prevent an auto-migration from being generated 65 | # when there are no changes to the schema 66 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 67 | def process_revision_directives(context, revision, directives): 68 | if getattr(config.cmd_opts, 'autogenerate', False): 69 | script = directives[0] 70 | if script.upgrade_ops.is_empty(): 71 | directives[:] = [] 72 | logger.info('No changes in schema detected.') 73 | 74 | connectable = current_app.extensions['migrate'].db.get_engine() 75 | 76 | with connectable.connect() as connection: 77 | context.configure( 78 | connection=connection, 79 | target_metadata=target_metadata, 80 | process_revision_directives=process_revision_directives, 81 | **current_app.extensions['migrate'].configure_args 82 | ) 83 | 84 | with context.begin_transaction(): 85 | context.run_migrations() 86 | 87 | 88 | if context.is_offline_mode(): 89 | run_migrations_offline() 90 | else: 91 | run_migrations_online() 92 | -------------------------------------------------------------------------------- /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'{field} : {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 | 45 | return user.to_dict() 46 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401 47 | 48 | 49 | @auth_routes.route('/signup', methods=['POST']) 50 | def sign_up(): 51 | """ 52 | Creates a new user and logs them in 53 | """ 54 | form = SignUpForm() 55 | form['csrf_token'].data = request.cookies['csrf_token'] 56 | if form.validate_on_submit(): 57 | user = User( 58 | username=form.data['username'], 59 | email=form.data['email'], 60 | password=form.data['password'], 61 | profile_pic=form.data['profile_pic'] 62 | ) 63 | db.session.add(user) 64 | db.session.commit() 65 | login_user(user) 66 | return user.to_dict() 67 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401 68 | 69 | 70 | @auth_routes.route('/logout') 71 | def logout(): 72 | """ 73 | Logs a user out 74 | """ 75 | logout_user() 76 | return {'message': 'User logged out'} 77 | 78 | 79 | @auth_routes.route('/demo', methods=['GET', 'POST']) 80 | def demo_login(): 81 | ''' 82 | Logs in demo user 83 | 84 | ''' 85 | demo_user = User.query.filter(User.email == 'demo@aa.io').first() 86 | login_user(demo_user) 87 | # {'id': 2, 'username': 'Demo', 'email': 'demo@aa.io', 'bio': 'I started painting as a hobby when I was little. I didn’t know I had any talent. I believe talent is just a pursued interest. Anybody can do what I do.', 'profile_pic': 'https://imgur.com/ckiJh7g'} 88 | return demo_user.to_dict() 89 | 90 | 91 | @auth_routes.route('/unauthorized') 92 | def unauthorized(): 93 | """ 94 | Returns unauthorized JSON when flask-login authentication fails 95 | """ 96 | return {'errors': ['Unauthorized']}, 401 97 | -------------------------------------------------------------------------------- /app/api/post_routes.py: -------------------------------------------------------------------------------- 1 | from ..models.post import Post 2 | from ..models.user import User 3 | from ..models.db import db 4 | from flask import Blueprint, request, jsonify 5 | from flask_login import current_user, login_required 6 | from ..forms.post_form import CreatePostForm 7 | from datetime import datetime 8 | from .auth_routes import validation_errors_to_error_messages 9 | 10 | post_routes = Blueprint('posts', __name__) 11 | 12 | 13 | @post_routes.route('/') 14 | @login_required 15 | def get_posts(): 16 | user = current_user 17 | following_ids = [following.id for following in user.follows] 18 | following_ids.append(user.id) 19 | posts = Post.query.filter(Post.user_id.in_( 20 | following_ids)).order_by(Post.timestamp.desc()).all() 21 | 22 | return {post.id: post.to_dict() for post in posts} 23 | # return {"Posts": {post.id: post.to_dict() for post in posts}} 24 | 25 | 26 | @post_routes.route('/all') 27 | def get_all_posts(): 28 | posts = Post.query.all() 29 | return {post.id: post.to_dict() for post in posts} 30 | 31 | 32 | @post_routes.route('/new', methods=['GET', 'POST']) 33 | @login_required 34 | def create_posts(): 35 | user = current_user 36 | form = CreatePostForm() 37 | form['csrf_token'].data = request.cookies['csrf_token'] 38 | if form.validate_on_submit(): 39 | data = form.data 40 | new_post = Post( 41 | user_id=user.id, 42 | caption=data['caption'], 43 | pic_url=data['pic_url'], 44 | timestamp=datetime.now() 45 | ) 46 | 47 | db.session.add(new_post) 48 | db.session.commit() 49 | return new_post.to_dict() 50 | 51 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401 52 | 53 | 54 | @post_routes.route('/', methods=['PUT']) 55 | @login_required 56 | def update_caption(id): 57 | post = Post.query.get(id) 58 | form = CreatePostForm() 59 | 60 | form['csrf_token'].data = request.cookies['csrf_token'] 61 | if form.validate_on_submit(): 62 | data = form.data 63 | post.caption = data['caption'] 64 | 65 | db.session.commit() 66 | return post.to_dict() 67 | 68 | 69 | @post_routes.route('/', methods=["DELETE"]) 70 | @login_required 71 | def delete_post(id): 72 | post = Post.query.get(id) 73 | db.session.delete(post) 74 | db.session.commit() 75 | 76 | return jsonify("Delete successful") 77 | 78 | 79 | @post_routes.route('//like', methods=['PUT']) 80 | @login_required 81 | def likeOnPost(id): 82 | user = current_user 83 | post = Post.query.get(id) 84 | 85 | # post.postLikes.append(int(user.id)) 86 | 87 | # post.postLikes is a list contains the User object. not the user.id 88 | # this is getting all the id in the post.postLikes. 89 | allUsersId = [user.id for user in post.postLikes] 90 | 91 | if user.id in allUsersId: 92 | # have to remove the whole user object. 93 | post.postLikes.remove(user) 94 | else: 95 | # this has to add the user object. instead of just the user.id 96 | post.postLikes.append(user) 97 | 98 | db.session.commit() 99 | # print('this is the post!!!!!!!!!!!', post.postLikes) 100 | return post.to_dict() 101 | -------------------------------------------------------------------------------- /setup.md: -------------------------------------------------------------------------------- 1 | # Flask React Project 2 | This is the starter for the Flask React project. 3 | 4 | ## Getting started 5 | 1. Clone this repository (only this branch) 6 | 7 | git clone https://github.com/appacademy-starters/python-project-starter.git 8 | 2. Install dependencies 9 | 10 | pipenv install --dev -r dev-requirements.txt && pipenv install -r requirements.txt 11 | 3. Create a .env file based on the example with proper settings for your development environment 12 | 13 | 4. Setup your PostgreSQL user, password and database and make sure it matches your .env file 14 | 15 | 5. Get into your pipenv, migrate your database, seed your database, and run your flask app 16 | 17 | pipenv shell 18 | flask db upgrade 19 | flask seed all 20 | flask run 21 | 22 | 6. To run the React App in development, checkout the README inside the react-app directory. 23 | 24 | IMPORTANT! If you add any python dependencies to your pipfiles, you'll need to regenerate your requirements.txt before deployment. You can do this by running: 25 | 26 | pipenv lock -r > requirements.txt 27 | ALSO IMPORTANT! psycopg2-binary MUST remain a dev dependency because you can't install it on apline-linux. There is a layer in the Dockerfile that will install psycopg2 (not binary) for us. 28 | 29 | ## Deploy to Heroku 30 | 1. Before you deploy, don't forget to run the following command in order to ensure that your production environment has all of your up-to-date dependencies. You only have to run this command when you have installed new Python packages since your last deployment, but if you aren't sure, it won't hurt to run it again. 31 | 32 | pipenv lock -r > requirements.txt 33 | 34 | 2. Create a new project on Heroku 35 | 36 | 3. Under Resources click "Find more add-ons" and add the add on called "Heroku Postgres" 37 | 38 | 4. Install the Heroku CLI 39 | 40 | 5. Run 41 | 42 | heroku login 43 | Login to the heroku container registry 44 | 45 | heroku container:login 46 | 7. Update the REACT_APP_BASE_URL variable in the Dockerfile. This should be the full URL of your Heroku app: i.e. "https://flask-react-aa.herokuapp.com" 47 | 48 | 8. Push your docker container to heroku from the root directory of your project. (If you are using an M1 mac, follow these steps below instead, then continue on to step 9.) This will build the Dockerfile and push the image to your heroku container registry. 49 | 50 | heroku container:push web -a {NAME_OF_HEROKU_APP} 51 | 9. Release your docker container to heroku 52 | 53 | heroku container:release web -a {NAME_OF_HEROKU_APP} 54 | 10. set up your database 55 | 56 | heroku run -a {NAME_OF_HEROKU_APP} flask db upgrade 57 | heroku run -a {NAME_OF_HEROKU_APP} flask seed all 58 | 11 .Under Settings find "Config Vars" and add any additional/secret .env variables. 59 | 60 | 12. profit 61 | 62 | ## For M1 Mac users 63 | (Replaces Step 8) 64 | 65 | 1. Build image with linux platform for heroku servers. Replace {NAME_OF_HEROKU_APP} with your own tag: 66 | 67 | docker buildx build --platform linux/amd64 -t {NAME_OF_HEROKU_APP} . 68 | 2. Tag your app with the url for your apps registry. Make sure to use the name of your Heroku app in the url and tag name: 69 | 70 | docker tag {NAME_OF_HEROKU_APP} registry.heroku.com/{NAME_OF_HEROKU_APP}/web 71 | 3. Use docker to push the image to the Heroku container registry: 72 | 73 | docker push registry.heroku.com/{NAME_OF_HEROKU_APP}/web 74 | -------------------------------------------------------------------------------- /migrations_old_version/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 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | fileConfig(config.config_file_name) 18 | logger = logging.getLogger('alembic.env') 19 | 20 | # add your model's MetaData object here 21 | # for 'autogenerate' support 22 | # from myapp import mymodel 23 | # target_metadata = mymodel.Base.metadata 24 | from flask import current_app 25 | config.set_main_option( 26 | 'sqlalchemy.url', 27 | str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%')) 28 | target_metadata = current_app.extensions['migrate'].db.metadata 29 | 30 | # other values from the config, defined by the needs of env.py, 31 | # can be acquired: 32 | # my_important_option = config.get_main_option("my_important_option") 33 | # ... etc. 34 | 35 | 36 | def run_migrations_offline(): 37 | """Run migrations in 'offline' mode. 38 | 39 | This configures the context with just a URL 40 | and not an Engine, though an Engine is acceptable 41 | here as well. By skipping the Engine creation 42 | we don't even need a DBAPI to be available. 43 | 44 | Calls to context.execute() here emit the given string to the 45 | script output. 46 | 47 | """ 48 | url = config.get_main_option("sqlalchemy.url") 49 | context.configure( 50 | url=url, target_metadata=target_metadata, literal_binds=True, 51 | compare_type=True 52 | ) 53 | 54 | with context.begin_transaction(): 55 | context.run_migrations() 56 | 57 | 58 | def run_migrations_online(): 59 | """Run migrations in 'online' mode. 60 | 61 | In this scenario we need to create an Engine 62 | and associate a connection with the context. 63 | 64 | """ 65 | 66 | # this callback is used to prevent an auto-migration from being generated 67 | # when there are no changes to the schema 68 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 69 | def process_revision_directives(context, revision, directives): 70 | if getattr(config.cmd_opts, 'autogenerate', False): 71 | script = directives[0] 72 | if script.upgrade_ops.is_empty(): 73 | directives[:] = [] 74 | logger.info('No changes in schema detected.') 75 | 76 | connectable = engine_from_config( 77 | config.get_section(config.config_ini_section), 78 | prefix='sqlalchemy.', 79 | poolclass=pool.NullPool, 80 | ) 81 | 82 | with connectable.connect() as connection: 83 | context.configure( 84 | compare_type=True, 85 | connection=connection, 86 | target_metadata=target_metadata, 87 | process_revision_directives=process_revision_directives, 88 | **current_app.extensions['migrate'].configure_args 89 | ) 90 | 91 | with context.begin_transaction(): 92 | context.run_migrations() 93 | 94 | 95 | if context.is_offline_mode(): 96 | run_migrations_offline() 97 | else: 98 | run_migrations_online() 99 | -------------------------------------------------------------------------------- /react-app/src/components/UserProfile/user.css: -------------------------------------------------------------------------------- 1 | 2 | .userPageBody { 3 | display: flex; 4 | padding-top: 100px; 5 | flex-direction: column; 6 | justify-content: center; 7 | margin: 0px 10%; 8 | } 9 | 10 | .userDetails { 11 | display: flex; 12 | flex-direction: row; 13 | justify-content: center; 14 | } 15 | .userProfilePic { 16 | border-radius: 50%; 17 | width: 200px; 18 | height: 200px; 19 | margin-right: 100px; 20 | 21 | } 22 | 23 | .postFollowerFollowing { 24 | display: flex; 25 | flex-direction: row; 26 | } 27 | 28 | .userPhoto__container{ 29 | max-width: 100%; 30 | justify-content: center; 31 | padding: 0 10%; 32 | } 33 | 34 | 35 | .userPhotoFeed { 36 | display: flex; 37 | flex-direction: row; 38 | justify-content: center; 39 | flex-wrap: wrap; 40 | box-sizing: border-box; 41 | margin-bottom: 50px; 42 | } 43 | 44 | .borderContainer { 45 | display: flex; 46 | justify-content: center; 47 | margin-bottom: 50px; 48 | margin-top: 40px; 49 | } 50 | 51 | .border { 52 | width: 1150px; 53 | border-top: 1px solid rgba(128, 128, 128, 0.384); 54 | } 55 | 56 | .userPostPhoto { 57 | width: 300px; 58 | height: 300px; 59 | /* padding: 15px; */ 60 | box-sizing: border-box; 61 | border-radius: 10px; 62 | margin: 10px; 63 | object-fit: cover; 64 | } 65 | .userPostPhoto:hover{ 66 | opacity: 0.8; 67 | } 68 | 69 | .post_followers_following { 70 | display: flex; 71 | flex-direction: row; 72 | padding-right: 20px; 73 | padding-left: 20px; 74 | width: 500px; 75 | 76 | } 77 | .numberPost { 78 | 79 | font-size: 20px; 80 | } 81 | 82 | .followers { 83 | color: black; 84 | padding-right: 20px; 85 | padding-left: 20px; 86 | text-decoration: none; 87 | font-size: 20px; 88 | } 89 | 90 | .following { 91 | color: black; 92 | padding-right: 20px; 93 | padding-left: 20px; 94 | text-decoration: none; 95 | font-size: 20px; 96 | } 97 | /* 98 | a:link { 99 | color: black; 100 | padding-right: 20px; 101 | padding-left: 20px; 102 | text-decoration: none; 103 | } */ 104 | 105 | .userName { 106 | font-size: xx-large; 107 | margin-bottom: 40px; 108 | font-family:Arial, Helvetica, sans-serif 109 | } 110 | 111 | .userBio { 112 | width: 500px; 113 | overflow-wrap: break-word; 114 | font-size: large; 115 | margin-top: 40px; 116 | font-family:Arial, Helvetica, sans-serif; 117 | } 118 | 119 | .post_followers_following { 120 | /* width: 300px; */ 121 | padding-left: 0px; 122 | display: flex; 123 | justify-content: space-between; 124 | } 125 | 126 | .userNameAndButton { 127 | display: flex; 128 | flex-direction: row; 129 | } 130 | 131 | .button { 132 | margin-top: 0px; 133 | margin-left: 30px; 134 | background-color: white; 135 | text-align: center; 136 | font-size: 17px; 137 | height: 35px; 138 | width: 90px; 139 | border-color: rgba(199, 197, 197, 0.384); 140 | font-family:Arial, Helvetica, sans-serif; 141 | } 142 | 143 | .addPic{ 144 | width: 150px; 145 | height: 100px; 146 | 147 | } 148 | 149 | .addNewPost { 150 | margin-left: 100%; 151 | } 152 | -------------------------------------------------------------------------------- /react-app/src/components/auth/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { NavLink, Redirect } from 'react-router-dom'; 4 | import { login } from '../../store/session'; 5 | import DemoLogin from './DemoLogin'; 6 | import "./login-signup.css"; 7 | import Particles from "react-particles-js"; 8 | 9 | 10 | const LoginForm = () => { 11 | const [errors, setErrors] = useState([]); 12 | const [email, setEmail] = useState(''); 13 | const [password, setPassword] = useState(''); 14 | const user = useSelector(state => state.session.user); 15 | const dispatch = useDispatch(); 16 | 17 | const onLogin = async (e) => { 18 | e.preventDefault(); 19 | const data = await dispatch(login(email, password)); 20 | if (data) { 21 | // console.log(data) 22 | setErrors(['Incorrect email or password']); 23 | } 24 | }; 25 | 26 | const updateEmail = (e) => { 27 | setEmail(e.target.value); 28 | }; 29 | 30 | const updatePassword = (e) => { 31 | setPassword(e.target.value); 32 | }; 33 | 34 | if (user) { 35 | return ; 36 | } 37 | 38 | return ( 39 | <> 40 |
    41 | 42 |

    Artygram

    43 |
    44 |
    45 |
    46 | {errors.map((error) => ( 47 |
    {error}
    48 | ))} 49 |
    50 |
    51 | 52 |
    53 | 61 |
    62 |
    63 | 71 | 74 |
    75 |
    76 |
    77 |
    78 | OR 79 |
    80 |
    81 |
    82 | 83 |
    84 |
    85 |
    86 |

    87 | Don't have an account?{" "} 88 | 89 | Sign up 90 | 91 |

    92 |
    93 | 110 | 111 | ); 112 | }; 113 | 114 | export default LoginForm; 115 | -------------------------------------------------------------------------------- /react-app/src/components/Homepage/index.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | import { useSelector, useDispatch } from "react-redux" 3 | import { Link } from "react-router-dom" 4 | import { getAllPosts, likeOnePost } from "../../store/post" 5 | import { getAllFollowing } from "../../store/session" 6 | import { getAllUsers } from "../../store/user" 7 | import './homepage.css' 8 | 9 | 10 | const HomePage = () => { 11 | const user = useSelector(state => state.session.user) 12 | const posts = useSelector(state => Object.values(state.posts)) 13 | const allUsers = useSelector(state => state.users) 14 | const dispatch = useDispatch(); 15 | 16 | const [getPost, setPost] = useState(false); 17 | 18 | const Ids = user.follows.map(user => user.id) 19 | Ids.push(user.id) 20 | const filtered = posts.filter((post) => Ids.includes(post.user_id)) 21 | 22 | const sortedPosts = filtered.reverse() 23 | 24 | let array = [] 25 | function randomUserPosts(min, max) { 26 | array.push(Math.floor(Math.random() * max) + min) 27 | array.push(Math.floor(Math.random() * max) + min) 28 | array.push(Math.floor(Math.random() * max) + min) 29 | // console.log(array) 30 | } 31 | 32 | useEffect(() => { 33 | dispatch(getAllPosts()) 34 | dispatch(getAllUsers()) 35 | dispatch(getAllFollowing(user.id)) 36 | 37 | }, [dispatch]) 38 | 39 | useEffect(() => { 40 | if (getPost) { 41 | dispatch(likeOnePost(getPost)) 42 | // dispatch(getAllPosts()) 43 | } 44 | }, [getPost, dispatch]) 45 | 46 | randomUserPosts(1, allUsers.users?.length) 47 | 48 | 49 | const filtered2 = posts.filter((post) => array.includes(post.user_id)) 50 | const sortedPosts2 = filtered2.reverse() 51 | 52 | let conditionalPosts 53 | user.follows?.length !== 0 ? conditionalPosts = sortedPosts : conditionalPosts = sortedPosts2 54 | 55 | 56 | if (array.length === 0) { 57 | return null 58 | } 59 | 60 | return ( 61 |
    62 | {conditionalPosts?.map((post) => ( 63 |
    64 |
    65 | 66 | {[post.id]} 72 | 73 | 74 | {post.user?.username} 75 | 76 |
    77 |
    78 | 79 | {`img-${post.id}`} 84 | 85 |
    86 |
    87 | {post.postlikes.includes(user.id) ? ( 88 | 91 | ) : ( 92 | 95 | )} 96 |
    97 |
    98 |
    likes: {post.likesnum}
    99 |
    {post.caption}
    100 | {/*
    comments: {post.commentsnum}
    */} 101 |
    {post.timestamp}
    102 |
    103 |
    104 | ))} 105 |
    106 | ); 107 | } 108 | 109 | export default HomePage 110 | -------------------------------------------------------------------------------- /react-app/src/store/session.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // constants 4 | const SET_USER = 'session/SET_USER'; 5 | const REMOVE_USER = 'session/REMOVE_USER'; 6 | const DEMO_LOGIN = 'session/demoLogin'; 7 | const GET_FOLLOWING = 'followers/GET_FOLLOWING' 8 | 9 | 10 | const setUser = (user) => ({ 11 | type: SET_USER, 12 | payload: user 13 | }); 14 | 15 | const removeUser = () => ({ 16 | type: REMOVE_USER, 17 | }) 18 | 19 | const demoLogin = (demoUser) => { 20 | return { 21 | type: DEMO_LOGIN, 22 | payload: demoUser 23 | } 24 | } 25 | 26 | const getFollowing = (user) => ({ 27 | type: GET_FOLLOWING, 28 | payload: user 29 | }) 30 | 31 | 32 | const initialState = { user: null }; 33 | 34 | export const authenticate = () => async (dispatch) => { 35 | const response = await fetch('/api/auth/', { 36 | headers: { 37 | 'Content-Type': 'application/json' 38 | } 39 | }); 40 | if (response.ok) { 41 | const data = await response.json(); 42 | if (data.errors) { 43 | return; 44 | } 45 | 46 | dispatch(setUser(data)); 47 | } 48 | } 49 | 50 | export const login = (email, password) => async (dispatch) => { 51 | const response = await fetch('/api/auth/login', { 52 | method: 'POST', 53 | headers: { 54 | 'Content-Type': 'application/json' 55 | }, 56 | body: JSON.stringify({ 57 | email, 58 | password 59 | }) 60 | }); 61 | 62 | 63 | if (response.ok) { 64 | const data = await response.json(); 65 | dispatch(setUser(data)) 66 | return null; 67 | } else if (response.status < 500) { 68 | const data = await response.json(); 69 | if (data.errors) { 70 | return data.errors; 71 | } 72 | } else { 73 | return ['An error occurred. Please try again.'] 74 | } 75 | 76 | } 77 | 78 | export const logout = () => async (dispatch) => { 79 | const response = await fetch('/api/auth/logout', { 80 | headers: { 81 | 'Content-Type': 'application/json', 82 | } 83 | }); 84 | 85 | if (response.ok) { 86 | dispatch(removeUser()); 87 | } 88 | }; 89 | 90 | 91 | export const signUp = (username, email, password, profile_pic) => async (dispatch) => { 92 | const response = await fetch('/api/auth/signup', { 93 | method: 'POST', 94 | headers: { 95 | 'Content-Type': 'application/json', 96 | }, 97 | body: JSON.stringify({ 98 | username, 99 | email, 100 | password, 101 | profile_pic 102 | }), 103 | }); 104 | 105 | if (response.ok) { 106 | const data = await response.json(); 107 | dispatch(setUser(data)) 108 | return null; 109 | } else if (response.status < 500) { 110 | const data = await response.json(); 111 | if (data.errors) { 112 | return data.errors; 113 | } 114 | } else { 115 | return ['An error occurred. Please try again.'] 116 | } 117 | } 118 | 119 | export const loginDemo = () => async (dispatch) => { 120 | 121 | const response = await fetch('/api/auth/demo', { 122 | method: 'POST', 123 | }); 124 | 125 | if (response.ok) { 126 | const data = await response.json() 127 | dispatch(demoLogin(data)); 128 | return response; 129 | } 130 | } 131 | 132 | export const getAllFollowing = (id) => async (dispatch) => { 133 | const response = await fetch(`/api/users/${id}/following`); 134 | 135 | if (response.ok) { 136 | const user = await response.json(); 137 | dispatch(setUser(user)); 138 | } 139 | }; 140 | 141 | 142 | 143 | 144 | 145 | export default function reducer(state = initialState, action) { 146 | switch (action.type) { 147 | case SET_USER: 148 | return { user: action.payload } 149 | case REMOVE_USER: 150 | return { user: null } 151 | case DEMO_LOGIN: 152 | return { user: action.payload } 153 | // case GET_FOLLOWING : { 154 | // return {user: action.payload} 155 | // } 156 | default: 157 | return state; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /app/seeds/users.py: -------------------------------------------------------------------------------- 1 | from app.models import db, User 2 | 3 | 4 | # Adds a demo user, you can add other users here if you want 5 | def seed_users(): 6 | user1 = User( 7 | username='thearts', email='streetarts@aa.io', password='password', profile_pic='https://i.imgur.com/mniUYaK.png', bio="my art is dedicated to those with street smarts (;") 8 | user2 = User( 9 | username='lemalooma', email='lema@aa.io', password='password', profile_pic='https://i.imgur.com/7TYdoF2.png', bio="Art is a line around your thoughts. ― Gustav Klimt") 10 | user3 = User( 11 | username='diana', email='diana@aa.io', password='password', profile_pic='https://i.imgur.com/3JU5Kuc.png', bio="Midnight snacker 🌝") 12 | user4 = User( 13 | username='kyle', email='kyle@aa.io', password='password', profile_pic='https://i.imgur.com/sWy6Lnw.png', bio="It's so fine and yet so terrible to stand in front of a blank canvas.") 14 | user5 = User( 15 | username='zhuoxin', email='zhuoxin@aa.io', password='password', profile_pic='https://i.imgur.com/z8CiLIY.png', bio="The painter has the Universe in his mind and hands.") 16 | user6 = User( 17 | username='alexandertheartist', email='alexander@aa.io', password='password', profile_pic='https://i.imgur.com/SRTRK7S.png', bio="If people only knew how hard I work to gain my mastery. It wouldn't seem so wonderful at all.") 18 | user7 = User( 19 | username='flulemaity', email='flulematiy@aa.io', password='password', profile_pic='https://i.imgur.com/j6hXb7F.png', bio="The world always seems brighter when you’ve just made something that wasn’t there before.") 20 | user8 = User( 21 | username='dollythedancer', email='dolly@aa.io', password='password', profile_pic='https://i.imgur.com/euqVH5z.png', bio="While dancing I discover what I really want to say.") 22 | user9 = User( 23 | username='art_thou', email='harry@aa.io', password='password', profile_pic='https://i.imgur.com/iCNi0A9.png', bio="Art is never finished, only abandoned.") 24 | user10 = User( 25 | username='mervstheword', email='merv@aa.io', password='password', profile_pic='https://i.imgur.com/oZUjm7O.png', bio="Life is sometimes hard. Things go wrong, in life and in love and in business and in friendship and in health and in all other ways that life can go wrong. And when things get tough, this is what you should do. Make good art.") 26 | user11 = User( 27 | username='artgod', email='artgod@aa.io', password='password', profile_pic='https://i.imgur.com/wLdTcGQ.png', bio="Creativity is intelligence having fun.") 28 | user12 = User( 29 | username='vanDough', email='vadough@aa.io', password='password', profile_pic='https://i.imgur.com/oZUjm7O.png', bio="Learn the rules like a pro, so you can break them like an artist.") 30 | user13 = User( 31 | username='callen_shaub', email='callen@aa.io', password='password', profile_pic='https://i.imgur.com/tVZPwDr.png', bio="There is no must in art because art is free.") 32 | user14 = User( 33 | username='oil_paintings', email='oil@aa.io', password='password', profile_pic='https://i.imgur.com/7TYdoF2.png', bio="Artists are just children who refuse to put down their crayons.") 34 | user15 = User( 35 | username='marvelousmonet', email='monet@aa.io', password='password', profile_pic='https://i.imgur.com/3JU5Kuc.png', bio="A true artist is not one who is inspired, but one who inspires others.") 36 | demo = User( 37 | username='Demo', email='demo@aa.io', password='password', profile_pic='https://i.imgur.com/sWy6Lnw.png', bio='I started painting as a hobby when I was little. I didn’t know I had any talent. I believe talent is just a pursued interest. Anybody can do what I do.', follows=[user1, user2, user3, user10, user7] ) 38 | 39 | db.session.add(user1) 40 | db.session.add(user2) 41 | db.session.add(user3) 42 | db.session.add(user4) 43 | db.session.add(user5) 44 | db.session.add(user6) 45 | db.session.add(user7) 46 | db.session.add(user8) 47 | db.session.add(user9) 48 | db.session.add(user10) 49 | db.session.add(user11) 50 | db.session.add(user12) 51 | db.session.add(user13) 52 | db.session.add(user14) 53 | db.session.add(user15) 54 | db.session.add(demo) 55 | 56 | 57 | db.session.commit() 58 | 59 | 60 | # Uses a raw SQL query to TRUNCATE the users table. 61 | # SQLAlchemy doesn't have a built in function to do this 62 | # TRUNCATE Removes all the data from the table, and RESET IDENTITY 63 | # resets the auto incrementing primary key, CASCADE deletes any 64 | # dependent entities 65 | def undo_users(): 66 | db.session.execute('TRUNCATE users RESTART IDENTITY CASCADE;') 67 | db.session.commit() 68 | -------------------------------------------------------------------------------- /react-app/src/store/post.js: -------------------------------------------------------------------------------- 1 | 2 | const GET_POSTS = 'posts/GET_POSTS' 3 | const DELETE_POST = 'post/DELETE_POST' 4 | const CREATE_POST = 'posts/CREATE_POST' 5 | const EDIT_CAPTION = 'posts/EDIT_CAPTION' 6 | const LIKE_POST = 'posts/LIKE_POST' 7 | const ALL_POSTS = 'posts/ALL_POSTS' 8 | 9 | const allPosts = (posts) => ({ 10 | type: ALL_POSTS, 11 | posts 12 | }) 13 | 14 | const getPosts = (posts) => ({ 15 | type: GET_POSTS, 16 | posts 17 | }) 18 | 19 | const deletePost = id => ({ 20 | type: DELETE_POST, 21 | id 22 | }) 23 | 24 | const newPost = (post) => ({ 25 | type: CREATE_POST, 26 | payload: post 27 | }) 28 | 29 | const editCaption = (post) => ({ 30 | type: EDIT_CAPTION, 31 | post 32 | }) 33 | 34 | const likePost = (post) => ({ 35 | type: LIKE_POST, 36 | post, 37 | }); 38 | 39 | 40 | export const getLoginPosts = () => async dispatch => { 41 | const req = await fetch(`/api/posts/`); 42 | if (req.ok) { 43 | const posts = await req.json(); 44 | dispatch(getPosts(posts)) 45 | } 46 | return req 47 | } 48 | 49 | export const getAllPosts = () => async dispatch => { 50 | const response = await fetch('/api/posts/all'); 51 | if(response.ok){ 52 | const posts = await response.json(); 53 | dispatch(allPosts(posts)) 54 | } 55 | return response 56 | } 57 | 58 | 59 | export const updateCaption = (post) => async dispatch => { 60 | const { id } = post 61 | const res = await fetch(`/api/posts/${id}`, { 62 | method: 'PUT', 63 | headers: { 'Content-Type': 'application/json' }, 64 | body: JSON.stringify(post) 65 | }) 66 | if (res.ok) { 67 | const editPost = await res.json(); 68 | dispatch(editCaption(editPost)) 69 | return editPost 70 | } 71 | } 72 | 73 | export const deleteOnePost = (id) => async dispatch => { 74 | const res = await fetch(`/api/posts/${id}`, { 75 | method: 'DELETE' 76 | }) 77 | 78 | 79 | if (res.ok) { 80 | const deleted = await res.json() 81 | dispatch(deletePost(id)) 82 | return deleted 83 | } 84 | } 85 | 86 | export const createPost = (caption, pic_url) => async dispatch => { 87 | 88 | const req = await fetch(`/api/posts/new`, { 89 | method: 'POST', 90 | headers: { 91 | 'Content-Type': 'application/json' 92 | }, 93 | body: JSON.stringify({ 94 | caption, pic_url 95 | }) 96 | }); 97 | 98 | if (req.ok) { 99 | const data = await req.json(); 100 | dispatch(newPost(data)) 101 | return data 102 | } else if (req.status < 500) { 103 | const data = await req.json(); 104 | if (data.errors) { 105 | return data.errors; 106 | } 107 | } else { 108 | return ['An error occurred. Please try again.'] 109 | } 110 | } 111 | // export const likePost = async (id) => { 112 | // let response = await fetch(`/api/posts/${id}`, { 113 | // method: "POST", 114 | // }); 115 | // if(response.ok) { 116 | // const res = await response.json(); 117 | // dispatch(likePost(res)) 118 | // return res 119 | // } 120 | 121 | 122 | // }; 123 | 124 | export const likeOnePost = (post) => async dispatch => { 125 | const { id } = post 126 | 127 | let response = await fetch(`/api/posts/${id}/like`, { 128 | method: "PUT", 129 | headers: {'Content-Type': 'application/json'}, 130 | body: JSON.stringify(post) 131 | }); 132 | if (response.ok) { 133 | const res = await response.json(); 134 | dispatch(likePost(res)) 135 | return res 136 | } 137 | 138 | } 139 | 140 | const initialState = {}; 141 | 142 | export default function posts(state = initialState, action) { 143 | switch (action.type) { 144 | case GET_POSTS: { 145 | 146 | return { ...state, ...action.posts } 147 | } 148 | case ALL_POSTS: { 149 | return {...state, ...action.posts} 150 | } 151 | 152 | case DELETE_POST: { 153 | let afterState = { ...state } 154 | 155 | delete afterState[action.id] 156 | return afterState 157 | 158 | } 159 | case EDIT_CAPTION: { 160 | const updatedState = { ...state, [action.post.id]: action.post } 161 | 162 | return updatedState 163 | } 164 | 165 | case CREATE_POST: { 166 | const newState = { ...state } 167 | return newState 168 | } 169 | case LIKE_POST: { 170 | const newState = { ...state, [action.post.id]: action.post }; 171 | return newState; 172 | } 173 | default: 174 | return state; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /react-app/src/components/PostDetails/index.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | 4 | import { getLoginPosts, likeOnePost } from "../../store/post"; 5 | 6 | 7 | import { useParams, useHistory, Link } from "react-router-dom"; 8 | import { deleteOnePost } from "../../store/post"; 9 | import EditCaption from "../EditCaption"; 10 | import './postdetails.css' 11 | 12 | const PostDetails = () => { 13 | const { postId } = useParams(); 14 | 15 | const dispatch = useDispatch(); 16 | const history = useHistory(); 17 | 18 | 19 | const posts = useSelector((state) => Object.values(state.posts)); 20 | const user = useSelector((state) => state.session.user); 21 | const [showEditCaption, setShowEditCaption] = useState(null) 22 | 23 | 24 | 25 | useEffect(() => { 26 | dispatch(getLoginPosts()); 27 | }, [dispatch]); 28 | 29 | 30 | 31 | const post = posts?.find((post) => post.id === +postId); 32 | 33 | const handleDelete = async () => { 34 | let swo = await dispatch(deleteOnePost(postId)) 35 | if (swo) { 36 | history.push('/') 37 | } 38 | } 39 | 40 | const likePostDetail = async () => { 41 | await dispatch(likeOnePost(post)) 42 | 43 | } 44 | 45 | 46 | let edit = null; 47 | 48 | if (showEditCaption) { 49 | edit = ( 50 | setShowEditCaption(null)} /> 51 | ) 52 | } 53 | 54 | const handleComment = () => { 55 | alert('comment feature is comming soon!') 56 | } 57 | 58 | 59 | return ( 60 |
    61 | {post ? ( 62 |
    63 |
    64 | 65 | {post.id} 71 | 72 | 73 | {post.user?.username} 74 | 75 |
    76 | 77 |
    78 | {`img-${post.id}`} 83 |
    84 | 85 |
    86 | {post.postlikes.includes(user.id) ? ( 87 |
    88 | likePostDetail(post)} 90 | className="fas liked fa-heart" 91 | > 92 |
    93 | ) : ( 94 |
    95 | likePostDetail(post)} 97 | className="far unliked fa-heart" 98 | > 99 |
    100 | )} 101 | 107 |
    108 | 109 |
    likes: {post.likesnum}
    110 | 111 |
    112 |
    113 | {post.user.username} {post.caption} 114 |
    115 | 116 | 125 |
    126 | 127 | {showEditCaption ? edit : ""} 128 | 129 | {/*
    comments: {post.commentsnum}
    */} 130 | 131 | {/*
    {post.timestamp}
    */} 132 | 133 | {/*
    134 |
    135 |
    Be the first comment here!
    136 | 139 |
    140 | {post.comments.length > 0 ? ( 141 | post.comments.map((comment) =>
    {comment.caption}
    ) 142 | ) : ( 143 |
    144 | )} 145 |
    */} 146 |
    147 | ) : null} 148 |
    149 | ); 150 | }; 151 | 152 | export default PostDetails; 153 | -------------------------------------------------------------------------------- /react-app/src/components/UserProfile/User.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { NavLink, useParams } from "react-router-dom"; 4 | import { getAllPosts } from "../../store/post"; 5 | import { getAllFollowing } from "../../store/session"; 6 | // import { getAllUsers } from '../../store/user'; 7 | // import PostForm from '../PostForm'; 8 | import "./user.css"; 9 | 10 | function User() { 11 | const { userId } = useParams(); 12 | const dispatch = useDispatch(); 13 | const [user, setUser] = useState({}); 14 | const loggedInUser = useSelector((state) => state.session.user); 15 | 16 | const [isFollowing, setIsFollowing] = useState( 17 | loggedInUser.follows.map((u) => +u.id).includes(+userId) 18 | ); 19 | 20 | const posts = useSelector((state) => Object.values(state.posts)); 21 | 22 | 23 | const filteredPost = posts.filter((post) => post.user_id === +userId); 24 | 25 | let navbar_pfp; 26 | 27 | if (user?.profile_pic) { 28 | navbar_pfp = user?.profile_pic 29 | } 30 | else { 31 | navbar_pfp = "https://www.tenforums.com/geek/gars/images/2/types/thumb_15951118880user.png" 32 | } 33 | 34 | const handleFollow = async () => { 35 | const response = await fetch(`/api/users/${userId}/follow`); 36 | const obj = await response.json(); 37 | 38 | setUser({ ...obj.otherUser }); 39 | // console.log() 40 | setIsFollowing(!isFollowing); 41 | }; 42 | 43 | useEffect(() => { 44 | dispatch(getAllPosts()); 45 | dispatch(getAllFollowing(loggedInUser.id)) 46 | }, [dispatch]); 47 | 48 | const addpic = "https://i.imgur.com/3yiJpcr.png"; 49 | 50 | useEffect(() => { 51 | if (!userId) { 52 | return; 53 | } 54 | (async () => { 55 | const response = await fetch(`/api/users/${userId}`); 56 | const user = await response.json(); 57 | 58 | setUser(user); 59 | })(); 60 | }, [userId]); 61 | 62 | if (!user) { 63 | return null; 64 | } 65 | 66 | return ( 67 |
    68 | 69 |
    70 |
    71 | {user?.profile_pic === null || user?.profile_pic?.includes("jpeg") || user?.profile_pic?.includes("jpg") || user?.profile_pic?.includes("png") || user?.profile_pic?.includes("image") ? : } 72 | {/* {user.id} */} 73 |
    74 | 75 |
    76 |
    77 | 78 |
    {user.username}
    79 | 80 |
    81 | {+userId !== +loggedInUser.id && (isFollowing ? ( 82 | 83 | ) : ( 84 | 85 | ))} 86 |
    87 |
    88 | 89 |
    90 |
    91 |
    92 | 93 |
    {filteredPost.length} posts
    94 |
    95 | 96 |
    97 | 98 | {user.follow_by?.length} followers 99 | 100 |
    101 | 102 |
    103 | 104 | {user.follows?.length} following 105 | 106 |
    107 | 108 |
    109 | 110 |
    111 | 112 |
    {user.bio}
    113 | 114 |
    115 | 116 |
    117 | 118 |
    119 |
    120 |
    121 |
    122 |
    123 | {filteredPost.length > 0 ? ( 124 | filteredPost.map((post) => ( 125 | 126 | {post.id} 127 | 128 | )) 129 | ) : ( 130 | 131 |
    132 |
    133 | 134 | {`addpic`} 135 | 136 |
    137 |
    138 | )} 139 |
    140 |
    141 |
    142 | ); 143 | } 144 | export default User; 145 | -------------------------------------------------------------------------------- /react-app/src/components/auth/SignUpForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux' 3 | import { Redirect, NavLink } from 'react-router-dom'; 4 | import { signUp } from '../../store/session'; 5 | import DemoLoginButton from './DemoLogin'; 6 | import "./login-signup.css"; 7 | import Particles from "react-particles-js"; 8 | 9 | 10 | const SignUpForm = ({ setAuthenticated }) => { 11 | const [errors, setErrors] = useState([]); 12 | const [username, setUsername] = useState(''); 13 | const [email, setEmail] = useState(''); 14 | const [password, setPassword] = useState(''); 15 | const [repeatPassword, setRepeatPassword] = useState(''); 16 | const [profile_pic, setProfilePic] = useState('') 17 | const user = useSelector(state => state.session.user); 18 | const dispatch = useDispatch(); 19 | 20 | const onSignUp = async (e) => { 21 | e.preventDefault(); 22 | if (password === repeatPassword) { 23 | const data = await dispatch(signUp(username, email, password, profile_pic)); 24 | if (data) { 25 | setErrors(data) 26 | } 27 | } else { 28 | setErrors(["Passwords do no match!"]) 29 | } 30 | }; 31 | 32 | const updateUsername = (e) => { 33 | setUsername(e.target.value); 34 | }; 35 | 36 | const updateEmail = (e) => { 37 | setEmail(e.target.value); 38 | }; 39 | 40 | const updatePassword = (e) => { 41 | setPassword(e.target.value); 42 | }; 43 | 44 | const updateRepeatPassword = (e) => { 45 | setRepeatPassword(e.target.value); 46 | }; 47 | 48 | const updateProfilePic = (e) => { 49 | setProfilePic(e.target.value); 50 | }; 51 | 52 | if (user) { 53 | return ; 54 | } 55 | 56 | return ( 57 | <> 58 |
    59 | 60 |

    Sign up

    61 |
    62 |
    63 |
    64 | {errors.map((error) => ( 65 |
    {error}
    66 | ))} 67 |
    68 |
    69 |
    70 | 78 |
    79 |
    80 | 89 |
    90 |
    91 | 99 |
    100 |
    101 | 110 |
    111 |
    112 | 120 |
    121 | {/* Kyle's working on this optional profile picture input */} 122 | 125 |
    126 |
    127 |
    128 | OR 129 |
    130 |
    131 |
    132 | 133 |
    134 |
    135 |
    136 |

    137 | Already have an account?{" "} 138 | 139 | Log in 140 | 141 |

    142 |
    143 | 159 | 160 | ); 161 | }; 162 | 163 | export default SignUpForm; 164 | -------------------------------------------------------------------------------- /react-app/src/components/splash-page/SplashPage.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .header__container { 4 | display: flex; 5 | } 6 | 7 | .content-title { 8 | width: 300px; 9 | font-size: 2rem; 10 | } 11 | 12 | .splash-title { 13 | font-size: 2rem; 14 | margin: 0; 15 | margin-top: 5px; 16 | margin-left: 20px; 17 | } 18 | 19 | .grid__container { 20 | margin-top: 30px; 21 | display: flex; 22 | flex-direction: column; 23 | align-items: center; 24 | } 25 | 26 | .grid__container-row { 27 | display: flex; 28 | justify-content: center; 29 | margin: 10px; 30 | } 31 | 32 | .grid-pic { 33 | margin: 10px; 34 | height: 115px; 35 | width: 130px; 36 | border-radius: 3px; 37 | box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 38 | 0px 3px 1px -2px rgba(0, 0, 0, 0.12), 0px 1px 5px 0px rgba(0, 0, 0, 0.2); 39 | } 40 | 41 | .main__container { 42 | display: flex; 43 | } 44 | 45 | .content-title { 46 | width: 300px; 47 | font-size: 2rem; 48 | } 49 | 50 | .content__container { 51 | margin-left: 30px; 52 | margin-top: 30px; 53 | display: flex; 54 | flex-direction: column; 55 | align-items: center; 56 | } 57 | 58 | .splash__container { 59 | display: flex; 60 | flex-direction: column; 61 | align-items: center; 62 | 63 | margin-top: 70px; 64 | } 65 | .log-in-button { 66 | margin-top: 20px; 67 | background-color: #b2dffc; 68 | 69 | border: 0px; 70 | height: 33.33px; 71 | 72 | color: #f5f7f9; 73 | border-radius: 3px; 74 | font-size: 0.9rem; 75 | font-weight: bold; 76 | transition: background-color 0.5s; 77 | transition-timing-function: ease-in-out; 78 | width: 100%; 79 | text-decoration: none; 80 | } 81 | 82 | .log-in-button:hover { 83 | background-color: #0095f6; 84 | cursor: pointer; 85 | } 86 | 87 | .sign-up-button { 88 | margin-top: 20px; 89 | border: 0px; 90 | background-color: #a5f1a9; 91 | 92 | height: 33.33px; 93 | /* letter-spacing: 0.1rem; */ 94 | color: #f5f7f9; 95 | border-radius: 3px; 96 | font-size: 0.9rem; 97 | font-weight: bold; 98 | transition: background-color 0.5s; 99 | transition-timing-function: ease-in-out; 100 | width: 100%; 101 | text-decoration: none; 102 | } 103 | 104 | .sign-up-button:hover { 105 | 106 | background-color: #5cd376; 107 | 108 | cursor: pointer; 109 | } 110 | .demo-button { 111 | margin-top: 20px; 112 | background-color: #fdc9fd; 113 | 114 | border: 0px; 115 | height: 33.33px; 116 | 117 | color: #ffffff; 118 | border-radius: 3px; 119 | font-size: 0.8rem; 120 | font-weight: bold; 121 | transition: background-color 0.5s; 122 | transition-timing-function: ease-in-out; 123 | width: 100%; 124 | text-decoration: none; 125 | } 126 | .demo-button:hover { 127 | background-color: #eb54c0; 128 | 129 | cursor: pointer; 130 | } 131 | 132 | .text { 133 | display: flex; 134 | justify-content: center; 135 | align-items: center; 136 | height: 100%; 137 | } 138 | 139 | #text{ 140 | display: flex; 141 | justify-content: center; 142 | align-items: center; 143 | height: 100%; 144 | } 145 | 146 | 147 | .footer-link { 148 | text-decoration: None; 149 | margin: 15px; 150 | color: rgb(65, 65, 65); 151 | font-size: 0.8rem; 152 | } 153 | 154 | .search-bar { 155 | display: flex; 156 | width: 80%; 157 | justify-content: center; 158 | } 159 | 160 | .search-bar { 161 | border-radius: 30px; 162 | width: 300px; 163 | height: 30px; 164 | font-size: 15px; 165 | text-align: center; 166 | position: relative; 167 | top: 10px; 168 | background-color: #eee; 169 | border: solid 1px rgb(143, 143, 143); 170 | } 171 | 172 | 173 | .footer__container { 174 | display: flex; 175 | align-items:center; 176 | position: fixed; 177 | /* left: 20%; 178 | right: 20%; */ 179 | bottom: 0px; 180 | width: 100%; 181 | margin-top: 50px; 182 | justify-content: center; 183 | 184 | } 185 | 186 | .personal-info__container{ 187 | display: flex; 188 | flex-direction: column; 189 | box-sizing: border-box; 190 | padding: 10px; 191 | margin: 0px 10px; 192 | justify-content: center; 193 | } 194 | 195 | .personal-info__container a { 196 | margin: unset; 197 | } 198 | .portfolio{ 199 | text-decoration: unset; 200 | color: black; 201 | } 202 | .portfolio:hover{ 203 | text-decoration: underline; 204 | } 205 | 206 | .linkin-github__container{ 207 | display: flex; 208 | flex-direction: row; 209 | justify-content: space-evenly; 210 | } 211 | 212 | .fa-linkedin{ 213 | font-size: 30px; 214 | color: #0A66C2; 215 | } 216 | .fa-github{ 217 | font-size: 30px; 218 | } 219 | 220 | /* .footer-link { 221 | text-decoration: None; 222 | margin: 40px; 223 | color: rgb(65, 65, 65); 224 | font-size: 0.8rem; 225 | } */ 226 | 227 | .project-repo__container{ 228 | display: flex; 229 | flex-direction: column; 230 | box-sizing: border-box; 231 | padding: 10px; 232 | margin: 0px 10px; 233 | justify-content: center; 234 | align-items: center; 235 | } 236 | 237 | .party { 238 | position: fixed; 239 | right: 0; 240 | bottom: 0; 241 | min-width: 100%; 242 | min-height: 100%; 243 | z-index: -1; 244 | } 245 | -------------------------------------------------------------------------------- /react-app/src/components/splash-page/SplashPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink, Redirect } from "react-router-dom"; 3 | import "./SplashPage.css"; 4 | import DemoLogin from "../auth/DemoLogin"; 5 | import Particles from "react-particles-js"; 6 | 7 | 8 | export default function SplashPage({ authenticated }) { 9 | if (authenticated) { 10 | return ; 11 | } 12 | 13 | return ( 14 | <> 15 |
    16 |
    17 | 18 |
    19 |
    20 |
    21 |
    22 | Splash Page 27 | Splash Page 32 | Splash Page 37 |
    38 |
    39 | Splash Page 44 | Splash Page 49 | Splash Page 54 |
    55 |
    56 | Splash Page 61 | Splash Page 66 | Splash Page 71 |
    72 |
    73 |
    74 |

    75 | The Connection between all Art and Your Heart. 76 |

    77 | 78 | Log In 79 | 80 | 81 | Sign up 82 | 83 | 84 |
    85 |
    86 |
    87 |
    88 |
    89 | Lema El-Sherbiny 90 |
    91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 |
    99 |
    100 | 101 |
    102 | Diana Beatriz Tinoco 103 | 114 |
    115 |
    116 | Project Repo 117 | 121 | 122 | 123 |
    124 |
    125 | Kyle Tseng 126 | 134 |
    135 | 136 |
    137 | Zhuoxin Tan 138 | 146 |
    147 |
    148 | 164 | 165 | ); 166 | }; 167 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "998dcdfb70c39c24ab73ae6405cc93b4bdca44a36070f28bb816eb0e25bc07db" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | }, 16 | { 17 | "url": "https://pypi.org/simple", 18 | "verify_ssl": true 19 | } 20 | ] 21 | }, 22 | "default": { 23 | "alembic": { 24 | "hashes": [ 25 | "sha256:a21fedebb3fb8f6bbbba51a11114f08c78709377051384c9c5ead5705ee93a51", 26 | "sha256:e78be5b919f5bb184e3e0e2dd1ca986f2362e29a2bc933c446fe89f39dbe4e9c" 27 | ], 28 | "index": "pypi", 29 | "version": "==1.6.5" 30 | }, 31 | "click": { 32 | "hashes": [ 33 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 34 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 35 | ], 36 | "index": "pypi", 37 | "version": "==7.1.2" 38 | }, 39 | "flask": { 40 | "hashes": [ 41 | "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55", 42 | "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9" 43 | ], 44 | "index": "pypi", 45 | "version": "==2.0.1" 46 | }, 47 | "flask-cors": { 48 | "hashes": [ 49 | "sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16", 50 | "sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a" 51 | ], 52 | "index": "pypi", 53 | "version": "==3.0.8" 54 | }, 55 | "flask-login": { 56 | "hashes": [ 57 | "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b", 58 | "sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0" 59 | ], 60 | "index": "pypi", 61 | "version": "==0.5.0" 62 | }, 63 | "flask-migrate": { 64 | "hashes": [ 65 | "sha256:4d42e8f861d78cb6e9319afcba5bf76062e5efd7784184dd2a1cccd9de34a702", 66 | "sha256:df9043d2050df3c0e0f6313f6b529b62c837b6033c20335e9d0b4acdf2c40e23" 67 | ], 68 | "index": "pypi", 69 | "version": "==3.0.1" 70 | }, 71 | "flask-sqlalchemy": { 72 | "hashes": [ 73 | "sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912", 74 | "sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390" 75 | ], 76 | "index": "pypi", 77 | "version": "==2.5.1" 78 | }, 79 | "flask-wtf": { 80 | "hashes": [ 81 | "sha256:6ff7af73458f182180906a37a783e290bdc8a3817fe4ad17227563137ca285bf", 82 | "sha256:ff177185f891302dc253437fe63081e7a46a4e99aca61dfe086fb23e54fff2dc" 83 | ], 84 | "index": "pypi", 85 | "version": "==0.15.1" 86 | }, 87 | "greenlet": { 88 | "hashes": [ 89 | "sha256:03f28a5ea20201e70ab70518d151116ce939b412961c33827519ce620957d44c", 90 | "sha256:06d7ac89e6094a0a8f8dc46aa61898e9e1aec79b0f8b47b2400dd51a44dbc832", 91 | "sha256:06ecb43b04480e6bafc45cb1b4b67c785e183ce12c079473359e04a709333b08", 92 | "sha256:096cb0217d1505826ba3d723e8981096f2622cde1eb91af9ed89a17c10aa1f3e", 93 | "sha256:0c557c809eeee215b87e8a7cbfb2d783fb5598a78342c29ade561440abae7d22", 94 | "sha256:0de64d419b1cb1bfd4ea544bedea4b535ef3ae1e150b0f2609da14bbf48a4a5f", 95 | "sha256:14927b15c953f8f2d2a8dffa224aa78d7759ef95284d4c39e1745cf36e8cdd2c", 96 | "sha256:16183fa53bc1a037c38d75fdc59d6208181fa28024a12a7f64bb0884434c91ea", 97 | "sha256:206295d270f702bc27dbdbd7651e8ebe42d319139e0d90217b2074309a200da8", 98 | "sha256:22002259e5b7828b05600a762579fa2f8b33373ad95a0ee57b4d6109d0e589ad", 99 | "sha256:2325123ff3a8ecc10ca76f062445efef13b6cf5a23389e2df3c02a4a527b89bc", 100 | "sha256:258f9612aba0d06785143ee1cbf2d7361801c95489c0bd10c69d163ec5254a16", 101 | "sha256:3096286a6072553b5dbd5efbefc22297e9d06a05ac14ba017233fedaed7584a8", 102 | "sha256:3d13da093d44dee7535b91049e44dd2b5540c2a0e15df168404d3dd2626e0ec5", 103 | "sha256:408071b64e52192869129a205e5b463abda36eff0cebb19d6e63369440e4dc99", 104 | "sha256:598bcfd841e0b1d88e32e6a5ea48348a2c726461b05ff057c1b8692be9443c6e", 105 | "sha256:5d928e2e3c3906e0a29b43dc26d9b3d6e36921eee276786c4e7ad9ff5665c78a", 106 | "sha256:5f75e7f237428755d00e7460239a2482fa7e3970db56c8935bd60da3f0733e56", 107 | "sha256:60848099b76467ef09b62b0f4512e7e6f0a2c977357a036de602b653667f5f4c", 108 | "sha256:6b1d08f2e7f2048d77343279c4d4faa7aef168b3e36039cba1917fffb781a8ed", 109 | "sha256:70bd1bb271e9429e2793902dfd194b653221904a07cbf207c3139e2672d17959", 110 | "sha256:76ed710b4e953fc31c663b079d317c18f40235ba2e3d55f70ff80794f7b57922", 111 | "sha256:7920e3eccd26b7f4c661b746002f5ec5f0928076bd738d38d894bb359ce51927", 112 | "sha256:7db68f15486d412b8e2cfcd584bf3b3a000911d25779d081cbbae76d71bd1a7e", 113 | "sha256:8833e27949ea32d27f7e96930fa29404dd4f2feb13cce483daf52e8842ec246a", 114 | "sha256:944fbdd540712d5377a8795c840a97ff71e7f3221d3fddc98769a15a87b36131", 115 | "sha256:9a6b035aa2c5fcf3dbbf0e3a8a5bc75286fc2d4e6f9cfa738788b433ec894919", 116 | "sha256:9bdcff4b9051fb1aa4bba4fceff6a5f770c6be436408efd99b76fc827f2a9319", 117 | "sha256:a9017ff5fc2522e45562882ff481128631bf35da444775bc2776ac5c61d8bcae", 118 | "sha256:aa4230234d02e6f32f189fd40b59d5a968fe77e80f59c9c933384fe8ba535535", 119 | "sha256:ad80bb338cf9f8129c049837a42a43451fc7c8b57ad56f8e6d32e7697b115505", 120 | "sha256:adb94a28225005890d4cf73648b5131e885c7b4b17bc762779f061844aabcc11", 121 | "sha256:b3090631fecdf7e983d183d0fad7ea72cfb12fa9212461a9b708ff7907ffff47", 122 | "sha256:b33b51ab057f8a20b497ffafdb1e79256db0c03ef4f5e3d52e7497200e11f821", 123 | "sha256:b97c9a144bbeec7039cca44df117efcbeed7209543f5695201cacf05ba3b5857", 124 | "sha256:be13a18cec649ebaab835dff269e914679ef329204704869f2f167b2c163a9da", 125 | "sha256:be9768e56f92d1d7cd94185bab5856f3c5589a50d221c166cc2ad5eb134bd1dc", 126 | "sha256:c1580087ab493c6b43e66f2bdd165d9e3c1e86ef83f6c2c44a29f2869d2c5bd5", 127 | "sha256:c35872b2916ab5a240d52a94314c963476c989814ba9b519bc842e5b61b464bb", 128 | "sha256:c70c7dd733a4c56838d1f1781e769081a25fade879510c5b5f0df76956abfa05", 129 | "sha256:c767458511a59f6f597bfb0032a1c82a52c29ae228c2c0a6865cfeaeaac4c5f5", 130 | "sha256:c87df8ae3f01ffb4483c796fe1b15232ce2b219f0b18126948616224d3f658ee", 131 | "sha256:ca1c4a569232c063615f9e70ff9a1e2fee8c66a6fb5caf0f5e8b21a396deec3e", 132 | "sha256:cc407b68e0a874e7ece60f6639df46309376882152345508be94da608cc0b831", 133 | "sha256:da862b8f7de577bc421323714f63276acb2f759ab8c5e33335509f0b89e06b8f", 134 | "sha256:dfe7eac0d253915116ed0cd160a15a88981a1d194c1ef151e862a5c7d2f853d3", 135 | "sha256:ed1377feed808c9c1139bdb6a61bcbf030c236dd288d6fca71ac26906ab03ba6", 136 | "sha256:f42ad188466d946f1b3afc0a9e1a266ac8926461ee0786c06baac6bd71f8a6f3", 137 | "sha256:f92731609d6625e1cc26ff5757db4d32b6b810d2a3363b0ff94ff573e5901f6f" 138 | ], 139 | "index": "pypi", 140 | "markers": "python_version >= '3'", 141 | "version": "==1.1.0" 142 | }, 143 | "gunicorn": { 144 | "hashes": [ 145 | "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", 146 | "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" 147 | ], 148 | "index": "pypi", 149 | "version": "==20.1.0" 150 | }, 151 | "itsdangerous": { 152 | "hashes": [ 153 | "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", 154 | "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" 155 | ], 156 | "index": "pypi", 157 | "version": "==2.0.1" 158 | }, 159 | "jinja2": { 160 | "hashes": [ 161 | "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", 162 | "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" 163 | ], 164 | "index": "pypi", 165 | "version": "==3.0.1" 166 | }, 167 | "mako": { 168 | "hashes": [ 169 | "sha256:17831f0b7087c313c0ffae2bcbbd3c1d5ba9eeac9c38f2eb7b50e8c99fe9d5ab", 170 | "sha256:aea166356da44b9b830c8023cd9b557fa856bd8b4035d6de771ca027dfc5cc6e" 171 | ], 172 | "index": "pypi", 173 | "version": "==1.1.4" 174 | }, 175 | "markupsafe": { 176 | "hashes": [ 177 | "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", 178 | "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", 179 | "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", 180 | "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", 181 | "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", 182 | "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", 183 | "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", 184 | "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", 185 | "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", 186 | "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", 187 | "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", 188 | "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", 189 | "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", 190 | "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", 191 | "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", 192 | "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", 193 | "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", 194 | "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", 195 | "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", 196 | "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", 197 | "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", 198 | "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", 199 | "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", 200 | "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", 201 | "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", 202 | "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", 203 | "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", 204 | "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", 205 | "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", 206 | "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", 207 | "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", 208 | "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", 209 | "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", 210 | "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", 211 | "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", 212 | "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", 213 | "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", 214 | "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", 215 | "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", 216 | "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", 217 | "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", 218 | "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", 219 | "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", 220 | "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", 221 | "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", 222 | "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", 223 | "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", 224 | "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", 225 | "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", 226 | "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", 227 | "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", 228 | "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", 229 | "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", 230 | "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" 231 | ], 232 | "index": "pypi", 233 | "version": "==2.0.1" 234 | }, 235 | "python-dateutil": { 236 | "hashes": [ 237 | "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", 238 | "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" 239 | ], 240 | "index": "pypi", 241 | "version": "==2.8.1" 242 | }, 243 | "python-dotenv": { 244 | "hashes": [ 245 | "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d", 246 | "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423" 247 | ], 248 | "index": "pypi", 249 | "version": "==0.14.0" 250 | }, 251 | "python-editor": { 252 | "hashes": [ 253 | "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", 254 | "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", 255 | "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8", 256 | "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77", 257 | "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522" 258 | ], 259 | "index": "pypi", 260 | "version": "==1.0.4" 261 | }, 262 | "six": { 263 | "hashes": [ 264 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 265 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 266 | ], 267 | "index": "pypi", 268 | "version": "==1.15.0" 269 | }, 270 | "sqlalchemy": { 271 | "hashes": [ 272 | "sha256:0fb3f73e5009f5a4c9b24469939d3d57cc3ad8099a09c0cfefc47fe45ab7ffbe", 273 | "sha256:20f4bf1459548a74aade997cb045015e4d72f0fde1789b09b3bb380be28f6511", 274 | "sha256:2ace9ab2af9d7d7b0e2ff2178809941c56ab8921e38128278192a73a8a1c08a2", 275 | "sha256:311051c06f905774427b4a92dcb3924d6ee563dea3a88176da02fdfc572d0d1d", 276 | "sha256:45b0f773e195d8d51e2fd67cb5b5fb32f5a1f5e7f0752016207091bed108909a", 277 | "sha256:57ba8a96b6d058c7dcf44de8ac0955b7a787f7177a0221dd4b8016e0191268f5", 278 | "sha256:58d4f79d119010fdced6e7fd7e4b9f2230dbf55a8235d7c58b1c8207ef74791b", 279 | "sha256:5c92d9ebf4b38c22c0c9e4f203a80e101910a50dc555b4578816932015b97d7f", 280 | "sha256:6317701c06a829b066c794545512bb70b1a10a74574cfa5658a0aaf49f31aa93", 281 | "sha256:64eab458619ef759f16f0f82242813d3289e829f8557fbc7c212ca4eadf96472", 282 | "sha256:6fd1b745ade2020a1a7bf1e22536d8afe86287882c81ca5d860bdf231d5854e9", 283 | "sha256:89a5a13dcf33b7e47c7a9404a297c836965a247c7f076a0fe0910cae2bee5ce2", 284 | "sha256:8cba69545246d16c6d2a12ce45865947cbdd814bacddf2e532fdd4512e70728c", 285 | "sha256:8f1e7f4de05c15d6b46af12f3cf0c2552f2940d201a49926703249a62402d851", 286 | "sha256:9014fd1d8aebcb4eb6bc69a382dd149200e1d5924412b1d08b4443f6c1ce526f", 287 | "sha256:9133635edcec1e7fbfc16eba5dc2b5b3b11818d25b7a57cfcbfa8d3b3e9594fd", 288 | "sha256:93ba458b3c279581288a10a55df2aa6ac3509882228fcbad9d9d88069f899337", 289 | "sha256:942ca49b7ec7449d2473a6587825c55ad99534ddfc4eee249dd42be3cc1aa8c9", 290 | "sha256:95a9fd0a11f89a80d8815418eccba034f3fec8ea1f04c41b6b8decc5c95852e9", 291 | "sha256:96d3d4a7ead376d738775a1fa9786dc17a31975ec664cea284e53735c79a5686", 292 | "sha256:9c0945c79cbe507b49524e31a4bb8700060bbccb60bb553df6432e176baff3d5", 293 | "sha256:a34a7fd3353ee61a1dca72fc0c3e38d4e56bdc2c343e712f60a8c70acd4ef5bf", 294 | "sha256:c6efc7477551ba9ce632d5c3b448b7de0277c86005eec190a1068fcc7115fd0e", 295 | "sha256:cefd44faca7c57534503261f6fab49bd47eb9c2945ee0bab09faaa8cb047c24f", 296 | "sha256:d04160462f874eaa4d88721a0d5ecca8ebf433616801efe779f252ef87b0e216", 297 | "sha256:d3cf5f543d048a7c8da500133068c5c90c97a2c4bf0c027928a85028a519f33d", 298 | "sha256:d7b21a4b62921cf6dca97e8f9dea1fbe2432aebbb09895a2bd4f527105af41a4", 299 | "sha256:ddbce8fe4d0190db21db602e38aaf4c158c540b49f1ef7475323ec682a9fbf2d", 300 | "sha256:e2761b925fda550debfd5a8bc3cef9debc9a23c6a280429c4ec3a07c35c6b4b3", 301 | "sha256:fa05a77662c23226c9ec031638fd90ae767009e05cd092b948740f09d10645f0" 302 | ], 303 | "index": "pypi", 304 | "version": "==1.4.19" 305 | }, 306 | "werkzeug": { 307 | "hashes": [ 308 | "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42", 309 | "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8" 310 | ], 311 | "index": "pypi", 312 | "version": "==2.0.1" 313 | }, 314 | "wtforms": { 315 | "hashes": [ 316 | "sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c", 317 | "sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c" 318 | ], 319 | "index": "pypi", 320 | "version": "==2.3.3" 321 | } 322 | }, 323 | "develop": { 324 | "psycopg2-binary": { 325 | "hashes": [ 326 | "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c", 327 | "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67", 328 | "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0", 329 | "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6", 330 | "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db", 331 | "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94", 332 | "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52", 333 | "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056", 334 | "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b", 335 | "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", 336 | "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", 337 | "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679", 338 | "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83", 339 | "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77", 340 | "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2", 341 | "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77", 342 | "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2", 343 | "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd", 344 | "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859", 345 | "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1", 346 | "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25", 347 | "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152", 348 | "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf", 349 | "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f", 350 | "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729", 351 | "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71", 352 | "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66", 353 | "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4", 354 | "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449", 355 | "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da", 356 | "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a", 357 | "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c", 358 | "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb", 359 | "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4", 360 | "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5" 361 | ], 362 | "index": "pypi", 363 | "version": "==2.8.6" 364 | } 365 | } 366 | } 367 | --------------------------------------------------------------------------------