--- /dev/null
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Technique;
+use Illuminate\Http\Request;
+
+class TechniqueController extends Controller
+{
+
+ public function single(Request $request, Technique $tech) {
+ $this->authorize('view', $tech);
+ return $tech->toJson();
+ }
+
+}
--- /dev/null
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Technique extends Model
+{
+ use HasFactory;
+}
--- /dev/null
+<?php
+
+namespace App\Policies;
+
+use App\Models\Technique;
+use App\Models\User;
+use Illuminate\Auth\Access\HandlesAuthorization;
+
+class TechniquePolicy
+{
+ use HandlesAuthorization;
+
+ /**
+ * Determine whether the user can view any models.
+ *
+ * @param \App\Models\User $user
+ * @return \Illuminate\Auth\Access\Response|bool
+ */
+ public function viewAny(?User $user = null)
+ {
+ return true;
+ }
+
+ /**
+ * Determine whether the user can view the model.
+ *
+ * @param \App\Models\User $user
+ * @param \App\Models\Technique $technique
+ * @return \Illuminate\Auth\Access\Response|bool
+ */
+ public function view(?User $user = null, Technique $technique)
+ {
+ return true;
+ }
+
+ /**
+ * Determine whether the user can create models.
+ *
+ * @param \App\Models\User $user
+ * @return \Illuminate\Auth\Access\Response|bool
+ */
+ public function create(User $user)
+ {
+ return $user->isAdmin();
+ }
+
+ /**
+ * Determine whether the user can update the model.
+ *
+ * @param \App\Models\User $user
+ * @param \App\Models\Technique $technique
+ * @return \Illuminate\Auth\Access\Response|bool
+ */
+ public function update(User $user, Technique $technique)
+ {
+ return $user->isAdmin();
+ }
+
+ /**
+ * Determine whether the user can delete the model.
+ *
+ * @param \App\Models\User $user
+ * @param \App\Models\Technique $technique
+ * @return \Illuminate\Auth\Access\Response|bool
+ */
+ public function delete(User $user, Technique $technique)
+ {
+ return $user->isAdmin();
+ }
+
+ /**
+ * Determine whether the user can restore the model.
+ *
+ * @param \App\Models\User $user
+ * @param \App\Models\Technique $technique
+ * @return \Illuminate\Auth\Access\Response|bool
+ */
+ public function restore(User $user, Technique $technique)
+ {
+ return $user->isAdmin();
+ }
+
+ /**
+ * Determine whether the user can permanently delete the model.
+ *
+ * @param \App\Models\User $user
+ * @param \App\Models\Technique $technique
+ * @return \Illuminate\Auth\Access\Response|bool
+ */
+ public function forceDelete(User $user, Technique $technique)
+ {
+ return $user->isAdmin();
+ }
+}
--- /dev/null
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create('techniques', function (Blueprint $table) {
+ $table->id();
+ $table->string('name')->unique();
+ $table->text('title');
+ $table->text('short');
+ $table->text('description');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('techniques');
+ }
+};
import Header from './common/Header';
import AlttpSeed from './pages/AlttpSeed';
+import Technique from './pages/Technique';
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/:name" element={<Technique />} />
<Route path="tournaments/:id" element={<Tournament />} />
<Route path="users/:id" element={<User />} />
<Route path="*" element={<Navigate to="/tournaments/4" />} />
--- /dev/null
+import axios from 'axios';
+import React, { useEffect, useState } from 'react';
+import { useParams } from 'react-router-dom';
+
+import ErrorBoundary from '../common/ErrorBoundary';
+import ErrorMessage from '../common/ErrorMessage';
+import Loading from '../common/Loading';
+import NotFound from '../pages/NotFound';
+import Detail from '../techniques/Detail';
+
+const Technique = () => {
+ const params = useParams();
+ const { name } = params;
+
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [technique, setTechnique] = useState(null);
+
+ useEffect(() => {
+ const ctrl = new AbortController();
+ setLoading(true);
+ axios
+ .get(`/api/tech/${name}`, { signal: ctrl.signal })
+ .then(response => {
+ setError(null);
+ setLoading(false);
+ setTechnique(response.data);
+ window.document.title = response.data.title;
+ })
+ .catch(error => {
+ setError(error);
+ setLoading(false);
+ setTechnique(null);
+ });
+ return () => {
+ ctrl.abort();
+ };
+ }, [name]);
+
+ if (loading) {
+ return <Loading />;
+ }
+
+ if (error) {
+ return <ErrorMessage error={error} />;
+ }
+
+ if (!technique) {
+ return <NotFound />;
+ }
+
+ return <ErrorBoundary>
+ <Detail technique={technique} />
+ </ErrorBoundary>;
+};
+
+export default Technique;
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Container } from 'react-bootstrap';
+
+const Detail = ({ technique }) => <Container>
+ <h1>{technique.title}</h1>
+ <div dangerouslySetInnerHTML={{ __html: technique.description }} />
+</Container>;
+
+Detail.propTypes = {
+ technique: PropTypes.shape({
+ description: PropTypes.string,
+ title: PropTypes.string,
+ }),
+};
+
+export default Detail;
Route::post('rounds/{round}/setSeed', 'App\Http\Controllers\RoundController@setSeed');
Route::post('rounds/{round}/unlock', 'App\Http\Controllers\RoundController@unlock');
+Route::get('tech/{tech:name}', 'App\Http\Controllers\TechniqueController@single');
+
Route::get('tournaments/{id}', 'App\Http\Controllers\TournamentController@single');
Route::post('tournaments/{tournament}/apply', 'App\Http\Controllers\TournamentController@apply');
Route::post('tournaments/{tournament}/close', 'App\Http\Controllers\TournamentController@close');