Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zh/add search bar #69

Open
wants to merge 28 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
39748c5
Display search
zkhussain Sep 22, 2021
4f8e577
Add api call
zkhussain Sep 22, 2021
6a1a0d1
Handle search submit
zkhussain Sep 22, 2021
aa4dbd7
Add style for SearchBar
zkhussain Sep 22, 2021
62bd960
Create api call for search modules
zkhussain Sep 22, 2021
e45d0e8
Update Explore to display search bar
zkhussain Sep 22, 2021
2614deb
Add Tooltip
zkhussain Sep 22, 2021
e94ef23
Remove Tooltip link
zkhussain Oct 6, 2021
aab98e9
Add search bar to a user
zkhussain Oct 6, 2021
8af8d79
Improve search
zkhussain Oct 20, 2021
500bdb6
Merge branch 'dev' into zh/add-search-bar
zkhussain Nov 3, 2021
6e47d9a
Update Tooltip
zkhussain Nov 4, 2021
e1cfd55
Merge remote-tracking branch 'origin/jh/tag-bar' into zh/add-search-bar
zkhussain Dec 12, 2021
b6586f1
Update search to display advanced options
zkhussain Dec 15, 2021
6e510a0
Fix code smell
zkhussain Dec 15, 2021
67b8693
Add module difficulty
zkhussain Jan 3, 2022
588730d
Update TagEditor to allow custom styles
zkhussain Jan 3, 2022
1ca9a5a
Make search options type
zkhussain Jan 3, 2022
87fabba
Update modules search to display tags
zkhussain Jan 3, 2022
d1eba2e
Update to add helpers for modules search
zkhussain Jan 3, 2022
1f51254
Update api calls for modules search
zkhussain Jan 3, 2022
f276c60
Add modules filter
zkhussain Jan 3, 2022
e108484
Update module display pages to show searched modules
zkhussain Jan 3, 2022
c83dc15
Change p to not apply on root
zkhussain Jan 3, 2022
977fa9d
Update module search and filter
zkhussain Jan 4, 2022
2d225ec
Fix code smells and add filter tooltip
zkhussain Jan 4, 2022
eacd63d
Fix linting issues
zkhussain Jan 23, 2022
9fcfb3b
Merge branch 'dev' into zh/add-search-bar
jasekiw Apr 4, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {UploadByUrlForm, UploadForm, uploadFormToFormData} from '../components/V
import {Maintenance} from '../types/Maintenance';
import {Tag} from '../types/Tag';
import {SystemMessage} from '../types/SystemMessage';
import {SearchOptions} from 'types/SearchOptions';

let api = makeAxios(process.env.REACT_APP_API_URL);

Expand Down Expand Up @@ -78,6 +79,18 @@ export async function getModule(id: number) {
return ( await api.get<Module>(`/modules/${id}`)).data;
}

export async function searchModules(searchTerm : string) {
return (await api.get<Module[]>(`/module/search/${searchTerm}`)).data;
}

export async function searchOptionsModules(searchInputs : SearchOptions) {
const searchParams = {
title: searchInputs.title, description: searchInputs.description,
difficulty: searchInputs.difficulty, tags: searchInputs.tags.map(t => t.name).toString()
};
return ( await api.get<Module[]>(`/module/search`, {params: searchParams}) ).data;
}

export async function getUserList() {
return ( await api.get<User[]>(`/user/`) ).data;
}
Expand Down Expand Up @@ -139,16 +152,44 @@ export async function getUserModules() {
export async function getUserModule(id: number) {
return handleResponse( await api.get<UserModule>(`/user-module/${id}`)).data;
}
export async function searchUserModules(searchTerm : string) {
return handleResponse( await api.get<UserModule[]>(`/user-module/search/${searchTerm}`)).data;
}

export async function searchOptionsUserModules(searchInputs : SearchOptions) {
const searchParams = {
title: searchInputs.title, description: searchInputs.description,
difficulty: searchInputs.difficulty, tags: searchInputs.tags.map(t => t.name).toString()
};
return handleResponse( await api.get<UserModule[]>(`/user-module/search`, {params: searchParams})).data;
}

export async function getEditorsModules() {
return handleResponse(await api.get<UserModule[]>(`module/modules-editor`)).data;
}

export async function getAdminModules() {
return handleResponse(await api.get<Module[]>(`module/modules-editor/admin`)).data;
}

export async function searchEditorsModules(searchTerm : string) {
return handleResponse( await api.get<UserModule[]>(`module/modules-editor/search/${searchTerm}`)).data;
}

export async function searchOptionsEditorsModules(searchInputs: SearchOptions) {
const searchParams = {
title: searchInputs.title, description: searchInputs.description,
difficulty: searchInputs.difficulty, tags: searchInputs.tags.map(t => t.name).toString()
};
return handleResponse(await api.get<UserModule[]>(`module/modules-editor/search`, {params: searchParams})).data;
}

export async function getUserLab(id: number) {
return handleResponse( await api.get<UserLab>(`/user-lab/${id}`)).data;
}

export async function getTags(name: string) {
return handleResponse( await api.get<Tag[]>(`/tag`)).data;
return handleResponse( await api.get<Tag[]>(`/tag/search/${name}`)).data;
}

export async function updateEndDateTime(id: number) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.card {
margin-bottom: 35px;
margin-bottom: 25px;
}

.card-footer a {
Expand Down
3 changes: 2 additions & 1 deletion src/components/CreatorsModuleCard/CreatorsModuleCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {WebState} from '../../redux/types/WebState';
import {connect} from 'react-redux';
import {isAuthenticated} from '../../redux/selectors/entities';
import {UserModule} from '../../types/UserModule';
import {getLocalDateTimeString} from '../../util';
import {getLocalDateTimeString, getModuleDifficultyLabel} from '../../util';
import CopyToClipboard from 'react-copy-to-clipboard';
import {Link} from 'react-router-dom';
import {IconButton} from '../util/IconButton/IconButton';
Expand Down Expand Up @@ -35,6 +35,7 @@ class CreatorsModuleCardComponent extends Component<CreatorsModuleCardProps > {
<div style={{marginBottom: '1rem'}}>
<b>Published: </b> {`${module.published}`} <br/>
<b>Module type: </b> {`${module.type}`} <br/>
<b>Difficulty: </b> {`${getModuleDifficultyLabel(module.difficulty)}`} <br/>
</div>
<Link style={{marginRight: '0.5rem'}} to={`/module/${module.specialCode}`}>Share Link</Link>
{<CopyToClipboard text={getModuleShareLink(module.specialCode)}>
Expand Down
7 changes: 4 additions & 3 deletions src/components/ModuleCard/ModuleCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {connect} from 'react-redux';
import {isAuthenticated} from '../../redux/selectors/entities';
import {Link} from 'react-router-dom';
import {UserModule} from '../../types/UserModule';
import {getLocalDateTimeString} from '../../util';
import {getLocalDateTimeString, getModuleDifficultyLabel} from '../../util';

interface ModuleCardProps extends ReturnType<typeof mapStateToProps> {
module: Module|UserModule;
Expand All @@ -33,6 +33,7 @@ class ModuleCardComponent extends Component<ModuleCardProps > {
</Card.Body>
<Card.Footer style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
<small className='text-muted'>{getLocalDateTimeString(module.updatedAt)}</small>
<small className='text-muted'>{getModuleDifficultyLabel(module.difficulty)}</small>
{this.getStartButton()}
</Card.Footer>
</Card>
Expand All @@ -44,11 +45,11 @@ class ModuleCardComponent extends Component<ModuleCardProps > {
if (this.props.buttonLink) {
return (
<Link to={this.props.buttonLink}>
<Button className='btn btn-primary' style={{width: 200}}>View</Button>
<Button className='btn btn-primary' style={{width: 100}}>View</Button>
</Link>
);
} else if (this.props.buttonAction) {
return (<Button onClick={this.props.buttonAction} className='btn btn-primary' style={{width: 200}}>View</Button>);
return (<Button onClick={this.props.buttonAction} className='btn btn-primary' style={{width: 100}}>View</Button>);
} else {
return null;
}
Expand Down
32 changes: 32 additions & 0 deletions src/components/ModulesSearch/ModulesFilter.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.filter-options {
position: absolute;
z-index: 1;
top: calc(75% + 6px);
right: 0;
width: 14rem;
padding: 1.5rem;
background-color: #FFF;
border-radius: 0.25rem;
box-shadow: 0 0 8px rgb(175, 175, 175);
}

.icon:hover {
cursor: pointer;
color: maroon;
}

.sort-option {
margin-top: 0.5rem;
}

.sort-option label {
padding-left: 0.5rem;
}

.hide {
display: none
}

.show {
display: block !important;
}
130 changes: 130 additions & 0 deletions src/components/ModulesSearch/ModulesFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import {faFilter} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {Container, Form} from 'react-bootstrap';
import React, {useRef, useState} from 'react';
import {connect} from 'react-redux';
import {isAdmin, isAuthenticated, isCreator} from 'redux/selectors/entities';
import {WebState} from 'redux/types/WebState';
import styles from './ModulesFilter.module.scss';
import {RoutePaths} from 'router/RoutePaths';
import {Module} from 'types/Module';
import {UserModule} from 'types/UserModule';
import {getAdminModules} from 'api';

export type Modules = Module[] | UserModule[];

interface ModulesFilterProps extends ReturnType<typeof mapUserStateToProps> {
modules: Modules;
loadModules(): void;
showSortedModules(modules: Modules): void;
}

const ModulesFilter = (props: ModulesFilterProps) => {

const creatorSortOptions = [
props.admin ? { name: 'All Modules', value: 1 } : {},
{ name: 'Type', value: 2},
{ name: 'Published', value: 3}
];

const sortOptions = [
...(props.creator || props.admin) && window.location.pathname === RoutePaths.contentCreator ?
[...creatorSortOptions] : [],
{ name: 'Name', value: 4 },
{ name: 'Difficulty', value: 5 },
{ name: 'Date', value: 6 }
];

const [sortOption, setSortOption] = useState(0);
const [filterOn, setFilterOn] = useState(false);

const focusRef = useRef<any>();

const onFilterClick = () => {
setFilterOn(!filterOn);
};

const onBlur = (e: any) => {
if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget)) {
setFilterOn(false);
}
};

const onSortChange = (e: any) => {
let selectedOption = parseInt(e.target.value, 10);

if (selectedOption === sortOption) {
selectedOption = 0;
}
setSortOption(selectedOption);
showSortedModules(selectedOption);
};

const showSortedModules = (option: number) => {
const modules: Module[] = props.modules[0]['module'] !== undefined ?
(props.modules as UserModule[]).map(um => um.module) : props.modules as Module[];

if (option === 0) // all options unchecked, load original
props.loadModules();

getSortedModules(option, modules).then(response => {
props.showSortedModules(response);
});
};

const getSortedModules = async (option: number, modules: Modules) => {
switch (option) {
case 1:
const allAdminModules = await getAdminModules();
return allAdminModules;
case 2:
return modules.sort( (a: any, b: any) => b.type.localeCompare(a.type) );
case 3:
return modules.sort( (a: any, b: any) => a.published - b.published );
case 4:
return modules.sort( (a: any, b: any) => a.name.localeCompare(b.name) );
case 5:
return modules.sort( (a: any, b: any) => a.difficulty - b.difficulty );
case 6:
return modules.sort( (a: any, b: any) => new Date(b?.updatedAt).getTime() - new Date(a?.updatedAt).getTime() );
default:
return modules;
}
};

return (
<Form
className='d-flex justify-content-center align-items-center mr-3'
style={{ position: 'relative' }}
ref={(ref: any) => focusRef.current = ref}
onBlur={onBlur}
tabIndex={-1}
>
<FontAwesomeIcon icon={faFilter} size='lg' onClick={onFilterClick} className={styles['icon']} />
{filterOn &&
<Form.Group className={styles['filter-options']} >
<Form.Label column={true} className='p-0'>Sort by</Form.Label>
<Container className='pr-0 pl-4'>
{sortOptions.map((option, idx) => (
option.value &&
<Form.Check
className={styles['sort-option']}
key={idx}
id={`radio-${idx}`}
type='checkbox'
label={option.name}
name='radio'
value={option.value}
checked={sortOption === option.value}
onChange={onSortChange}
/>
))}
</Container>
</Form.Group>
}
</Form>
);
};

const mapUserStateToProps = (state: WebState) => ({authenticated: isAuthenticated(state), creator: isCreator(state), admin: isAdmin(state)});
export default connect(mapUserStateToProps)(ModulesFilter);
77 changes: 77 additions & 0 deletions src/components/ModulesSearch/ModulesSearch.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
.search-bar {
padding: 4px 12px;
height: auto;
min-width: 18rem;
border: 1px solid #343a40;
border-radius: 22px;
box-shadow: inset 0 -2px 0 #343a40;
}

.difficulty-dropdown {
font-size: 0.930125rem;
border-radius: 0.25rem;
background-color: #FFF;
color: #868E96;
border: 1px solid rgba(0,0,0,0.1);
}

.icon {
display: flex;
justify-content: center;
}

.icon:hover {
cursor: pointer;
color: maroon;
}

.search-options {
position: absolute;
z-index: 1;
top: calc(100% + 6px);
right: 0;
width: 28rem;
padding: 24px;
background-color: #FFF;
border-radius: 0.25rem;
box-shadow: 0 0 8px rgb(175, 175, 175);
}

.search-input {
height: calc(1.5em + 1rem + 6px);
min-width: 14rem;
}

.input-row {
margin-bottom: 0.75rem !important;
}

div .tags-no-border.tags-no-border {
border: none;
padding: 3px;
}

div .tags.tags {
padding: 4px;
min-width: 110px;
}

.selected-tag.selected-tag {
padding: 1px 3px;
font-size: 0.930125rem;
margin: 2px;
}

.tags-search.tags-search {
padding: 3px 3px;
margin: 0;
}

.tags-input.tags-input {
width: auto !important;
display: none; // to disable adding tags to search term
}

.tags-suggestions.tags-suggestions {
width: 10rem;
}
Loading