Oraql is a website for users to ask and answer questions about supernatural phenomena. It is inspired by Quora.
Try asking and answering questions at our live site: Oraql
| MVP Feature List | Database Schema | API Documentation | Frontend Routes | User Stories |
-
Clone this repository
git clone [email protected]:starsabhi/Oraql.git
-
Install dependencies
npm install
-
Create a .env file based on the .env.example given
-
Setup your username and database based on what you setup in your .env
-
Migrate and Seed models
npx dotenv sequelize db:migrate
&&npx dotenv sequelize db:seed:all
-
Start the app using:
npm start
-
You can use the Demo user or create an account
Oraql is a question-and-answer website for users to post/edit/delete questions and answers. Logged in sers can dynamically edit and delete answers without redirecting.
Logged out users can:
- View Questions and Associated Answers
- Search for Questions
- View Questions by Topics/Tags
Logged in users can:
- Add/Edit/Delete Questions
- Add/Edit/Delete Answers
- Search for Questions
- View Questions by Topics/Tags
- Comments on Answers:
- Logged-in users can add comments on answers.
- Logged-in users can upvote comments/answers/questions.
- Likes on Questions/Answers/Comments
- Logged-in users can remove their own like from questions/answers/comments.
- All users can see how many users have liked a question/answer/comment.
- User profiles
- One of our first challenges was search bar functionality: how to process an input and search for related information in the database. Our solution is to segment the input string into a list of words and query the question.content column for data containing any of the words.
router.post(
"/results",
inputValidators,
asyncHandler(async (req, res) => {
let words = req.body.searchText.trim().split(/\s+/);
const validatorErrors = validationResult(req);
if (!validatorErrors.isEmpty()) {
backURL = req.header("Referer") || "/";
return res.redirect(backURL);
}
words = words.map((word) => `%${word}%`);
const questions = await db.Question.findAll({
where: {
content: {
[Op.iLike]: {
[Op.any]: words,
},
},
},
include: [{ model: db.User }, { model: db.Tag }],
order: [["createdAt", "DESC"]],
});
res.render("search-results", {
title: "Search Results",
questions,
search: req.body.searchText,
});
})
);
- The other challenge was how to display a question and a list of answers on the question which are sorted by createdAt date. Our solution is to perform an eager loading to query of several different models, including Question, Answer and User, at once. Then we sort the answers of a question by createdAt and display the sorted answer on the page.
router.get(
"/:id(\\d+)",
asyncHandler(async (req, res) => {
const questionId = parseInt(req.params.id, 10);
const question = await db.Question.findByPk(questionId, {
include: [
db.User,
{
model: db.Answer,
include: db.User,
},
db.Tag,
],
});
const answers = question.Answers;
answers.sort((a, b) => {
const keyA = new Date(a.createdAt);
const keyB = new Date(b.createdAt);
return keyA < keyB ? -1 : 1;
});
answers.forEach((answer) => {
answer.date = answer.createdAt.toDateString();
});
res.render("question-detail", {
title: question.content,
question,
answers,
});
})
);
- custom validations on sign up input
const userValidators = [
check("username")
.exists({ checkFalsy: true })
.withMessage("Please provide a value for Username")
.isLength({ max: 20 })
.withMessage("Username must not be more than 20 characters long")
.custom((value) => {
return db.User.findOne({ where: { username: value } }).then((user) => {
if (user) {
return Promise.reject(
"The provided Username is already in use by another account"
);
}
});
})
.custom((value) => !/^ *$/.test(value))
.withMessage("Username cannot be empty"),
];
- Dynamically update answers without redirection
submitBtn.addEventListener("click", async (submitEvent) => {
submitEvent.preventDefault();
const contentValue = document.getElementById(
`${answerId}-edit-content`
).value;
const questionId = parseInt(
submitEvent.target.classList[0].split("-")[1],
10
);
const res = await fetch(`/questions/${questionId}/answers/${answerId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
content: contentValue,
}),
});
const data = await res.json();
if (data.message === "Success") {
let contentEle = document.getElementById(`answer-content-${answerId}`);
let lines = data.answer.content.split("\n");
lines = lines.map((line) => `<div>${line}</div>`);
contentEle.innerHTML = lines.join("");
form.classList.add("hidden");
answerContent.classList.remove("hidden");
editAnswerButton.classList.remove("hidden");
deleteAnswerButton.classList.remove("hidden");
} else {
if (data.message === "Failure") {
const errorDiv = document.getElementById(`edit-errors-${answerId}`);
errorDiv.innerHTML = data.errors[0];
}
}
});