├── .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 |
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 |
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 |
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 | *
13 |
14 | *
15 |
16 | *
17 |
18 | *
19 |
20 |
21 | *
22 |
23 | *
24 |
25 | ## Welcome View
26 |
27 | 
28 |
29 | ## Home View
30 |
31 | 
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 |
44 |
45 |
46 |
47 | {/*
48 |
49 |
*/}
50 |
51 |
52 |
53 |
54 |
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 | {/*
*/}
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 |
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 |
76 |
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]}]({`${post.user?.profile_pic}`})
72 |
73 |
74 |
{post.user?.username}
75 |
76 |
77 |
78 |
79 |

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 |

71 |
72 |
73 |
{post.user?.username}
74 |
75 |
76 |
77 |
78 |

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 |
105 |
106 |
107 |
108 |
109 |
likes: {post.likesnum}
110 |
111 |
112 |
113 | {post.user.username} {post.caption}
114 |
115 |
116 |
120 | setShowEditCaption(post.id)}
122 | className="far fa-edit"
123 | >
124 |
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 | {/*

*/}
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 |
121 |
122 |
123 | {filteredPost.length > 0 ? (
124 | filteredPost.map((post) => (
125 |
126 |
127 |
128 | ))
129 | ) : (
130 |
131 |
132 |
133 |
134 |
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 |
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 |
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 |
100 |
101 |
102 | Diana Beatriz Tinoco
103 |
114 |
115 |
124 |
125 | Kyle Tseng
126 |
134 |
135 |
136 |
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 |
--------------------------------------------------------------------------------