From f1c8c3cc53a09d1c261875d2f482b6e19bc48a9a Mon Sep 17 00:00:00 2001
From: Daniel Karbach <daniel.karbach@localhorst.tv>
Date: Mon, 6 Feb 2023 15:49:52 +0100
Subject: [PATCH] tech attribution & requirements

---
 app/Models/Technique.php                      |  1 +
 ...02_06_124402_more_technique_properties.php | 40 +++++++++++++++++++
 resources/js/components/common/Icon.js        |  1 +
 resources/js/components/common/ZeldaIcon.js   | 34 +++++++++++-----
 resources/js/components/techniques/Detail.js  |  9 ++++-
 .../js/components/techniques/Requirement.js   | 17 ++++++++
 .../js/components/techniques/Requirements.js  | 34 ++++++++++++++++
 resources/js/i18n/de.js                       |  4 ++
 resources/js/i18n/en.js                       |  4 ++
 resources/sass/common.scss                    | 24 +++++++++++
 resources/sass/map.scss                       |  2 +-
 resources/sass/techniques.scss                | 27 +++++++++++++
 12 files changed, 184 insertions(+), 13 deletions(-)
 create mode 100644 database/migrations/2023_02_06_124402_more_technique_properties.php
 create mode 100644 resources/js/components/techniques/Requirement.js
 create mode 100644 resources/js/components/techniques/Requirements.js

diff --git a/app/Models/Technique.php b/app/Models/Technique.php
index c8ad91b..4469671 100644
--- a/app/Models/Technique.php
+++ b/app/Models/Technique.php
@@ -30,6 +30,7 @@ class Technique extends Model
 
 	protected $casts = [
 		'index' => 'boolean',
+		'requirements' => 'array',
 		'rulesets' => 'array',
 	];
 
diff --git a/database/migrations/2023_02_06_124402_more_technique_properties.php b/database/migrations/2023_02_06_124402_more_technique_properties.php
new file mode 100644
index 0000000..803fcc5
--- /dev/null
+++ b/database/migrations/2023_02_06_124402_more_technique_properties.php
@@ -0,0 +1,40 @@
+<?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->text('requirements')->nullable()->default(null);
+			$table->text('attribution')->default('');
+		});
+		Schema::table('technique_translations', function(Blueprint $table) {
+			$table->text('attribution')->default('');
+		});
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+		Schema::table('techniques', function(Blueprint $table) {
+			$table->dropColumn('requirements');
+			$table->dropColumn('attribution');
+		});
+		Schema::table('technique_translations', function(Blueprint $table) {
+			$table->dropColumn('attribution');
+		});
+    }
+};
diff --git a/resources/js/components/common/Icon.js b/resources/js/components/common/Icon.js
index 3e094cc..dd88669 100644
--- a/resources/js/components/common/Icon.js
+++ b/resources/js/components/common/Icon.js
@@ -81,6 +81,7 @@ Icon.REMOVE = makePreset('RemoveIcon', 'square-xmark');
 Icon.RESULT = makePreset('ResultIcon', 'clock');
 Icon.SECOND_PLACE = makePreset('SecondPlaceIcon', 'medal');
 Icon.SETTINGS = makePreset('SettingsIcon', 'cog');
+Icon.SLASH = makePreset('SlashIcon', 'slash');
 Icon.STREAM = makePreset('StreamIcon', ['fab', 'twitch']);
 Icon.THIRD_PLACE = makePreset('ThirdPlaceIcon', 'award');
 Icon.TWITCH = makePreset('TwitchIcon', ['fab', 'twitch']);
diff --git a/resources/js/components/common/ZeldaIcon.js b/resources/js/components/common/ZeldaIcon.js
index 84f2220..c53f4fd 100644
--- a/resources/js/components/common/ZeldaIcon.js
+++ b/resources/js/components/common/ZeldaIcon.js
@@ -1,8 +1,8 @@
 import PropTypes from 'prop-types';
 import React from 'react';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
-import i18n from '../../i18n';
+import Icon from './Icon';
 
 const getIconURL = name => {
 	switch (name) {
@@ -64,17 +64,29 @@ const getIconURL = name => {
 	}
 };
 
-const ZeldaIcon = ({ name }) =>
-<span className="zelda-icon">
-	<img
-		alt={i18n.t(`icon.zelda.${name}`)}
-		src={getIconURL(name)}
-		title={i18n.t(`icon.zelda.${name}`)}
-	/>
-</span>;
+const ZeldaIcon = ({ name }) => {
+	const { t } = useTranslation();
+
+	const invert = name.startsWith('not-');
+	const strippedName = invert ? name.substr(4) : name;
+	const title = t(`icon.zelda.${name}`);
+
+	return <span className="zelda-icon">
+		<img
+			alt={title}
+			src={getIconURL(strippedName)}
+			title={title}
+		/>
+		{invert ?
+			<span className="strike">
+				<Icon.SLASH title="" />
+			</span>
+		: null}
+	</span>;
+};
 
 ZeldaIcon.propTypes = {
 	name: PropTypes.string,
 };
 
-export default withTranslation()(ZeldaIcon);
+export default ZeldaIcon;
diff --git a/resources/js/components/techniques/Detail.js b/resources/js/components/techniques/Detail.js
index 6653923..9d9534e 100644
--- a/resources/js/components/techniques/Detail.js
+++ b/resources/js/components/techniques/Detail.js
@@ -1,10 +1,11 @@
 import PropTypes from 'prop-types';
 import React from 'react';
-import { Container } from 'react-bootstrap';
+import { Alert, Container } from 'react-bootstrap';
 import { withTranslation } from 'react-i18next';
 
 import List from './List';
 import Outline from './Outline';
+import Requirements from './Requirements';
 import Rulesets from './Rulesets';
 import RawHTML from '../common/RawHTML';
 import {
@@ -23,6 +24,7 @@ const Detail = ({ technique }) => <Container as="article">
 		: null}
 	</div>
 	<Outline technique={technique} />
+	<Requirements technique={technique} />
 	<RawHTML html={getTranslation(technique, 'description', i18n.language)} />
 	{technique.chapters ? technique.chapters.map(chapter =>
 		<section id={`c${chapter.id}`} key={`c${chapter.id}`}>
@@ -40,6 +42,11 @@ const Detail = ({ technique }) => <Container as="article">
 		<h2 className="mt-5">{i18n.t('techniques.seeAlso')}</h2>
 		<List techniques={sorted(getRelations(technique, 'related'))} />
 	</> : null}
+	{getTranslation(technique, 'attribution', i18n.language) ?
+		<Alert variant="dark">
+			<RawHTML html={getTranslation(technique, 'attribution', i18n.language)} />
+		</Alert>
+	: null}
 </Container>;
 
 Detail.propTypes = {
diff --git a/resources/js/components/techniques/Requirement.js b/resources/js/components/techniques/Requirement.js
new file mode 100644
index 0000000..5faa372
--- /dev/null
+++ b/resources/js/components/techniques/Requirement.js
@@ -0,0 +1,17 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import ZeldaIcon from '../common/ZeldaIcon';
+
+const Requirement = ({ requirement }) =>
+	<div className="requirement">
+		{requirement.map(r =>
+			<ZeldaIcon key={r} name={r} />
+		)}
+	</div>;
+
+Requirement.propTypes = {
+	requirement: PropTypes.arrayOf(PropTypes.string),
+};
+
+export default Requirement;
diff --git a/resources/js/components/techniques/Requirements.js b/resources/js/components/techniques/Requirements.js
new file mode 100644
index 0000000..0d4501e
--- /dev/null
+++ b/resources/js/components/techniques/Requirements.js
@@ -0,0 +1,34 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+import Requirement from './Requirement';
+
+const Requirements = ({ technique }) => {
+	const { t } = useTranslation();
+
+	if (!technique.requirements || !technique.requirements.length) {
+		return null;
+	}
+
+	return <p className="tech-requirements">
+		{t('techniques.requirements')}
+		<ul>
+			{technique.requirements.map((r, i) =>
+				<li key={i}>
+					<Requirement requirement={r} />
+				</li>
+			)}
+		</ul>
+	</p>;
+};
+
+Requirements.propTypes = {
+	technique: PropTypes.shape({
+		requirements: PropTypes.arrayOf(
+			PropTypes.arrayOf(PropTypes.string),
+		),
+	}),
+};
+
+export default Requirements;
diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js
index 02adc9d..f130c08 100644
--- a/resources/js/i18n/de.js
+++ b/resources/js/i18n/de.js
@@ -363,6 +363,8 @@ export default {
 				mitts: 'Titan \'s Mitts',
 				moonpearl: 'Moonpearl',
 				mushroom: 'Mushroom',
+				'not-flippers': 'Keine Flippers',
+				'not-moonpearl': 'Keine Moonpearl',
 				powder: 'Powder',
 				quake: 'Quake',
 				'red-bomb': 'Red Bomb',
@@ -508,6 +510,8 @@ export default {
 		},
 		techniques: {
 			heading: 'Techniken',
+			lastModified: 'Zuletzt geändert: {{ date, L }}',
+			requirements: 'Erfordert: ',
 			rulesetCodes: {
 				competitive: 'COM',
 				mg: 'MG',
diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js
index efb99b3..a7ca19c 100644
--- a/resources/js/i18n/en.js
+++ b/resources/js/i18n/en.js
@@ -363,6 +363,8 @@ export default {
 				mitts: 'Titan \'s Mitts',
 				moonpearl: 'Moonpearl',
 				mushroom: 'Mushroom',
+				'not-flippers': 'No Flippers',
+				'not-moonpearl': 'No Moonpearl',
 				powder: 'Powder',
 				quake: 'Quake',
 				'red-bomb': 'Red Bomb',
@@ -508,6 +510,8 @@ export default {
 		},
 		techniques: {
 			heading: 'Techniques',
+			lastModified: 'Last modified: {{ date, L }}',
+			requirements: 'Requires: ',
 			rulesetCodes: {
 				competitive: 'COM',
 				mg: 'MG',
diff --git a/resources/sass/common.scss b/resources/sass/common.scss
index 83e5b90..879253c 100644
--- a/resources/sass/common.scss
+++ b/resources/sass/common.scss
@@ -193,6 +193,7 @@ h1 {
 }
 
 .zelda-icon {
+	position: relative;
 	display: inline-flex;
 	align-items: center;
 	width: 2em;
@@ -203,4 +204,27 @@ h1 {
 		max-width: 100%;
 		max-height: 100%;
 	}
+	.strike {
+		position: absolute;
+		top: 0;
+		left: 0;
+		width: 2em;
+		height: 2em;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		pointer-events: none;
+
+		svg {
+			width: 100%;
+			height: 100%;
+		}
+
+		path {
+			fill: red;
+			stroke: black;
+			stroke-width: 1px;
+			vector-effect: non-scaling-stroke;
+		}
+	}
 }
diff --git a/resources/sass/map.scss b/resources/sass/map.scss
index 599cd15..38376a1 100644
--- a/resources/sass/map.scss
+++ b/resources/sass/map.scss
@@ -2,7 +2,7 @@
 	path {
 		fill: red;
 		stroke: black;
-		stroke-width: 2px;
+		stroke-width: 1px;
 		vector-effect: non-scaling-stroke;
 	}
 }
diff --git a/resources/sass/techniques.scss b/resources/sass/techniques.scss
index 8355e70..eff0c62 100644
--- a/resources/sass/techniques.scss
+++ b/resources/sass/techniques.scss
@@ -34,6 +34,33 @@
 	}
 }
 
+.tech-requirements {
+	ul {
+		margin: 0;
+		padding: 0;
+		list-style: none;
+	}
+	li {
+		display: inline;
+		list-style: none;
+		margin: 0 1ex;
+		padding: 0;
+
+		&::before {
+			display: inline;
+			content: " / ";
+			margin-right: 1ex;
+		}
+		&:first-child::before {
+			display: none;
+		}
+	}
+	.requirement {
+		display: inline-flex;
+		vertical-align: middle;
+	}
+}
+
 .tech-outline {
 	float: right;
 }
-- 
2.39.5