public function search(Request $request) {
$validatedData = $request->validate([
'phrase' => 'string|nullable',
+ 'ruleset' => 'array|nullable',
+ 'ruleset.competitive' => 'boolean',
+ 'ruleset.mg' => 'boolean',
+ 'ruleset.nl' => 'boolean',
+ 'ruleset.owg' => 'boolean',
'type' => 'string|nullable',
]);
});
}
+ if (isset($validatedData['ruleset'])) {
+ $com = isset($validatedData['ruleset']['competitive']) && $validatedData['ruleset']['competitive'];
+ $owg = isset($validatedData['ruleset']['owg']) && $validatedData['ruleset']['owg'];
+ $mg = isset($validatedData['ruleset']['mg']) && $validatedData['ruleset']['mg'];
+ $nl = isset($validatedData['ruleset']['nl']) && $validatedData['ruleset']['nl'];
+ $any = $com || $owg || $mg || $nl;
+ $all = $com && $owg && $mg && $nl;
+ if ($any && !$all) {
+ $techs->where(function(Builder $query) use ($com, $owg, $mg, $nl) {
+ $query->whereNull('rulesets');
+ if ($com) {
+ $query->orWhere('rulesets->competitive', '=', true);
+ }
+ if ($owg) {
+ $query->orWhere('rulesets->owg', '=', true);
+ }
+ if ($mg) {
+ $query->orWhere('rulesets->mg', '=', true);
+ }
+ if ($nl) {
+ $query->orWhere('rulesets->nl', '=', true);
+ }
+ });
+ }
+ }
+
return $techs->get()->toJson();
}
const Techniques = ({ namespace, type }) => {
const [error, setError] = React.useState(null);
+ const [filter, setFilter] = React.useState({});
const [loading, setLoading] = React.useState(true);
const [techniques, setTechniques] = React.useState([]);
+ React.useEffect(() => {
+ const savedFilter = localStorage.getItem(`content.filter.${type}`);
+ if (savedFilter) {
+ setFilter(JSON.parse(savedFilter));
+ } else {
+ setFilter(filter => filter ? {} : filter);
+ }
+ }, [type]);
+
+ const updateFilter = React.useCallback(newFilter => {
+ localStorage.setItem(`content.filter.${type}`, JSON.stringify(newFilter));
+ setFilter(newFilter);
+ }, [type]);
+
React.useEffect(() => {
const ctrl = new AbortController();
- setLoading(true);
+ if (!techniques.length) {
+ setLoading(true);
+ }
window.document.title = i18n.t(`${namespace}.heading`);
axios
.get(`/api/content`, {
params: {
type,
+ ...filter,
},
signal: ctrl.signal
})
setTechniques(response.data.sort(compareTranslation('title', i18n.language)));
})
.catch(error => {
- setError(error);
- setLoading(false);
- setTechniques([]);
+ if (!axios.isCancel(error)) {
+ setError(error);
+ setLoading(false);
+ setTechniques([]);
+ }
});
return () => {
ctrl.abort();
};
- }, [namespace, type]);
+ }, [filter, namespace, type]);
React.useEffect(() => {
window.document.title = i18n.t(`${namespace}.heading`);
}
return <ErrorBoundary>
- <Overview namespace={namespace} techniques={techniques} />
+ <Overview
+ filter={filter}
+ namespace={namespace}
+ setFilter={updateFilter}
+ techniques={techniques}
+ type={type}
+ />
</ErrorBoundary>;
};
import { withTranslation } from 'react-i18next';
import List from './List';
+import TechFilter from './TechFilter';
import i18n from '../../i18n';
const Overview = ({
+ filter,
namespace,
+ setFilter,
techniques,
+ type,
}) => <Container>
- <h1>{i18n.t(`${namespace}.heading`)}</h1>
+ <div className="d-flex align-items-center justify-content-between">
+ <h1>{i18n.t(`${namespace}.heading`)}</h1>
+ {type === 'tech' ?
+ <TechFilter filter={filter} setFilter={setFilter} />
+ : null}
+ </div>
<List techniques={techniques} />
</Container>;
Overview.propTypes = {
+ filter: PropTypes.shape({}),
namespace: PropTypes.string,
+ setFilter: PropTypes.func,
techniques: PropTypes.arrayOf(PropTypes.shape({
})),
+ type: PropTypes.string,
};
export default withTranslation()(Overview);
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Form } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+const TechFilter = ({ filter, setFilter }) => {
+ const { t } = useTranslation();
+
+ const handleChange = React.useCallback(e => {
+ if (e.target.name.startsWith('ruleset.')) {
+ const r = e.target.name.substring(8);
+ setFilter({
+ ...filter,
+ ruleset: {
+ ...filter.ruleset || {},
+ [r]: e.target.checked ? '1' : '0',
+ },
+ });
+ }
+ }, [filter]);
+
+ return <div className="tech-filter">
+ <div>{t('techniques.rulesetFilterHeading')}</div>
+ <div className="ruleset-box">
+ {['competitive', 'owg', 'mg', 'nl'].map(r =>
+ <Form.Check
+ checked={!!(filter && filter.ruleset && filter.ruleset[r] === '1')}
+ key={r}
+ id={`tech.filter.ruleset.${r}`}
+ name={`ruleset.${r}`}
+ label={t(`techniques.rulesetCodes.${r}`)}
+ onChange={handleChange}
+ title={t(`techniques.rulesetDescriptions.${r}`)}
+ type="checkbox"
+ />
+ )}
+ </div>
+ </div>;
+};
+
+TechFilter.propTypes = {
+ filter: PropTypes.shape({
+ ruleset: PropTypes.shape({
+ }),
+ }),
+ setFilter: PropTypes.func,
+};
+
+export default TechFilter;
nl: 'No Logic',
owg: 'Overworld Glitches',
},
+ rulesetFilterHeading: 'Zeige nur Techniken, die in folgenden Regelsätzen erlaubt sind:',
seeAlso: 'Siehe auch',
},
tournaments: {
nl: 'No Logic',
owg: 'Overworld Glitches',
},
+ rulesetFilterHeading: 'Only show techniques allowed in the following rulesets:',
seeAlso: 'See also',
},
tournaments: {
gap: 0 1ex;
padding: 0.5ex;
border-radius: 0.5ex;
+
+ .form-check label {
+ margin-top: 0;
+ }
+}
+
+.tech-filter {
+ max-width: 18em;
+ text-align: right;
+ .form-check {
+ text-align: left;
+ }
}
.tech-list {