public function single(Request $request, Technique $tech) {
$this->authorize('view', $tech);
+ $tech->load('chapters');
return $tech->toJson();
}
{
use HasFactory;
+ public function chapters() {
+ return $this
+ ->belongsToMany(Technique::class, 'technique_chapter', 'parent_id', 'child_id')
+ ->withPivot('level', 'order')
+ ->orderByPivot('order')
+ ->using(TechniqueChapter::class);
+ }
+
protected $casts = [
'index' => 'boolean',
];
--- /dev/null
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Relations\Pivot;
+
+class TechniqueChapter extends Pivot
+{
+ //
+}
--- /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::table('techniques', function (Blueprint $table) {
+ $table->string('name')->nullable()->default(null)->change();
+ });
+ Schema::create('technique_chapter', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('parent_id')->references('id')->on('techniques')->constrained();
+ $table->foreignId('child_id')->references('id')->on('techniques')->constrained();
+ $table->integer('level')->default(2);
+ $table->integer('order')->default(0);
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('technique_chapter');
+ }
+};
import React from 'react';
import { Container } from 'react-bootstrap';
-const Detail = ({ technique }) => <Container>
+import Outline from './Outline';
+
+const Detail = ({ technique }) => <Container as="article">
<h1>{technique.title}</h1>
+ <Outline technique={technique} />
<div dangerouslySetInnerHTML={{ __html: technique.description }} />
+ {technique.chapters ? technique.chapters.map(chapter =>
+ <section id={`c${chapter.id}`} key={`c${chapter.id}`}>
+ {chapter.pivot.level ?
+ React.createElement(`h${chapter.pivot.level}`, {}, chapter.title)
+ : null}
+ <div dangerouslySetInnerHTML={{ __html: chapter.description }} />
+ </section>
+ ) : null}
</Container>;
Detail.propTypes = {
technique: PropTypes.shape({
+ chapters: PropTypes.arrayOf(PropTypes.shape({
+ })),
description: PropTypes.string,
title: PropTypes.string,
}),
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+import { ListGroup } from 'react-bootstrap';
+
+const Outline = ({ technique }) => technique.chapters && technique.chapters.length ?
+ <aside className="tech-outline mb-3 ms-3">
+ <ListGroup>
+ {technique.chapters.map(chapter => chapter.pivot.level ?
+ <ListGroup.Item
+ action
+ href={`#c${chapter.id}`}
+ key={`c${chapter.id}`}
+ title={chapter.short || null}
+ >
+ {chapter.title}
+ </ListGroup.Item>
+ : null)}
+ </ListGroup>
+ </aside>
+: null;
+
+Outline.propTypes = {
+ technique: PropTypes.shape({
+ chapters: PropTypes.arrayOf(PropTypes.shape({
+ })),
+ }),
+};
+
+export default Outline;
@import 'participants';
@import 'results';
@import 'rounds';
+@import 'techniques';
@import 'tournaments';
@import 'users';
--- /dev/null
+.tech-outline {
+ float: right;
+}