Skip to content

Commit

Permalink
Merge pull request #115 from ryanchua00/question-tagging-updated
Browse files Browse the repository at this point in the history
Add Question Filtering
  • Loading branch information
bokuanT authored Nov 14, 2023
2 parents d9bf58c + ce024e3 commit ff91adc
Show file tree
Hide file tree
Showing 9 changed files with 421 additions and 11 deletions.
2 changes: 1 addition & 1 deletion frontend/components/ProfilePage/HistoryTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ const HistoryTable = ({ username }: HistoryTableProps) => {
</Stack>
<Box>
<Pagination
count={3}
count={displayedResponses.length}
page={questionNumber}
color="primary"
onChange={handleChange}
Expand Down
42 changes: 42 additions & 0 deletions frontend/components/QuestionsPage/FilterBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// FilterBar.tsx
import * as React from 'react';
import FilterSelector from './FilterSelector'; // Import your FilterSelector component
import { Box } from '@mui/material';

interface FilterBarProps {
categoryOptions: { value: number; label: string }[];
difficultyOptions: { value: number; label: string }[];
onApplyFilters: (selectedCategories: string[], selectedDifficulties: string[]) => void; // Update types here
onResetFilters: () => void;
}

const FilterBar: React.FC<FilterBarProps> = ({
categoryOptions,
difficultyOptions,
onApplyFilters,
onResetFilters,
}) => {
const [selectedCategories, setSelectedCategories] = React.useState<string[]>([]); // Update type here
const [selectedDifficulties, setSelectedDifficulties] = React.useState<string[]>([]); // Update type here

const handleApplyFilters = () => {
onApplyFilters(selectedCategories, selectedDifficulties);
};

const handleResetFilters = () => {
setSelectedCategories([]);
setSelectedDifficulties([]);
onResetFilters();
};

return (
<Box sx={{paddingX: 2, paddingY: 1}}>
<FilterSelector options={categoryOptions} selectedValues={selectedCategories} onChange={setSelectedCategories} />
<FilterSelector options={difficultyOptions} selectedValues={selectedDifficulties} onChange={setSelectedDifficulties} />
<button onClick={handleApplyFilters}>Apply Filters</button>
<button onClick={handleResetFilters}>Reset Filters</button>
</Box>
);
};

export default FilterBar;
199 changes: 199 additions & 0 deletions frontend/components/QuestionsPage/FilterSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import * as React from 'react';
import {
Select as BaseSelect,
SelectProps,
selectClasses,
SelectRootSlotProps,
} from '@mui/base/Select';
import { Option as BaseOption, optionClasses } from '@mui/base/Option';
import { Popper as BasePopper } from '@mui/base/Popper';
import { styled } from '@mui/system';
import UnfoldMoreRoundedIcon from '@mui/icons-material/UnfoldMoreRounded';

interface FilterSelectorProps {
options: { value: number; label: string }[];
selectedValues: string[]; // Change this line to use string[]
onChange: (value: string[]) => void; // Change this line to use string[]
}

export default function FilterSelector({ options, selectedValues, onChange }: FilterSelectorProps) {
return (
<MultiSelect value={selectedValues} onChange={(event, labels) => onChange(labels)}>
{options.map((option) => (
<Option key={option.value} value={option.label}>
{option.label}
</Option>
))}
</MultiSelect>
);
}


const MultiSelect = React.forwardRef(function CustomMultiSelect(
props: SelectProps<string, true>, // Change the type to use string
ref: React.ForwardedRef<any>,
) {
const slots: SelectProps<string, true>['slots'] = {
root: Button,
listbox: Listbox,
popper: Popper,
...props.slots,
};

return <BaseSelect {...props} multiple ref={ref} slots={slots} />;
});

const blue = {
100: '#DAECFF',
200: '#99CCF3',
400: '#3399FF',
500: '#007FFF',
600: '#0072E5',
900: '#003A75',
};

const grey = {
50: '#F3F6F9',
100: '#E5EAF2',
200: '#DAE2ED',
300: '#C7D0DD',
400: '#B0B8C4',
500: '#9DA8B7',
600: '#6B7A90',
700: '#434D5B',
800: '#303740',
900: '#1C2025',
};

const Button = React.forwardRef(function Button<
TValue extends {},
Multiple extends boolean,
>(
props: SelectRootSlotProps<TValue, Multiple>,
ref: React.ForwardedRef<HTMLButtonElement>,
) {
const { ownerState, ...other } = props;
return (
<StyledButton type="button" {...other} ref={ref}>
{other.children}
<UnfoldMoreRoundedIcon />
</StyledButton>
);
});

const StyledButton = styled('button', { shouldForwardProp: () => true })(
({ theme }) => `
font-family: IBM Plex Sans, sans-serif;
font-size: 0.875rem;
box-sizing: border-box;
min-width: 320px;
padding: 8px 12px;
border-radius: 8px;
text-align: left;
line-height: 1.5;
background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'};
border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
position: relative;
box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 120ms;
&:hover {
background: ${theme.palette.mode === 'dark' ? grey[800] : grey[50]};
border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]};
}
&.${selectClasses.focusVisible} {
outline: 0;
border-color: ${blue[400]};
box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[600] : blue[200]};
}
& > svg {
font-size: 1rem;
position: absolute;
height: 100%;
top: 0;
right: 10px;
}
`,
);

const Listbox = styled('ul')(
({ theme }) => `
font-family: IBM Plex Sans, sans-serif;
font-size: 0.875rem;
box-sizing: border-box;
padding: 6px;
margin: 12px 0;
min-width: 320px;
border-radius: 12px;
overflow: auto;
outline: 0px;
background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'};
border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
box-shadow: 0px 2px 6px ${
theme.palette.mode === 'dark' ? 'rgba(0,0,0, 0.50)' : 'rgba(0,0,0, 0.05)'
};
`,
);

const Option = styled(BaseOption)(
({ theme }) => `
list-style: none;
padding: 8px;
border-radius: 8px;
cursor: default;
transition: border-radius 300ms ease;
&:last-of-type {
border-bottom: none;
}
&.${optionClasses.selected} {
background-color: ${theme.palette.mode === 'dark' ? blue[900] : blue[100]};
color: ${theme.palette.mode === 'dark' ? blue[100] : blue[900]};
}
&.${optionClasses.highlighted} {
background-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
}
@supports selector(:has(*)) {
&.${optionClasses.selected} {
& + .${optionClasses.selected} {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
&:has(+ .${optionClasses.selected}) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
}
&.${optionClasses.highlighted}.${optionClasses.selected} {
background-color: ${theme.palette.mode === 'dark' ? blue[900] : blue[100]};
color: ${theme.palette.mode === 'dark' ? blue[100] : blue[900]};
}
&.${optionClasses.disabled} {
color: ${theme.palette.mode === 'dark' ? grey[700] : grey[400]};
}
&:hover:not(.${optionClasses.disabled}) {
background-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
}
`,
);

const Popper = styled(BasePopper)`
z-index: 1;
`;
81 changes: 79 additions & 2 deletions frontend/components/QuestionsPage/QuestionsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import Question from "@/types/Question";
import { useAuth } from "@/contexts/AuthContext";
import LoginPage from "../LoginPage/LoginPage";
import { messageHandler } from "@/utils/handlers";
import { Box, Fab } from "@mui/material";
import { Alert, Box, Fab, Snackbar } from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import AddQuestionModal from "./AddQuestionModal";
import ConfirmResetDialog from "./ConfirmResetDialog";
import FilterBar from "./FilterBar";

const QuestionPage = () => {
// get user's role
Expand All @@ -33,11 +34,16 @@ const QuestionPage = () => {
setQuestionModalOpen(false);
};

// State for filter bar
const [categoryOptions, setCategoryOptions] = useState<{ value: number; label: string }[]>([]);
const [difficultyOptions, setDifficultyOptions] = useState<{ value: number; label: string }[]>([]);

// Stuff for question table
const [questions, setQuestions] = useState<Question[]>([]);
// state is changed on adding, editing or deleting qns, useEffect is called
// and refreshes the table
const [refresh, setRefresh] = useState(false);
const [openAlert, setOpenAlert] = useState(false);

const addQuestion = async (newQuestion: Question) => {
// Add the new question to the backend and then update the state
Expand Down Expand Up @@ -101,6 +107,32 @@ const QuestionPage = () => {
setOpen: setOpenResetDialog,
handleConfirm: setToDefaultQns,
};
// filter stuff
const handleApplyFilters = async (selectedCategories: string[], selectedDifficulties: string[]) => {
// Handle applying filters, e.g., fetching filtered data
console.log('Applying filters:', selectedCategories, selectedDifficulties);
// Update logic as needed

await fetchPost("/api/questions/filter", {
category: selectedCategories,
difficulty: selectedDifficulties
}).then((res) => {
console.log(res);
if (res.status === 200) {
setQuestions(res.data);
} else {
console.error("Failed to apply filters");
setOpenAlert(true);
}
});

};

const handleResetFilters = () => {
// Handle resetting filters, e.g., resetting the state or fetching all data
console.log('Resetting filters');
};


useEffect(() => {
const fetchQuestions = async () => {
Expand All @@ -110,6 +142,36 @@ const QuestionPage = () => {
fetchQuestions();
}, [refresh]);

useEffect(() => {
const categories = new Set<string>();
const difficulties = new Set<string>();
questions.forEach((question) => {
categories.add(question.category);
difficulties.add(question.difficulty);
});
// Note: This sets the filters to the tags of the remaining questions.
// While this works when all questions are present, after filtering, the options are reduced to that of the filtered questions
// If this is an unintended consequence
// Consider using a different variable to maintain the state of all the categories and difficulties at the start!
// If not, great work!

// Also, there are impossible combinations, e.g bit manipulation and hard.
// For this case, I added an alert!

const categoryOptions = Array.from(categories).map((category, index) => ({
value: index,
label: category,
}));
const difficultyOptions = Array.from(difficulties).map((difficulty, index) => ({
value: index,
label: difficulty,
}));
setCategoryOptions(categoryOptions);
setDifficultyOptions(difficultyOptions);
}, [questions]);

console.log('Current value of questions:', questions);

if (!user) {
return <LoginPage />;
}
Expand All @@ -122,7 +184,13 @@ const QuestionPage = () => {
addQuestion={addQuestion}
/>
)}
<Box display="flex" maxHeight="80vh" padding={2}>
<FilterBar
categoryOptions={categoryOptions}
difficultyOptions={difficultyOptions}
onApplyFilters={handleApplyFilters}
onResetFilters={handleResetFilters}
/>
<Box display="flex" padding={2}>
<QuestionTable
questions={questions}
deleteQuestion={deleteQuestion}
Expand Down Expand Up @@ -166,6 +234,15 @@ const QuestionPage = () => {
<ConfirmResetDialog {...confirmDialogProps} />
</>
)}
<Snackbar
open={openAlert}
autoHideDuration={6000}
onClose={() => setOpenAlert(false)}
>
<Alert onClose={() => setOpenAlert(false)} severity="error" sx={{ width: '100%' }}>
Question with specified filters does not exist!
</Alert>
</Snackbar>
</main>
);
};
Expand Down
Loading

0 comments on commit ff91adc

Please sign in to comment.