Getting Started : https://sunrise-yoga.onrender.com/
Sunrise Yoga is an app that allow users to access yoga tutorials through search and bookmark features based on their preferences. The app also leverages Nodemailer, Formik, and Yup to enhance the user experience and ensure a seamless, userfriendly interface.
- As a visitor, I want to view all the yoga guide on the homepage.
- As a visitor, I want to view individual yoga tutorials.
- As a visitor, I want to search and filter the yoga tutorials according to my preferences.
- As a user, I want to update my user details.
- As a user, I want to bookmark my favorite yoga tutorials.
- As a user, I want to receive welcome promotion through email.
Method | Path | Purpose |
---|---|---|
get | /api/users/signup | R View Sign up Page |
post | /api/users/signup | C Sign up User |
post | /api/users/signupemail | C Send Sign Up Email |
get | /api/users/login | R View Login Page |
post | /api/users/login | C Login User (Authorisation) |
get | /api/users/account | R View Account Page |
get | /api/users/edit | R View Edit Account Page |
put | /api/users/edit | U Edit User Details |
get | /api/users/checkbookmark | R Check Bookmark Status |
get | /api/users/bookmarks | R Bookmark Tutorials |
Method | Path | Purpose |
---|---|---|
get | /api/ | R View All Yoga Tutorials |
get | /api/yogas/:id | R View Individual Yoga Tutorial |
get | /api/yogas/?duration=&intensity=&name= | R Search & Filter Yoga Tutorials |
get | /api/users/bookmarks | R View Bookmark Tutorials |
post | /api/yogas/:id | C Bookmark Yoga Tutorials |
delete | /api/yogas/:id | D Unbookmark Yoga Tutorials |
get | /* | R Page Do Not Exist |
Front End | Back End | Database | Deployment |
---|---|---|---|
React (MUI) | Node.JS & Express | PostgreSQL | Render |
- React
- React Material UI
- Node.js
- Express Framework
- PostgreSQL
- JavaScript
- TypeScript
- Render deployment
- Git & GitHub
- Other libraries: Dayjs, Yup, Formik, Nodemailer, Jsonwebtoken
- Pagination
- Appointment Booking
- Increased Type of Contents
- Finalising PostgreSQL Tables
CREATE TABLE yoga (
id SERIAL PRIMARY KEY UNIQUE,
title VARCHAR NOT NULL,
intensity VARCHAR NOT NULL,
duration INT NOT NULL,
thumbnailimageurl VARCHAR UNIQUE NOT NULL,
videoembeddedurl VARCHAR UNIQUE NOT NULL,
description VARCHAR NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE users (
id SERIAL PRIMARY KEY UNIQUE,
name VARCHAR NOT NULL,
email VARCHAR UNIQUE NOT NULL,
birthday DATE NOT NULL,
password VARCHAR NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE usersyoga (
id SERIAL PRIMARY KEY,
users_id INT,
FOREIGN KEY (users_id) REFERENCES users(id),
yoga_id INT,
FOREIGN KEY (yoga_id) REFERENCES yoga(id)
);
- Validate input with Formik & Yup
const validateSchema = Yup.object().shape({
name: Yup.string().required("This field is required"),
email: Yup.string()
.email("Please enter a valid email")
.required("This field is required"),
birthday: Yup.date().required("This field is required"),
password: Yup.string()
.required("This field is required")
.min(5, "Pasword must be 5 or more characters")
.matches(/\d/, "Password should contain at least one number")
.matches(
/(?=.*[a-z])(?=.*[A-Z])\w+/,
"Password ahould contain at least one uppercase and lowercase character"
)
.matches(
/[`!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/,
"Password should contain at least one special character"
),
});
const formik = useFormik({
initialValues: {
name: "",
email: "",
birthday: "",
password: "",
},
validationSchema: validateSchema,
onSubmit: (values, { resetForm }) => {
console.log(values);
setTimeout(() => {
resetForm();
}, 1000 * 2);
},
});
return (
<Box className="SignUpFormContainer">
<form autoComplete="off" onSubmit={handleSubmit} className="SignUpForm">
<Typography variant="h5">Sign Up a new Account </Typography>
<Box className="R1">
<TextField
id="outlined-basic"
variant="outlined"
type="text"
label="Name"
name="name"
value={formik.values.name}
helperText={formik.errors.name ? formik.errors.name : ""}
InputLabelProps={{
style: { color: "#000000" },
}}
onChange={formik.handleChange}
className="my-textfield"
required
/>
</Box>
- Filter yoga tutorials according to preferences
export default function SearchBar({ yogas }: YogaCardProps) {
const [filters, setFilters] = useState([]);
const [searchParams, setSearchParams] = useSearchParams();
const durations = [...new Set(yogas.map((yoga) => yoga.duration))].sort();
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
const duration = searchParams.get("duration");
if (duration || intensity || name)
fetch(
`/api/yogas?duration=${duration}&intensity=${intensity}&name=${name}`,
{
signal,
}
)
.then((res) => res.json())
.then((data) => setFilters(data.filters));
//* useEffect return -> cleanup function
return () => {
console.log("unmount");
controller.abort();
};
}, [searchParams]);
const filteredYogas = yogas.filter(
(yoga) =>
searchParams.get("duration") === "" ||
yoga.duration === Number(searchParams.get("duration"))
);
const handleDuration = (event: any) => {
const duration = event.target.value;
setSearchParams({ ...Object.fromEntries(searchParams), duration });
};
return (
<>
<Box sx={{ bgcolor: "background.paper", p: 0.2, mt: 1 }}>
<FormControl
sx={{
m: 1,
minWidth: 100,
display: "flex",
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
flexWrap: "wrap",
"& > *": { mx: 1, my: 2 },
}}
>
<FormControl sx={{ m: 2, minWidth: 100, display: "inline" }}>
<InputLabel id="demo-simple-select-autowidth-label">
Duration
</InputLabel>
<Select
sx={{ minWidth: "100px" }}
labelId="demo-simple-select-autowidth-label"
id="demo-simple-select-autowidth"
value={searchParams.get("duration")}
onChange={handleDuration}
autoWidth
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{durations.map((duration) => (
<MenuItem key={duration} value={duration}>
{duration}
</MenuItem>
))}
</Select>
</FormControl>
</FormControl>
</Box>
<FilteredYogas yogas={yogas} filteredYogas={filteredYogas} />
</>
);
}
- Check bookmark status during initial fetch
const [isBookmarked, setIsBookmarked] = useState(false);
const { id } = useParams<{ id?: string }>();
useEffect(() => {
const token = localStorage.getItem("token");
if (!token) {
return;
}
const fetchUser = async () => {
try {
const response = await fetch("/api/users/checkbookmark", {
headers: {
Authorization: `Bearer ${token}`,
},
});
const userYoga = await response.json();
if (userYoga.length === 0) {
setIsBookmarked(false);
}
const isBookmarked = userYoga.some(
(item: UserYoga) => item.yoga_id === Number(id)
);
setUserYoga(userYoga);
setIsBookmarked(isBookmarked);
} catch (error) {
console.error(error);
}
};
fetchUser();
}, [id, setIsBookmarked]);
- Retrieve data from database
const showBookmarkYogas = async (req, res) => {
if (!req.headers.authorization) {
return res.status(401).json({ message: "Authorization header is missing" });
}
const token = req.headers.authorization.split(" ")[1];
const decodedToken = jwt.verify(token, process.env.JWT_SECRET);
const users_id = decodedToken.user.id;
pool.connect((err, client, done) => {
if (err) {
console.error("Error acquiring client", err.stack);
return res.status(500).json({ message: "Error acquiring client" });
}
client.query(
`SELECT *FROM yoga LEFT JOIN usersyoga ON yoga.id = usersyoga.yoga_id LEFT JOIN users ON usersyoga.users_id = users.id WHERE users.id = '${users_id}'`,
(err, result) => {
if (err) {
console.error("Error executing query", err.stack);
return res.status(500).json({ message: "Error executing query" });
}
res.json(result.rows);
client.release();
}
);
});
};
- Client side routing
{user ? <UserHeader setUser={setUser} /> : <Header />}
<CssBaseline />
<Routes>
<Route path="/users/signup" element={<SignUp />} />
<Route path="/users/login" element={<Login setUser={setUser} />} />
<Route path="/users/account" element={<AccountPage user={user} />} />
<Route
path="/users/edit"
element={<EditAccount user={user} setUser={setUser} />}
/>
<Route path="/users/bookmarks" element={<YogaBookmarksPage />} />
<Route path="/" element={<Homepage />} />
<Route path="/yogas/:id" element={<YogaPage />} />
<Route path="/*" element={<PageNotExist />} />
</Routes>
<Footer />
- Getting familiar with api fetch + route + controller functions
- Learning to use external libraries - Formik, Yup, MUI, Dayjs, Nodemailer
- Debugging issues patiently through tools such as Beekeeper & Insomnia
- Key Benefits of TypeScript
- Type annotations improve code maintainability and prevent errors
- Better developer experience with code completion and refactoring tools
- Supports modern ECMAScript features and provides better browser compatibility
- Better tooling and integration with popular frameworks
- Enables easier code sharing and collaboration between teams
- Offers better scalability and maintainability for large-scale applications
- Provides functional programming features
- Provides better integration with interfaces and generics
- Learning the difference between MongoDB and SQL
Difference | PostgreSQL | MongoDB |
---|---|---|
Database Type | Relational database management system | NoSQL document-oriented database |
Schema | Strict schema requirements | Flexible schema with dynamic document structures |
Data Storage | Tables with predefined schema | JSON-like documents without a fixed schema |
Query Language | SQL (Structured Query Language) | MongoDB Query Language (MQL) |
Joins and Transactions | Supports joins and complex transactions | Does not support joins and complex transactions |
Use Case | Better suited for complex queries and analytical applications | Better suited for rapid development and scalability |
Performance | Offers better performance for large datasets and high-volume transactions | Offers better performance for read-heavy workloads and high concurrency |
Ecosystem | Has strong community support and a rich set of add-ons and extensions | Has a less mature ecosystem with fewer add-ons and extensions |
Inspirations: Mindline | adplist | Do Yoga with Me | Airbnb | Headspace (Free Trial Available)
MUI Template: Material UI
MUI Theme Creator: zenoo
ERD Diagram: drawSQL
Quote API: Inspiration
Airtable API: Inspiration
Formik & Yup Tutorial: Shahed Nasser
SQL Tutorial (freeCodeCamp): Amigos Code | Giraffe Academy
Presentation Slide: Google Slides