diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1a6bd45 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +package-lock.json binary diff --git a/.gitignore b/.gitignore index 9c691df..decda56 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,109 @@ -.fo76-dumps-ids.db +## NPM +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/ + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + + +## Custom +src/main/.fo76-dumps-ids.db diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..436645e --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,126 @@ +const path = require("path"); + +module.exports = grunt => { + grunt.initConfig({ + pkg: grunt.file.readJSON("package.json"), + clean: { + default: ["dist/"], + }, + copy: { + db: { + files: [{expand: true, cwd: "src/main/", src: "**/.*.db", dest: "dist/"}] + }, + html: { + files: [{expand: true, cwd: "src/main/", src: "**/*.html", dest: "dist/"}] + }, + php: { + files: [{expand: true, cwd: "src/main/", src: "**/*.php", dest: "dist/"}] + }, + }, + focus: { + dev: { + include: ["html", "js", "link", "php"], + }, + }, + replace: { + dev: { + src: ["./dist/*.html", "./dist/*.js"], + replacements: [ + { + from: "%%VERSION_NUMBER%%", + to: "<%= pkg.version %>+" + new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "") + } + ], + overwrite: true + }, + deploy: { + src: ["./dist/*.html", "./dist/*.js"], + replacements: [ + { + from: "%%VERSION_NUMBER%%", + to: "<%= pkg.version %>" + } + ], + overwrite: true + }, + }, + watch: { + html: { + files: ["src/main/**/*.html"], + tasks: ["copy:html"], + }, + js: { + files: ["src/main/**/*.js"], + tasks: ["webpack:dev", "replace:dev"], + }, + link: { + files: ["node_modules/@fwdekker/*/dist/**"], + tasks: ["webpack:dev", "replace:dev"], + }, + php: { + files: ["src/main/**/*.php"], + tasks: ["copy:php"], + }, + }, + webpack: { + options: { + entry: "./src/main/js/index.js", + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: [".js"], + }, + output: { + filename: "bundle.js", + path: path.resolve(__dirname, "dist/"), + }, + }, + dev: { + mode: "development", + devtool: "inline-source-map", + }, + deploy: { + mode: "production", + }, + }, + }); + + grunt.loadNpmTasks("grunt-contrib-clean"); + grunt.loadNpmTasks("grunt-contrib-copy"); + grunt.loadNpmTasks("grunt-contrib-watch"); + grunt.loadNpmTasks("grunt-focus"); + grunt.loadNpmTasks("grunt-text-replace"); + grunt.loadNpmTasks("grunt-webpack"); + + grunt.registerTask("dev", [ + // Pre + "clean", + // Copy files + "copy:db", + "copy:html", + "copy:php", + // Compile JS + "webpack:dev", + "replace:dev", + ]); + grunt.registerTask("dev:server", ["dev", "focus:dev"]); + grunt.registerTask("deploy", [ + // Pre + "clean", + // Copy files + "copy:db", + "copy:html", + "copy:php", + // Compile JS + "webpack:deploy", + "replace:deploy", + ]); + + grunt.registerTask("default", ["dev"]); +}; diff --git a/index.html b/index.html deleted file mode 100644 index 531f124..0000000 --- a/index.html +++ /dev/null @@ -1,274 +0,0 @@ - - - - - - - - - - - Random Fallout 76 record - - - - - -
- -
-
-

Random Fallout 76 record

- -
-

- On this page you can retrieve a random record from the Fallout 76 game files. - Simply select the signatures you want to include below, and then press the "Get random record" - button. -

-
-
-
- - - -
-

Settings

-
- -
Loading... please wait.
- - -
-
- - - -
-

Record

-
-
- - - - -
- - - - - - diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bf8b285 Binary files /dev/null and b/package-lock.json differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..fbeb2ae --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "random-fo76", + "version": "1.0.8", + "description": "Random Fallout 76 record.", + "author": "Felix W. Dekker", + "browser": "dist/bundle.js", + "repository": { + "type": "git", + "url": "git@git.fwdekker.com:FWDekker/random-fo76.git" + }, + "private": true, + "scripts": { + "clean": "grunt clean", + "dev": "grunt dev", + "dev:server": "grunt dev:server", + "deploy": "grunt deploy" + }, + "dependencies": { + "@fwdekker/template": "^0.0.14", + "js-cookie": "^2.2.1" + }, + "devDependencies": { + "grunt": "^1.1.0", + "grunt-cli": "^1.3.2", + "grunt-contrib-clean": "^2.0.0", + "grunt-contrib-copy": "^1.0.0", + "grunt-contrib-watch": "^1.1.0", + "grunt-focus": "^1.0.0", + "grunt-text-replace": "^0.4.0", + "grunt-webpack": "^3.1.3", + "webpack": "^4.42.1", + "webpack-cli": "^3.3.11" + } +} diff --git a/api.php b/src/main/api.php similarity index 100% rename from api.php rename to src/main/api.php diff --git a/src/main/index.html b/src/main/index.html new file mode 100644 index 0000000..15023ff --- /dev/null +++ b/src/main/index.html @@ -0,0 +1,52 @@ + + + + + + + + + + + Random Fallout 76 record + + + + + +
+ +
+ + + + +
+

Settings

+
+ +
Loading... please wait.
+ + +
+
+ + +
+

Record

+
+
+
+ +
+ + + + diff --git a/src/main/js/index.js b/src/main/js/index.js new file mode 100644 index 0000000..a94a953 --- /dev/null +++ b/src/main/js/index.js @@ -0,0 +1,221 @@ +import {$, doAfterLoad, footer, header, nav} from "@fwdekker/template"; +import Cookies from "js-cookie"; + + +const signatureColCount = 8; + + +/** + * Returns an array of the signatures that are currently selected. + */ +const getSelectedSignatures = () => { + const signatures = []; + + const selectedCheckboxes = document.querySelectorAll("#signatures input:checked"); + for (let i = 0; i < selectedCheckboxes.length; i++) { + const selectedCheckbox = selectedCheckboxes[i]; + signatures.push(selectedCheckbox.value); + } + + return signatures; +}; + +/** + * Selects the indicated signatures, and deselects all others. + * + * @param signatures the array of signatures to select + */ +const setSelectedSignatures = signatures => { + const checkboxes = document.querySelectorAll("#signatures input"); + for (let i = 0; i < checkboxes.length; i++) + checkboxes[i].checked = false; + + for (let i = 0; i < signatures.length; i++) + $(`#signature-${signatures[i]}`).checked = true; + + updateSignatureToggle(); +}; + +/** + * Selects all signatures. + */ +const setAllSignatures = checked => { + const checkboxes = document.querySelectorAll("#signatures input"); + for (let i = 0; i < checkboxes.length; i++) + checkboxes[i].checked = checked; + saveSelectedSignaturesToCookie(); + + updateSignatureToggle(); +}; + +/** + * (De)selects signatures based on the selection stored in a cookie. + */ +const loadSelectedSignaturesFromCookie = () => { + const cookie = Cookies.get("selectedSignatures"); + let signatures; + if (cookie === undefined) + signatures = []; + else + signatures = cookie.split(","); + + setSelectedSignatures(signatures); +}; + +/** + * Saves the currently-selected signatures to a cookie. + */ +const saveSelectedSignaturesToCookie = + () => Cookies.set("selectedSignatures", getSelectedSignatures().join(","), { + expires: 5 * 365, + secure: true, + sameSite: "lax" + }); + +/** + * Updates the button used to toggle all signatures on or off. + */ +const updateSignatureToggle = () => { + const signatureToggle = $("#signatureToggle"); + + if (getSelectedSignatures().length === document.querySelectorAll("#signatures input").length) { + signatureToggle.innerHTML = "Deselect all signatures"; + signatureToggle.onclick = () => setAllSignatures(false); + } else { + signatureToggle.innerHTML = "Select all signatures"; + signatureToggle.onclick = () => setAllSignatures(true); + } +}; + + +/** + * Downloads an array of signatures from the API. + * + * @param callback the function to execute with the array of signatures + * @param handle the function to execute if signatures could not be downloaded + */ +const downloadSignatures = (callback, handle) => { + fetch("api.php?action=list-signatures") + .then(response => { + if (!response.ok) { + if (handle) handle(response); + console.error(response); + throw new Error("Failed to fetch list of signatures."); + } + + return response.json(); + }) + .then(signatures => callback(signatures)); +}; + +/** + * Creates buttons for the signatures and adds them to the form. + * + * @param signatures an array of signatures to create buttons for + */ +const createSignatureButtons = signatures => { + const form = $("#signatures"); + form.innerHTML = ""; + + let row; + for (let i = 0; i < signatures.length; i++) { + const signature = signatures[i]; + + if (i % signatureColCount === 0) { + if (row !== undefined) + form.appendChild(row); + + row = document.createElement("div"); + row.className = "row"; + } + + const col = document.createElement("div"); + col.className = "column"; + + const label = document.createElement("label"); + label.htmlFor = `signature-${signature}`; + label.innerHTML = signature; + col.appendChild(label); + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.id = `signature-${signature}`; + checkbox.name = `signature-${signature}`; + checkbox.value = signature; + checkbox.onclick = () => { + updateSignatureToggle(); + saveSelectedSignaturesToCookie(); + }; + col.appendChild(checkbox); + + row.appendChild(col); + } +}; + +/** + * Downloads a random record from the API. + * + * @param callback the function to execute with the record + * @param handle the function to execute if signatures could not be downloaded + */ +const downloadRandomRecord = (callback, handle) => { + const selectedSignatures = getSelectedSignatures(); + + fetch(`api.php?action=get-random&signatures=${selectedSignatures.join(",")}`) + .then(response => { + if (!response.ok) { + if (handle) handle(response); + console.error(response); + throw new Error("Failed to fetch random record."); + } + + return response.text(); + }) + .then(record => callback(record)); +}; + +/** + * Displays a record on the page. + * + * @param record the record to display + */ +const showRecord = (record) => { + $("#output").innerHTML = record; + + const scrollingElement = (document.scrollingElement || document.body); + scrollingElement.scrollTop = scrollingElement.scrollHeight; +}; + + +doAfterLoad(() => { + $("#nav").appendChild(nav()); + $("#header").appendChild(header({ + title: "Random Fallout 76 record", + description: "Retrieve a random record from the Fallout 76 game files" + })); + $("#footer").appendChild(footer({ + author: "Felix W. Dekker", + authorURL: "https://fwdekker.com/", + license: "MIT License", + licenseURL: "https://git.fwdekker.com/FWDekker/interlanguage-checker/src/branch/master/LICENSE", + vcs: "git", + vcsURL: "https://git.fwdekker.com/FWDekker/interlanguage-checker/", + version: "v%%VERSION_NUMBER%%" + })); + $("main").style.display = null; +}); + +doAfterLoad(() => { + $("#submit").onclick = () => downloadRandomRecord(record => showRecord(record)); + + downloadSignatures( + signatures => { + createSignatureButtons(signatures); + loadSelectedSignaturesFromCookie(); + }, + errorResponse => { + const form = $("#signatureForm"); + form.style.color = "red"; + form.innerHTML = "Error: Failed to download signatures." + }); +});