YourCity is a city matching App that matches users to their ideal city. It is a fullstack React App made with a Redux state manager and a backend using Python, Flask, SQL-Alchemy, and PostgresSQL.
Table of Contents |
---|
1. Features |
2. Installation |
3. Technical Implementation Details |
4. Future Features |
5. Contact |
6. Special Thanks |
Discover and search for new cities
Profile card about user and view cities
YourCity feed displays all cities
Single city of name, photos, insights
To build/run project locally, please follow these steps:
- Clone this repository
git clone https://github.com/{github-handle}/{app-name}.git
- Install Pipfile dependencies and create the virtual environment
pipenv install
- Install npm dependencies for the
/react-app
cd react-app
npm install
-
In the
/
root directory, create a.env
based on the.env.example
with proper settings -
Setup your PostgreSQL user, password and database and ensure it matches your
.env
file -
Before running any flask commands, confirm you are in the pipenv virtual env. If not, run the command:
pipenv shell
- In the root folder, create the database by running in the terminal:
flask db init
- In the root folder, migrate tables to the database by running in the terminal:
flask db migrate
- In the root folder, seed the database by running in the terminal:
flask seed all
- Start the flask backend in the
/
root directory
flask run
- Start the frontend in the
/react-app
directory
npm start
This is the first project I used flask and SQLAlchemy, and I didn't have much experience using the wtform validators. After reading documentation, I created Forms to validate required fields with DataRequired
and the length of fields with the Length
class by providing a min and max.
Code snippet is shown here:
class CityPostForm(FlaskForm):
name = StringField('name', validators=[DataRequired(), city_exists, Length(min=1, max=80)])
state = StringField('state', validators=[Length(min=0, max=50)])
thumbnail_img = StringField('thumbnail_img', validators=[Length(min=0, max=800)])
description = StringField('description', validators=[Length(min=0, max=1200)])
user_id = IntegerField('user_id', validators=[DataRequired()])
The form is created from the POST route to create a city, and it is validated using the validators above. If any fields throw an error, then the form.validate_on_submit()
will fail and return the errors from form.errors
. The resulting errors are passed into a custom error handler that sends back each of the errors to the frontend to display to the user, e.g. 'Field is required' or 'Name field must be between 0 and 100 characters in length'.
@city_routes.route('/', methods=['POST'])
@login_required
def city_post():
form = CityPostForm()
form['csrf_token'].data = request.cookies['csrf_token']
if form.validate_on_submit():
city = City()
form.populate_obj(city)
try:
db.session.add(city)
db.session.commit()
return city.to_dict()
except:
return throw_server_error()
return throw_validation_error(form.errors)
Posts for insights are can span an entire page, which is not ideal for user experience. In order to limit the length, I created a Read More
and Show Less
buttons to conditionally render the entire post and to hide the post. I was able to use the scrollHeight
and offsetHeight
of the textarea input to determine if the text was overfilling the container. If the scroll is greater than the offset, then the post is longer and a Read More
button should appear.
The frontend uses the isOverflow
state to initially determine if the post is overflowing.
const [showMore, setShowMore] = useState(false);
const [isOverflow, setIsOverflow] = useState(true);
useEffect(() => {
const scrollHeight = document.getElementById(`insight__text_id-${insight.id}`)?.scrollHeight;
const offsetHeight = document.getElementById(`insight__text_id-${insight.id}`)?.offsetHeight;
if (scrollHeight && offsetHeight) {
if (scrollHeight > offsetHeight) {
setShowMore(false);
} else {
setShowMore(true);
setIsOverflow(false);
}
}
}, [insight.id]);
The showMore
state is used to conditionally render a short post and the entire post. If showMore
is false the component will render a cut off post that has a Read more
click event to toggle the state. When the Read more
is clicked, showMore
is set to true and the component now renders the entire post.
In addition the isOverflow
is used to render Show less
only if the post is overfilling the container.
{!showMore &&
<>
<p>
<span>
{ username }
</span>
{ insight.insight }
</p>
<p className={styles.text_dots}>...</p>
<span
onClick={() => setShowMore(true)}
>
Read more
</span>
</>
}
{showMore &&
<>
<p>
<span>
{ username }
</span>
{ insight.insight }
</p>
{isOverflow &&
<span
onClick={() => setShowMore(false)}
>
Show less
</span>
}
</>
}
One of my goals on this project was to create a simple reducer with slices of state for each table. Taking code from one of my previous projects, I refactored the code to create four actions. The SET_CITY
action case is used for updating the store for the CRUD operations of CREATE, UPDATE, and READ.
The reducer for my City table is shown below:
export default function reducer(state = {}, action) {
let newState = { ...state };
switch (action.type) {
case SET_CITY:
newState[action.city.id] = action.city;
return newState;
case SET_ALL_CITIES:
action.cities.forEach(city => {
newState[city.id] = city;
});
return newState;
case DELETE_CITY:
delete newState[action.cityId];
return newState;
case UNLOAD_CITIES:
newState = {}
return newState;
default:
return state;
}
}
-
Matches - match people with cities based on their question responses
-
Search - search cities
-
Edit Profile - users edit profile info and add banner