diff --git a/templates/components/bbs/script.js b/templates/components/bbs/script.js
new file mode 100644
index 0000000..9a639c0
--- /dev/null
+++ b/templates/components/bbs/script.js
@@ -0,0 +1,126 @@
+let charFilter = "";
+let typeFilter = "";
+let searchType = "commands";
+
+document.addEventListener("DOMContentLoaded", (event) => {
+ const searchFilter = document.getElementById("filter");
+ let filterHandler = debounce(() => filter());
+ searchFilter.addEventListener("keyup", filterHandler);
+ searchFilter.placeholder = "Search commands...";
+
+ const searchInputs = document.querySelectorAll(
+ 'input[type="radio"][name="search"]',
+ );
+
+ searchInputs.forEach(function (item, index) {
+ item.addEventListener("input", function () {
+ searchType = this.checked ? this.value : "";
+ searchFilter.placeholder = "Search " + this.value + "...";
+ filter();
+ });
+ });
+
+ const charFilters = document.querySelectorAll(
+ 'input[type="radio"][name="charFilter"]',
+ );
+
+ charFilters.forEach(function (item, index) {
+ item.addEventListener("input", function () {
+ charFilter = this.checked ? this.value : "";
+ filter();
+ });
+ });
+
+ const typeFilters = document.querySelectorAll(
+ 'input[type="radio"][name="typeFilter"]',
+ );
+
+ typeFilters.forEach(function (item, index) {
+ item.addEventListener("input", function () {
+ typeFilter = this.checked ? this.value : "";
+ filter();
+ });
+ });
+});
+
+function debounce(callback, wait = 300) {
+ let timeoutId = null;
+ return (...args) => {
+ window.clearTimeout(timeoutId);
+ timeoutId = window.setTimeout(() => {
+ callback(...args);
+ }, wait);
+ };
+}
+
+function filter() {
+ const table = document.querySelector("table tbody");
+ const search = document.getElementById("filter").value.toLowerCase();
+
+ for (const child of table.children) {
+ const tds = child.children;
+ resetStyle(child, tds);
+
+ if (search.length > 0) {
+ if (searchType === "commands") {
+ // Check for command name
+ if (!tds[1].innerText.toLowerCase().includes(search)) {
+ child.style.display = "none";
+ } else {
+ applyStyle(tds[1]);
+ }
+ } else if (searchType === "ingredients") {
+ // Ingredients
+ if (!tds[2].innerText.toLowerCase().includes(search)) {
+ if (!tds[3].innerText.toLowerCase().includes(search)) {
+ child.style.display = "none";
+ } else {
+ applyStyle(tds[3]);
+ }
+ } else {
+ applyStyle(tds[2]);
+ if (tds[3].innerText.toLowerCase().includes(search)) {
+ applyStyle(tds[3]);
+ }
+ }
+ } else if (searchType === "abilities") {
+ // Abilities
+ hasLine = false;
+ for (let i = 0; i < 7; i++) {
+ const id = i + 6;
+ const ablName = tds[id].innerText.toLowerCase();
+ if (ablName.includes(search)) {
+ applyStyle(tds[id]);
+ hasLine = true;
+ break;
+ }
+ }
+
+ if (!hasLine) {
+ child.style.display = "none";
+ }
+ }
+ }
+
+ const typeCheck = typeFilter === "" || tds[1].className === typeFilter;
+ const charCheck =
+ charFilter === "" || child.className.includes(charFilter);
+
+ if (child.style.display == "" && (!typeCheck || !charCheck)) {
+ child.style.display = "none";
+ }
+ }
+}
+
+function resetStyle(child, tds) {
+ child.style.display = "";
+ for (const td of tds) {
+ td.style.fontWeight = "inherit";
+ td.style.color = "#fff";
+ }
+}
+
+function applyStyle(el) {
+ el.style.fontWeight = "bold";
+ el.style.color = "red";
+}
diff --git a/templates/components/bbs/style.css b/templates/components/bbs/style.css
new file mode 100644
index 0000000..3ab0956
--- /dev/null
+++ b/templates/components/bbs/style.css
@@ -0,0 +1,30 @@
+table {
+ .charlist {
+ display: inline-grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 15px;
+
+ .aqua {
+ color: #97c8ff;
+ }
+
+ .ventus {
+ color: #26ff62;
+ }
+
+ .terra {
+ color: #ff7400;
+ }
+ }
+
+ tbody tr:hover {
+ background-color: #4f4f4f;
+ }
+
+ & tr,
+ th,
+ td {
+ border: 1px solid #fff;
+ padding: 7px;
+ }
+}
diff --git a/templates/components/ddd/script.js b/templates/components/ddd/script.js
new file mode 100644
index 0000000..e69de29
diff --git a/templates/components/ddd/style.css b/templates/components/ddd/style.css
new file mode 100644
index 0000000..c1f8eb1
--- /dev/null
+++ b/templates/components/ddd/style.css
@@ -0,0 +1,213 @@
+.blue {
+ background-color: #35a0a8;
+}
+
+.purple {
+ background-color: #992a9b;
+}
+
+.green {
+ background-color: #7ecf50;
+}
+
+.yellow {
+ background-color: #d3c949;
+}
+
+.secret1 {
+ background-color: #497331;
+}
+
+.secret2 {
+ background-color: #881a22;
+}
+
+div.abilities {
+ display: flex;
+
+ & div {
+ margin-right: 60px;
+ }
+}
+
+.dispositions {
+ display: flex;
+ flex-wrap: wrap;
+
+ div.route {
+ display: flex;
+ align-items: center;
+ width: 40%;
+
+ & div {
+ display: flex;
+ align-items: center;
+ }
+
+ & .disposition {
+ width: 32px;
+ height: 32px;
+ margin-right: 5px;
+ display: inline-block;
+ vertical-align: middle;
+ }
+
+ & span {
+ font-size: 18px;
+ }
+
+ & ul {
+ list-style: disclosure-closed;
+ line-height: 2.5;
+
+ & li {
+ display: list-item;
+ }
+ }
+ }
+}
+
+table.board {
+ width: inherit;
+
+ td {
+ width: 120px;
+ height: 100px;
+ position: relative;
+
+ & span {
+ position: inherit;
+ z-index: 10;
+ color: black;
+ }
+
+ & .slot {
+ width: 70%;
+ height: 70%;
+ position: relative;
+ left: 15px;
+ background-color: #cbf;
+ z-index: 10;
+ align-content: center;
+ font-size: 14px;
+ box-shadow: 1px 1px 5px black;
+
+ .help:after {
+ content: "?";
+ position: absolute;
+ color: rgba(0, 0, 0, 0.3);
+ font-size: 24px;
+ bottom: 0;
+ right: 5px;
+ }
+ }
+
+ .tooltip-wrapper {
+ .tooltip {
+ visibility: hidden;
+ background-color: #555;
+ max-width: 250px;
+ min-width: 150px;
+ color: #fff;
+ text-align: center;
+ border-radius: 6px;
+ padding: 8px;
+ position: absolute;
+ z-index: 1;
+ bottom: 125%;
+ left: -50%;
+ opacity: 0;
+ transition: opacity 0.3s;
+ }
+
+ &:hover .tooltip {
+ visibility: visible;
+ opacity: 1;
+ }
+ }
+
+ &.slot-w {
+ height: 20px;
+ }
+
+ &.slot-h {
+ width: 20px;
+ }
+
+ &.start .slot {
+ background-color: #dcbf7e;
+ }
+
+ &.north.east.south.west .path::after {
+ content: "┼";
+ }
+
+ &.east.south.west .path::after {
+ content: "┬";
+ }
+
+ &.east.north.west .path::after {
+ content: "┴";
+ }
+
+ &.north.south.west .path::after {
+ content: "┤";
+ }
+
+ &.north.south.east .path::after {
+ content: "├";
+ }
+
+ &.south.east .path::after {
+ content: "┌";
+ }
+
+ &.south.west .path::after {
+ content: "┐";
+ }
+
+ &.north.west .path::after {
+ content: "┘";
+ }
+
+ &.north.east .path::after {
+ content: "└";
+ }
+
+ &.east.west .path::after {
+ content: "─";
+ }
+
+ &.north.south .path::after {
+ content: "│";
+ }
+
+ &.west .path::after {
+ content: "╴";
+ }
+
+ &.east .path::after {
+ content: "╶";
+ }
+
+ &.south .path::after {
+ content: "╷";
+ }
+
+ &.north .path::after {
+ content: "╵";
+ }
+ }
+}
+
+.path {
+ position: absolute;
+ top: -70px;
+ left: 0;
+ font-size: 200px;
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+ /*color: #3c3c3c;*/
+ color: #ccbbff;
+}
diff --git a/templates/components/kh2/script.js b/templates/components/kh2/script.js
new file mode 100644
index 0000000..1c47caa
--- /dev/null
+++ b/templates/components/kh2/script.js
@@ -0,0 +1,61 @@
+let showOnlyTracked = false;
+let kindFilter = new Set();
+
+document.addEventListener("DOMContentLoaded", (event) => {
+ const onlyTrackedFilter = document.querySelector(
+ 'input[name="onlyTracked"]',
+ );
+
+ onlyTrackedFilter.addEventListener("input", function () {
+ showOnlyTracked = this.checked ? true : false;
+ filter();
+ });
+
+ const kindFilters = document.querySelectorAll(
+ 'input[type="checkbox"][name="kindFilter"]',
+ );
+
+ kindFilters.forEach(function (item, index) {
+ item.addEventListener("input", function () {
+ if (this.checked) {
+ kindFilter.add(this.value);
+ } else {
+ kindFilter.delete(this.value);
+ }
+ console.log(kindFilter);
+ filter();
+ });
+ });
+});
+
+function track(element) {
+ let parent = element.parentElement.parentElement;
+ let isTracked = parent.dataset["isTracked"] ?? false;
+ isTracked = isTracked === "true" ? false : true;
+ parent.dataset["isTracked"] = isTracked;
+ element.innerHTML = isTracked ? "Stop tracking" : "Start tracking";
+ element.style["border-bottom-color"] = isTracked ? "#a00" : "#0a0";
+ filter();
+}
+
+function filter() {
+ const categories = document.querySelectorAll(".category-wrapper");
+
+ for (const category of categories) {
+ let isTracked = category.dataset["isTracked"] == "true";
+ let kind = category.dataset["matKind"];
+ let type = category.dataset["matType"];
+
+ category.style.display = "";
+
+ if (showOnlyTracked && !isTracked) {
+ category.style.display = "none";
+ }
+
+ if (kindFilter.size > 0) {
+ if (!kindFilter.has(kind)) {
+ category.style.display = "none";
+ }
+ }
+ }
+}
diff --git a/templates/components/kh2/style.css b/templates/components/kh2/style.css
new file mode 100644
index 0000000..1b9df15
--- /dev/null
+++ b/templates/components/kh2/style.css
@@ -0,0 +1,41 @@
+.category {
+ display: flex;
+ align-items: center;
+}
+
+.enemies {
+ display: flex;
+
+ .drop {
+ display: flex;
+ flex-direction: column;
+ text-align: center;
+ margin-right: 8px;
+ line-height: 30px;
+ font-size: 18px;
+ text-shadow: black 2px 2px;
+ transition: all 0.2s ease;
+
+ div {
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: contain;
+ width: 256px;
+ height: 256px;
+ background-size: 384px;
+ transition: all 0.2s ease;
+ box-shadow: inset rgb(51, 51, 51) 0px 0px 10px 15px;
+ }
+
+ /* &:hover { */
+ /* background-color: #333; */
+ /* box-shadow: 0 0 10px 1px rgba(0, 255, 0, 0.5); */
+ /* transform: scale(1.5); */
+ /**/
+ /* & > div { */
+ /* padding: 32px; */
+ /* margin: -32px; */
+ /* } */
+ /* } */
+ }
+}
diff --git a/templates/components/kh3/script.js b/templates/components/kh3/script.js
new file mode 100644
index 0000000..1e2b7c8
--- /dev/null
+++ b/templates/components/kh3/script.js
@@ -0,0 +1,162 @@
+let globalStats = { str: 0, mag: 0, def: 0, hp: 0, mp: 0 };
+const types = ["starters", "soups", "fish", "meat", "deserts"];
+let typeItem = [];
+let hasSelection = [false, false, false, false, false];
+
+document.addEventListener("DOMContentLoaded", (event) => {
+ updateStats();
+
+ types.forEach(function (type, typeIdx) {
+ const recipes = document.querySelectorAll(
+ "div.recipes." + type + " .recipe",
+ );
+
+ recipes.forEach(function (item, index) {
+ item.addEventListener("click", function () {
+ if (!hasSelection[typeIdx]) {
+ hasSelection[typeIdx] = true;
+ selectRecipe(item, typeIdx);
+ } else {
+ if (typeItem[typeIdx] != item) {
+ unselectRecipe(typeIdx);
+ selectRecipe(item, typeIdx);
+ } else {
+ hasSelection[typeIdx] = false;
+ unselectRecipe(typeIdx);
+ updateStats();
+ }
+ }
+ });
+ });
+ });
+
+ document
+ .getElementById("hearty-meal")
+ .addEventListener("click", function () {
+ console.log(this);
+ if (this.checked) {
+ let bonusStr = Math.ceil(globalStats.str * 0.25);
+ let bonusMag = Math.ceil(globalStats.mag * 0.25);
+ let bonusDef = Math.ceil(globalStats.def * 0.25);
+ let bonusHP = Math.ceil(globalStats.hp * 0.25);
+ let bonusMP = Math.ceil(globalStats.mp * 0.25);
+
+ let stats = {
+ str: bonusStr,
+ mag: bonusMag,
+ def: bonusDef,
+ hp: bonusHP,
+ mp: bonusMP,
+ };
+
+ addStats(stats);
+ } else {
+ let bonusStr = Math.ceil(globalStats.str * 0.2);
+ let bonusMag = Math.ceil(globalStats.mag * 0.2);
+ let bonusDef = Math.ceil(globalStats.def * 0.2);
+ let bonusHP = Math.ceil(globalStats.hp * 0.2);
+ let bonusMP = Math.ceil(globalStats.mp * 0.2);
+
+ let stats = {
+ str: bonusStr,
+ mag: bonusMag,
+ def: bonusDef,
+ hp: bonusHP,
+ mp: bonusMP,
+ };
+
+ removeStats(stats);
+ updateStats();
+ }
+ });
+});
+
+function unselectRecipe(index) {
+ typeItem[index].style["box-shadow"] = "0px 0px 10px rgba(0, 0, 0, 0.4)";
+ let stats = JSON.parse(typeItem[index].dataset["stats"]);
+ removeStats(stats);
+}
+
+function selectRecipe(item, index) {
+ typeItem[index] = item;
+ item.style["box-shadow"] = "0px 0px 10px rgba(0, 255, 0, 0.8)";
+
+ let stats = JSON.parse(item.dataset["stats"]);
+ addStats(stats);
+}
+
+function addStats(stats) {
+ globalStats.str += stats.str;
+ globalStats.mag += stats.mag;
+ globalStats.def += stats.def;
+ globalStats.hp += stats.hp;
+ globalStats.mp += stats.mp;
+ updateStats();
+}
+
+function removeStats(stats) {
+ globalStats.str -= stats.str;
+ globalStats.mag -= stats.mag;
+ globalStats.def -= stats.def;
+ globalStats.hp -= stats.hp;
+ globalStats.mp -= stats.mp;
+}
+
+const STR_MAX = 5;
+const MAG_MAX = 5;
+const DEF_MAX = 5;
+const HP_MAX = 63;
+const MP_MAX = 50;
+
+const CENTER_POINT = [150, 120];
+
+const STR_MAX_POINT = [150, 20];
+const MAG_MAX_POINT = [50, 100];
+const DEF_MAX_POINT = [250, 100];
+const HP_MAX_POINT = [210, 210];
+const MP_MAX_POINT = [90, 210];
+
+function updateStats() {
+ let strProgress = globalStats.str / STR_MAX;
+ let magProgress = globalStats.mag / MAG_MAX;
+ let defProgress = globalStats.def / DEF_MAX;
+ let hpProgress = globalStats.hp / HP_MAX;
+ let mpProgress = globalStats.mp / MP_MAX;
+
+ let poly = document.getElementById("stats-mod");
+ updatePoint(
+ poly,
+ 0,
+ STR_MAX_POINT,
+ strProgress,
+ globalStats.str,
+ "stat-str",
+ );
+ updatePoint(
+ poly,
+ 4,
+ MAG_MAX_POINT,
+ magProgress,
+ globalStats.mag,
+ "stat-mag",
+ );
+ updatePoint(
+ poly,
+ 1,
+ DEF_MAX_POINT,
+ defProgress,
+ globalStats.def,
+ "stat-def",
+ );
+ updatePoint(poly, 2, HP_MAX_POINT, hpProgress, globalStats.hp, "stat-hp");
+ updatePoint(poly, 3, MP_MAX_POINT, mpProgress, globalStats.mp, "stat-mp");
+}
+
+function updatePoint(poly, statId, max, progress, value, statElem) {
+ poly.points[statId].x =
+ CENTER_POINT[0] + (max[0] - CENTER_POINT[0]) * progress;
+ poly.points[statId].y =
+ CENTER_POINT[1] + (max[1] - CENTER_POINT[1]) * progress;
+
+ document.getElementById(statElem).innerHTML = "+" + value;
+}
diff --git a/templates/components/kh3/style.css b/templates/components/kh3/style.css
new file mode 100644
index 0000000..8c0213f
--- /dev/null
+++ b/templates/components/kh3/style.css
@@ -0,0 +1,62 @@
+#content > h1 {
+ text-align: center;
+ width: 80%;
+ margin-left: auto;
+}
+
+.recipes {
+ font-size: 12px;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: baseline;
+ gap: 16px;
+ width: 80%;
+ margin-right: 0;
+ margin-left: auto;
+}
+
+.recipe {
+ width: 20%;
+ align-self: stretch;
+ box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.4);
+ padding: 16px;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ .ingredients {
+ margin-left: 16px;
+ }
+
+ .stats {
+ table {
+ tr.plus {
+ color: gold;
+ }
+
+ th {
+ padding: 8px;
+ }
+
+ td {
+ background-color: #4d4d4d;
+ }
+ }
+ }
+}
+
+#stats {
+ position: fixed;
+}
+
+svg > text {
+ stroke-width: 0;
+ fill: white;
+}
+
+#hearty-meal-wrapper {
+ text-align: center;
+ width: 80%;
+}
diff --git a/templates/pages/bbs/melding.html b/templates/pages/bbs/melding.html
index bc7c793..c6e391f 100644
--- a/templates/pages/bbs/melding.html
+++ b/templates/pages/bbs/melding.html
@@ -3,173 +3,8 @@
{% block title %}BBS - Command Melding{% endblock %}
{% block head %}
-
-
+
+
{% endblock %}
{% block content %}
diff --git a/templates/pages/ddd/boards.html b/templates/pages/ddd/boards.html
index 2663462..529e3a4 100644
--- a/templates/pages/ddd/boards.html
+++ b/templates/pages/ddd/boards.html
@@ -3,232 +3,8 @@
{% block title %}DDD - Spirit Boards{% endblock %}
{% block head %}
-
-
+
+
{% endblock %}
{% block content %}
diff --git a/templates/pages/kh2/drops.html b/templates/pages/kh2/drops.html
index dfd2e7e..7c44dd3 100644
--- a/templates/pages/kh2/drops.html
+++ b/templates/pages/kh2/drops.html
@@ -4,111 +4,8 @@
{% block title %}KH2 - Drops{% endblock %}
{% block head %}
-
-
+
+
{% endblock %}
{% block content %}
diff --git a/templates/pages/kh3/food-sim.html b/templates/pages/kh3/food-sim.html
index cfac6ee..02d3690 100644
--- a/templates/pages/kh3/food-sim.html
+++ b/templates/pages/kh3/food-sim.html
@@ -3,249 +3,8 @@
{% block title %}KH3 - Food Simulator{% endblock %}
{% block head %}
-
-
+
+
{% endblock %}
{% block content %}