YourCity
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 |
Technologies
Features
Sign Up and Login Pages
Splash Page
Discover and search for new cities
Profile
Profile card about user and view cities
Feed Tab
YourCity feed displays all cities
View, Add, Edit, and Delete Cities
Single city of name, photos, insights
Add a City
Cancel adding city
Edit a city
Create, Read, Update, Delete City Insights
View Insights
Add Insights
Edit Insights
Installation
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
Technical Implementation Details
City Validators
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)
Read More for Long Posts (Insights)
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>
}
</>
}
City Reducer
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;
}
}
Future Features
-
Matches - match people with cities based on their question responses
-
Search - search cities
-
Edit Profile - users edit profile info and add banner