Unwined is a wine rating App that assists users to discover and rate new wines. It is a fullstack React App made with a Redux state manager and a backend using Node/Express, Sequelize, and PostgresSQL.
-
View the Unwined App Live
-
It is modeled after the Untappd App
-
There are over 5k wines, 3k wineries and 300 wine types seeded in the database
-
Reference to the Unwined Wiki Docs
Table of Contents |
---|
1. Features |
2. Installation |
3. Technical Implementation Details |
4. Future Features |
5. Contact |
6. Special Thanks |
Unwined logo and features of site are shown
Single wine details of name, wine type, price, review, etc.
Users can add reviews for a wine
Edited Review is highlighted in blue with options to save or cancel changes
Discover and search for new wines, or add a new one
Add a new wine to the database
Page 7 is highlighted and displays that subset of the wines
Modal renders search results below as user types input
To build/run project locally, please follow these steps:
- Clone this repository
git clone https://github.com/nicopierson/unwined.git
- Install npm dependencies for both the
/frontend
and/backend
npm install
- In the
/backend
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 - Run migrations and seeds in the
/backend
npx dotenv sequelize db:create
npx dotenv sequelize db:migrate
npx dotenv sequelize db:seed:all
- Start both the backend and frontend
npm start
The first goal was to find a method to dynamically unmount a component from an event e.g. click or an input change. After searching and experimenting, I discovered the CSSTransition
component from the react-transition-group
package.
First the state is declared and references are made:
const [toggleForm, setToggleForm] = useState(false);
const [ref, setRef] = useState(React.createRef());
useEffect(() => {
setRef(React.createRef())
}, [toggleForm]);
Then one CSSTransition
component holds the WineDetailPage
, and another the WineForm
as a child. The WineDetailPage
unmounts when a user clicks the Edit
button, and afterwards the WineForm
component mounts. These components will swap again when a user clicks the Cancel
button in the WineForm
.
Integrating these components with Transition
components allow dynamic mounting based on a toggle state such as toggleForm
shown below:
return (
<>
<CSSTransition
in={!toggleForm}
timeout={800}
classNames='wine_detail'
nodeRef={ref}
unmountOnExit
>
<>
<WineDetailPage
ref={ref}
setToggleForm={setToggleForm}
/>
<CheckIn />
</>
</CSSTransition>
<CSSTransition
in={toggleForm}
timeout={800}
classNames='wine_edit_form'
unmountOnExit
nodeRef={ref}
>
<WineForm
ref={ref}
setToggleForm={setToggleForm}
method={'PUT'}
title='Edit Wine'
/>
</CSSTransition>
</>
);
In order to access all of the wines, the search feature was vital. At first, I created separate routes accepting parameter variables to search based on name, rating, and price. Unfortunately, it became messy and hard to read. As a result, I opted to use a query string from the useLocation
React hook instead of sending search parameters with useParams
hook.
Below the route to query a wine from sequelize, uses the query string to extract variables for the search string, order, attribute, and page number:
router.get(
'/',
asyncHandler(async (req, res, next) => {
let { search, page, attribute, order: orders } = req.query;
if (!page) page = 1;
const offset = limitPerPage * (page - 1);
let { where, order } = createQueryOptions(attribute, orders);
if (search) {
where = {
...where,
name: {
[Op.iLike]: `%${search}%`
}
};
}
const wines = await Wine.findAndCountAll({
offset: offset,
limit: limitPerPage,
where: where ? where : {},
order: order ? order : [],
});
return res.json({ ...wines, offset })
})
);
It is also excessive to show more than 5k wines on a page, which can lead to excessive overhead and a slower response time.
In response, I created a Pagination component to calculate the offset number per page, and passed as a query string to the api route:
const { search: searchString } = useLocation();
let { attribute, order, search } = queryString.parse(searchString);
if (!attribute) attribute = 'name';
if (!order) order = 'desc';
let numberOfPages = Math.ceil(numberOfResults / itemsPerPage);
if (numberOfPages > pageLimit) numberOfPages = pageLimit;
if (!numberOfPages) return null;
const pageNumbers = [...Array(numberOfPages).keys()];
-
Feed Page - show most recent reviews
-
Favorite - like wines and add to feed page
-
Friends - add friends and display their reviews on feed page
-
Wineries - CRUD for wineries