Skip to content

Commit

Permalink
Hide solutions (#65)
Browse files Browse the repository at this point in the history
* implemented hiding solutions to unsolved challenges

* solutions to challenge dropdown

* implemented hiding and filtering by solution

* admin can filter solutions by any challenge

* pr nits addressed
  • Loading branch information
Vladmidir authored Apr 30, 2024
1 parent ca8be17 commit c653b18
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 12 deletions.
7 changes: 5 additions & 2 deletions client/src/pages/Challenge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ const Challenge = () => {
<Button
className="m-1"
target="_blank"
href={"/solutions/challenge/" + id}>
href={"/solutions"}>
View Solutions
</Button>
<Button className="m-1" onClick={() => setShowInstructions(true)}>
Expand All @@ -181,10 +181,13 @@ const Challenge = () => {
<Button
className="m-1"
target="_blank"
href={"/solutions/challenge/" + id}
href={"/solutions"}
>
View Solutions
</Button>
<Button className="m-1" onClick={() => setShowInstructions(true)}>
Instructions
</Button>
</ButtonToolbar>
</Row>
)
Expand Down
105 changes: 101 additions & 4 deletions client/src/pages/Solutions.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,49 @@
import { useState, useEffect } from "react";
import { Container, Row, Col } from "react-bootstrap";
import { Container, Row, Col, Form } from "react-bootstrap";
import SolutionCard from "../components/SolutionCard.tsx";
import { SolutionData } from "../types/SolutionData.ts";
import MasonryGrid from "../components/MasonryGrid.tsx";

const Solutions = () => {
const [solutions, setSolutions] = useState<SolutionData[]>([]);
const [showingSolutions, setShowingSolutions] = useState<SolutionData[]>([]);
const [challengeNames, setChallengeNames] = useState<string[]>([]);
const [solvedChallenges, setSolvedChallenges] = useState<string[]>([]);
const [thisUser, setThisUser] = useState<{username: string, role: string}>({username: "", role: ""});


// fetch the username and all solved challenges (we need the challenge titles)
useEffect(() => {
fetch("/api/users/whoami")
.then((res) => {
if (!res.ok) {
throw new Error(res.statusText);
}
return res.json() as Promise<{ username: string, role: string }>;
})
.then((data) => {
setThisUser(data);
console.log("fetching: /api/users/" + data.username + "/solvedChallenges")
return fetch("/api/users/" + data.username + "/solvedChallenges")
})
.then((res: Response) => {
if (!res.ok) {
throw new Error(res.statusText);
}
return res.json() as Promise<string[]>;
})
.then((data) => {
console.log(data);
console.log("Solved challenges");
setSolvedChallenges(data);
})
.catch((err) => {
console.error(err);
});
}, []);

// fetch all available solutions
useEffect(() => {
// Fetching solutions data
fetch("/api/solutions")
.then((resp) => resp.json())
.then((data: SolutionData[]) => {
Expand All @@ -19,12 +55,73 @@ const Solutions = () => {
});
}, []);

// fetch all challenges (we need the challenge titles)
useEffect(() => {
fetch("/api/challenges")
.then((res) => {
if (!res.ok) {
throw new Error(res.statusText);
}
return res.json() as Promise<{ id: number; title: string }[]>;
})
.then((data) => {
console.log(data);
setChallengeNames(data.map((challenge) => challenge.title));
})
.catch((err) => {
console.error(err);
});
}, []);

useEffect(() => {
setShowingSolutions(solutions);
}, [solutions]);

return (
<Container>
<h1 className="mt-5 mb-4">Solutions</h1>
<header>
<Row className="my-2">
<Col>
<h1 className="fs-2">Solutions</h1>
<h2 className="fs-5">View the solutions from the community!</h2>
</Col>
<Col>
<Form className="mt-1">
<Form.Group controlId="filterByChallenge">
<Form.Label>Solutions to a challenge</Form.Label>
<Form.Control
as="select"
onChange={(e) => {
const challengeTitle = e.target.value;
if (challengeTitle === "All") {
setShowingSolutions(solutions);
} else {
setShowingSolutions(
solutions.filter(
(solution) => solution.challengeTitle === challengeTitle,
),
);
}
}}
>
<option>All</option>
{challengeNames.map((challengeName) => (
<option
disabled={!(thisUser.role === "admin") && !solvedChallenges.includes(challengeName)}
key={challengeName}
>
{challengeName}
</option>
))}
</Form.Control>
</Form.Group>
</Form>
</Col>
</Row>
</header>
{solutions.length !== 0 ? (
<MasonryGrid sm={1} lg={3}>
{solutions.map((solution) => (
{showingSolutions.map((solution) => (
<Col key={solution.id} className="mb-4">
<SolutionCard
title={solution.title}
Expand Down
1 change: 1 addition & 0 deletions client/src/types/SolutionData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export type SolutionData = {
createdAt: string;
updatedAt: string;
User: UserData;
challengeTitle: string;
};
71 changes: 65 additions & 6 deletions server/controllers/SolutionController.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const STORAGE_CONFIG = require("../storage_config.json");
const db = require("../models/index");
const { HandledError } = require("../middleware/ErrorHandlingMiddleware");
const { AITA } = require("../AI/AITA.js");
const { where } = require("sequelize");
const Solution = db.Solution;
const Comment = db.Comment;
const Challenge = db.Challenge;
Expand All @@ -28,18 +29,76 @@ exports.getUserSolutions = async (req, res) => {


exports.getAll = async (req, res) => {
// eager load the user data
const solutions = await Solution.findAll({
limit: 50,
include: { model: User, as: "User" },
});
if (req.user.role === "admin") { // don't hide any solutions
// eager load the user data
const solutions = await Solution.findAll({
// limit: 50,
include: { model: User, as: "User" },
});

// append the challenge title to each solution
for (let i = 0; i < solutions.length; i++) {
const solution = solutions[i];
const challenge = await Challenge.findByPk(solution.challengeId);
solution.dataValues.challengeTitle = challenge.title;
}

res.status(200).json(solutions);
}
else { // only return the solutions to the challenges that the user has solved
// find all the solutions belonging to the user
const userSolutions = await Solution.findAll({
where: { userId: req.user.username },
});

const solvedChallengeIds = [];
const solutions = [];

// make a list of challenge id's that the user has solved
for (let i = 0; i < userSolutions.length; i++) {
if(!solvedChallengeIds.includes(userSolutions[i].challengeId)) {
solvedChallengeIds.push(userSolutions[i].challengeId);
}
}

// find all the solutions to the challenges that the user has solved
for (let i = 0; i < solvedChallengeIds.length; i++) {
const challengeId = solvedChallengeIds[i];
const challengeSolutions = await Solution.findAll({
where: { challengeId },
include: { model: User, as: "User" },
});
solutions.push(...challengeSolutions);
}

// append the challenge title to each solution
for (let i = 0; i < solutions.length; i++) {
const solution = solutions[i];
const challenge = await Challenge.findByPk(solution.challengeId);
solution.dataValues.challengeTitle = challenge.title;
}

// sort the solutions by date created
solutions.sort((a, b) => {
return b.createdAt - a.createdAt;
});

res.status(200).json(solutions);

}

res.status(200).json(solutions);
};

exports.get = async (req, res) => {
const { id } = req.params;
const solution = await Solution.findByPk(id);
// append the challenge title to the solution
const challenge = await Challenge.findByPk(solution.challengeId);
solution.dataValues.challengeTitle = challenge.title;
//append the user data to the solution
const user = await User.findByPk(solution.userId);
solution.dataValues.User = user;

res.status(200).json(solution);
};

Expand Down
19 changes: 19 additions & 0 deletions server/controllers/UserController.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,25 @@ exports.getMe = async (req, res) => {
};


exports.getSolvedChallengesTitles = async (req, res) => {
const solutions = await db.Solution.findAll({
where: {
userId: req.params.username,
},
include: { model: db.Challenge, as: "Challenge" },
});

const solvedNames = [];
for (let i = 0; i < solutions.length; i++) {
// make sure the challenge is not already in the list
if (!solvedNames.includes(solutions[i].Challenge.title)) {
solvedNames.push(solutions[i].Challenge.title);
}
}

res.status(200).json(solvedNames);
}

exports.get = async (req, res) => {
const { username } = req.params;

Expand Down
3 changes: 3 additions & 0 deletions server/routes/UserRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ router.get("/whoami", User.getMe);
// Get a user from the database.
router.get("/:username", User.get);

// get the names of the challenges that a user has solved
router.get("/:username/solvedChallenges", User.getSolvedChallengesTitles);

// USE FOR ADDING ADMINS
router.post("/", checkRole(["admin"]), User.create);

Expand Down

0 comments on commit c653b18

Please sign in to comment.