$urls[] = $url;
}
+ $url = new SitemapUrl();
+ $url->path = '/tech';
+ $url->lastmod = Technique::latest()->first()->created_at;
+ $url->changefreq = 'monthly';
+ $url->priority = 0.5;
+ $urls[] = $url;
+
foreach (Technique::where('index', true)->get() as $tech) {
$url = new SitemapUrl();
$url->path = '/tech/'.rawurlencode($tech->name);
$url->lastmod = $tech->updated_at ? $tech->updated_at : ($tech->created_at ? $tech->created_at : now());
- $url->changefreq = 'monthly';
+ $url->changefreq = 'never';
$url->priority = $tech->priority;
$urls[] = $url;
}
namespace App\Http\Controllers;
use App\Models\Technique;
+use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
class TechniqueController extends Controller
{
+ public function search(Request $request) {
+ $validatedData = $request->validate([
+ 'phrase' => 'string|nullable',
+ ]);
+
+ $techs = Technique::where('index', '=', 1);
+
+ if (!empty($validatedData['phrase'])) {
+ $search = $validatedData['phrase'];
+ $techs = $techs->where(function (Builder $query) use ($search) {
+ $query->where('title', 'LIKE', '%'.$search.'%')
+ ->orWhere('short', 'LIKE', '%'.$search.'%');
+ });
+ }
+
+ return $techs->get()->toJson();
+ }
+
public function single(Request $request, Technique $tech) {
$this->authorize('view', $tech);
$tech->load('chapters');
import AlttpSeed from './pages/AlttpSeed';
import Front from './pages/Front';
import Technique from './pages/Technique';
+import Techniques from './pages/Techniques';
import Tournament from './pages/Tournament';
import User from './pages/User';
import AlttpBaseRomProvider from '../helpers/AlttpBaseRomContext';
<Header doLogout={doLogout} />
<Routes>
<Route path="h/:hash" element={<AlttpSeed />} />
+ <Route path="tech" element={<Techniques />} />
<Route path="tech/:name" element={<Technique />} />
<Route path="tournaments/:id" element={<Tournament />} />
<Route path="users/:id" element={<User />} />
--- /dev/null
+import axios from 'axios';
+import React from 'react';
+import { withTranslation } from 'react-i18next';
+
+import NotFound from './NotFound';
+import ErrorBoundary from '../common/ErrorBoundary';
+import ErrorMessage from '../common/ErrorMessage';
+import Loading from '../common/Loading';
+import Overview from '../techniques/Overview';
+import { compareTranslation } from '../../helpers/Technique';
+import i18n from '../../i18n';
+
+const Techniques = () => {
+ const [error, setError] = React.useState(null);
+ const [loading, setLoading] = React.useState(true);
+ const [techniques, setTechniques] = React.useState([]);
+
+ React.useEffect(() => {
+ const ctrl = new AbortController();
+ setLoading(true);
+ window.document.title = i18n.t('techniques.heading');
+ axios
+ .get(`/api/tech`, { signal: ctrl.signal })
+ .then(response => {
+ setError(null);
+ setLoading(false);
+ setTechniques(response.data.sort(compareTranslation('title', i18n.language)));
+ })
+ .catch(error => {
+ setError(error);
+ setLoading(false);
+ setTechniques([]);
+ });
+ return () => {
+ ctrl.abort();
+ };
+ }, []);
+
+ React.useEffect(() => {
+ window.document.title = i18n.t('techniques.heading');
+ setTechniques(t => [...t].sort(compareTranslation('title', i18n.language)));
+ }, [i18n.language]);
+
+ if (loading) {
+ return <Loading />;
+ }
+
+ if (error) {
+ return <ErrorMessage error={error} />;
+ }
+
+ if (!techniques || !techniques.length) {
+ return <NotFound />;
+ }
+
+ return <ErrorBoundary>
+ <Overview techniques={techniques} />
+ </ErrorBoundary>;
+};
+
+export default withTranslation()(Techniques);
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+import { getTranslation } from '../../helpers/Technique';
+import i18n from '../../i18n';
+
+const List = ({ techniques }) => <ul className="tech-list">
+ {techniques.map(tech =>
+ <li key={tech.id}>
+ <h2>
+ <Link to={`/tech/${tech.name}`}>
+ {getTranslation(tech, 'title', i18n.language)}
+ </Link>
+ </h2>
+ <p>{getTranslation(tech, 'short', i18n.language)}</p>
+ </li>
+ )}
+</ul>;
+
+List.propTypes = {
+ techniques: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.number,
+ name: PropTypes.string,
+ })),
+};
+
+export default List;
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Container } from 'react-bootstrap';
+import { withTranslation } from 'react-i18next';
+
+import List from './List';
+import i18n from '../../i18n';
+
+const Overview = ({ techniques }) => <Container>
+ <h1>{i18n.t('techniques.heading')}</h1>
+ <List techniques={techniques} />
+</Container>;
+
+Overview.propTypes = {
+ techniques: PropTypes.arrayOf(PropTypes.shape({
+ })),
+};
+
+export default withTranslation()(Overview);
return tech[prop];
};
+export const compareTranslation = (prop, lang) => (a, b) =>
+ getTranslation(a, prop, lang).localeCompare(getTranslation(b, prop, lang));
+
export default {
getTranslation,
};
unlockError: 'Fehler beim Entsperren',
unlockSuccess: 'Runde entsperrt',
},
+ techniques: {
+ heading: 'Techniken',
+ },
tournaments: {
admins: 'Organisation',
applicationDenied: 'Antrag wurde abgelehnt',
unlockError: 'Error unlocking round',
unlockSuccess: 'Round unlocked',
},
+ techniques: {
+ heading: 'Techniques',
+ },
tournaments: {
admins: 'Admins',
applicationDenied: 'Application denied',
+.tech-list {
+ margin: 1em 0;
+ padding: 0;
+ list-style: none;
+
+ li {
+ margin: 1ex 0;
+ padding: 1ex;
+ border-top: thin solid silver;
+ }
+
+ h2 > a {
+ text-decoration: none;
+ }
+}
+
.tech-outline {
float: right;
}
Route::post('rounds/{round}/setSeed', 'App\Http\Controllers\RoundController@setSeed');
Route::post('rounds/{round}/unlock', 'App\Http\Controllers\RoundController@unlock');
+Route::get('tech', 'App\Http\Controllers\TechniqueController@search');
Route::get('tech/{tech:name}', 'App\Http\Controllers\TechniqueController@single');
Route::get('tournaments/{id}', 'App\Http\Controllers\TournamentController@single');