├── CNAME ├── ads.txt ├── _config.yml ├── favicon.ico ├── .gitignore ├── console ├── styles │ ├── account.css │ ├── modify.css │ ├── changelog.css │ ├── edit.css │ └── console.css ├── scripts │ ├── logout.js │ ├── login.js │ ├── base64.js │ ├── utf8.js │ ├── snarkdown.js │ ├── modify.js │ ├── changelog.js │ ├── jwt-decode.min.js │ ├── account.js │ ├── edit.js │ └── console.js ├── login.html ├── logout.html ├── changelog.html ├── modify.html ├── index.html └── edit.html ├── _includes ├── markdown │ ├── admin │ │ ├── requests.md │ │ ├── changelog.md │ │ ├── modify.md │ │ ├── console.md │ │ └── privacy.md │ ├── motd.md │ ├── tables │ │ ├── meme.md │ │ ├── competitive.md │ │ ├── deprecated.md │ │ └── brew.md │ ├── template.md │ ├── request.md │ ├── submit.md │ └── about.md ├── svg │ ├── colors │ │ ├── x.svg │ │ ├── u.svg │ │ ├── b.svg │ │ ├── g.svg │ │ ├── w.svg │ │ └── r.svg │ ├── back.svg │ ├── forward.svg │ ├── edit.svg │ ├── logo.svg │ ├── recommend.svg │ ├── not │ │ ├── recommend.svg │ │ ├── primer.svg │ │ ├── discord-logo.svg │ │ └── discord.svg │ ├── primer.svg │ ├── discord-logo.svg │ └── discord.svg ├── legal.html ├── head.html ├── console-navbar.html ├── javascript │ ├── dom.js │ ├── backend.js │ └── scryfall.js ├── loading.html ├── navbar.html ├── database.html ├── styles │ ├── database.css │ ├── main.css │ └── global.css └── scripts │ └── main.js ├── styles ├── 404.css ├── copyright.css ├── about.css ├── request.css └── submit.css ├── .github └── workflows │ └── json_validate.yaml ├── favicon.svg ├── scripts ├── about.js ├── request.js └── submit.js ├── 404.html ├── README.md ├── LICENSE ├── Gemfile ├── Gemfile.lock ├── about.html ├── copyright.html ├── _data └── decklist_schema.json ├── request.html ├── index.html └── submit.html /CNAME: -------------------------------------------------------------------------------- 1 | cedh-decklist-database.com -------------------------------------------------------------------------------- /ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-4373090976253918, DIRECT, f08c47fec0942fa0 -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | collections_dir: _includes/markdown 2 | collections: 3 | - updates 4 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/averagewagon/cEDH-Decklist-Database/HEAD/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-cache 4 | .jekyll-metadata 5 | vendor 6 | .directory 7 | .kate-swp 8 | -------------------------------------------------------------------------------- /console/styles/account.css: -------------------------------------------------------------------------------- 1 | #content h1, #content h1 a { 2 | text-align: center; 3 | color: white; 4 | margin: 64px auto; 5 | } 6 | -------------------------------------------------------------------------------- /_includes/markdown/admin/requests.md: -------------------------------------------------------------------------------- 1 | ## Requests 2 | The destination of the "Make a Request" form. Deleted requests are purged after 7 days. 3 | -------------------------------------------------------------------------------- /_includes/markdown/motd.md: -------------------------------------------------------------------------------- 1 | The submission deadline for the Tarkir -> Final Fantasy DDB update is May 31st, 2025 at 11:59 pm Eastern Time. Submissions after that date will be considered for the following update. 2 | -------------------------------------------------------------------------------- /_includes/svg/colors/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /_includes/markdown/tables/meme.md: -------------------------------------------------------------------------------- 1 | # Our meme section is deprecated. We will remove the category at a later date. We are not accepting new submissions for this category. 2 | 3 | If there are any questions about this choice, please visit our [Discord server](https://discord.gg/BXPyu2P). -------------------------------------------------------------------------------- /console/scripts/logout.js: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | (function() { "use strict"; 4 | window.addEventListener("load", init); 5 | 6 | // Initialization function 7 | function init() { 8 | window.localStorage.clear(); 9 | window.location.replace("/console/"); 10 | } 11 | 12 | })(); 13 | -------------------------------------------------------------------------------- /styles/404.css: -------------------------------------------------------------------------------- 1 | #content { 2 | flex-direction: column; 3 | align-items: center; 4 | } 5 | 6 | #content h1 { 7 | font-size: 38pt; 8 | } 9 | 10 | #content h2 { 11 | font-size: 28pt; 12 | } 13 | 14 | #content a { 15 | color: skyblue; 16 | font-size: 20pt; 17 | } 18 | -------------------------------------------------------------------------------- /_includes/markdown/tables/competitive.md: -------------------------------------------------------------------------------- 1 | If you're interested in new ideas and brews, please check out the Brewer's Corner (found under the 'section' filter). Before getting onto the main page, lists are initially displayed there. 2 | 3 | --- 4 | 5 | The main page showcases a selection of decks the DDB team has chosen as a snapshot of the cEDH environment. 6 | -------------------------------------------------------------------------------- /_includes/markdown/template.md: -------------------------------------------------------------------------------- 1 | # New Decks 2 | - DECK added to SECTION 3 | 4 | # New deck links 5 | - Added LIST to DECK 6 | 7 | # Modified Decks 8 | - Renamed OLDNAME to NEWNAME 9 | - Added a Discord server to DECK 10 | - Merged DECK1 and DECK2 11 | 12 | # Removed Decks 13 | - Deprecated DECK 14 | - Removed DECK 15 | 16 | # Recommendations 17 | - Recommended DECK 18 | - No longer recommending DECK -------------------------------------------------------------------------------- /_includes/markdown/admin/changelog.md: -------------------------------------------------------------------------------- 1 | ## Edit Changelog 2 | This page gives controls to modify the changelog on "About the DDB". After creating or editing a changelog post, there is a 30 minute cooldown to reduce site load. 3 | 4 | Changelogs are rendered using Markdown. If you are unfamiliar with Markdown, [Github has an excellent introductory guide](https://guides.github.com/features/mastering-markdown/). 5 | -------------------------------------------------------------------------------- /_includes/svg/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /_includes/svg/forward.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /_includes/legal.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.github/workflows/json_validate.yaml: -------------------------------------------------------------------------------- 1 | name: Validate JSONs 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | verify-json-validation: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: Validate JSON 11 | uses: docker://orrosenblatt/validate-json-action:latest 12 | env: 13 | INPUT_SCHEMA: ./_data/decklist_schema.json 14 | INPUT_JSONS: ./_data/database.json 15 | -------------------------------------------------------------------------------- /_includes/svg/colors/u.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /_includes/markdown/request.md: -------------------------------------------------------------------------------- 1 | # Make a Request 2 | You can use this form to send us ideas, feedback or bug reports about the decklist database. If you would like to submit a new deck, please use the form on the [Submit a Deck](/submit) page. 3 | 4 | ## How to Hear Back from us 5 | If you would like to receive a response for a request you've made, leave your Discord username with your request. If you want to know about the status of your request, you can message a Database Manager on our [Discord server](https://discord.gg/BXPyu2P). -------------------------------------------------------------------------------- /_includes/svg/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /_includes/svg/colors/b.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /_includes/svg/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /_includes/svg/recommend.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /_includes/svg/not/recommend.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /_includes/svg/primer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /scripts/about.js: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | (function() {"use strict"; 4 | window.addEventListener("load", init); 5 | 6 | // Initialization function 7 | function init() { 8 | qsa(".arrow").forEach(x => x.addEventListener("click", switchChangelog)); 9 | } 10 | 11 | function switchChangelog() { 12 | const index = this.dataset.index; 13 | qsa(".update").forEach(x => { 14 | if (x.dataset.index !== index) { 15 | x.classList.add("hidden"); 16 | } else { 17 | x.classList.remove("hidden"); 18 | } 19 | }); 20 | } 21 | 22 | /* HELPER FUNCTIONS */ 23 | {% include javascript/dom.js %} 24 | 25 | })(); 26 | -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: none 3 | --- 4 | 5 | 9 | 10 | 11 | {% include head.html %} 12 | 404 - cEDH Decklist Database 13 | 14 | 15 | 16 | 17 | 18 |
19 | {% include navbar.html %} 20 | 21 |
22 |

404

23 |

Page not found

24 |
25 | Return to the Database 26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /_includes/svg/not/primer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /_includes/markdown/admin/modify.md: -------------------------------------------------------------------------------- 1 | ## Modify Site 2 | This page gives edit access to the various text blobs across the site. In order to minimize the load on the website, there is a 30 minute cooldown between submissions made on the same file. The [Github page for the DDB](https://github.com/AverageDragon/cEDH-Decklist-Database) will keep track of all file changes made here. 3 | 4 | With the exception of the Message of the Day (the blue bar displayed on the top of the website), all text will be rendered as Markdown. If you are unfamiliar with Markdown, [Github has an excellent introductory guide](https://guides.github.com/features/mastering-markdown/). 5 | -------------------------------------------------------------------------------- /_includes/svg/discord-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /_includes/svg/not/discord-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /_includes/svg/colors/g.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /_includes/svg/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /_includes/svg/not/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /console/login.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: none 3 | --- 4 | 5 | 9 | 10 | 11 | {% include head.html %} 12 | Log In - cEDH Decklist Database 13 | 14 | 15 | 16 | 17 | 18 | {% include loading.html %} 19 |
20 | {% include console-navbar.html %} 21 | 22 |
23 |

Logging in...

24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cEDH Decklist Database 2 | 3 | A collection of Magic: The Gathering (MTG) decks from the Competitive EDH Format. 4 | 5 | The site is hosted at [cedh-decklist-database.com](https://cedh-decklist-database.com). It utilizes AWS as a backend for the forms, while the main database is rendered statically using Jekyll to improve performance and reduce the load on the backend. 6 | 7 | ## Features 8 | 9 | - Deck listings with Scryfall-imported commanders and user-submitted information 10 | - Sorting, searching, and filtering support on the main database 11 | - Static rendering using Jekyll and Liquid to knit data files into static content 12 | - Forms for new deck submissions and edit requests utilizing ReCaptcha and Firebase 13 | - Responsive web design that renders a mobile-friendly site on smaller screens 14 | - Arc Dark-inspired color theme 15 | -------------------------------------------------------------------------------- /_includes/svg/colors/w.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /_includes/svg/colors/r.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /_includes/markdown/tables/deprecated.md: -------------------------------------------------------------------------------- 1 | The 'Database' category consists of two types of decks: 2 | - Decks that are still viable within the current cEDH landscape, but are not impactful enough on the larger scale to be featured on the Main Page. 3 | - 'Historic' decks that represent cEDH's many years of innovation and meta changes. Most lists here were at the top at one point but were impacted by bans. 4 | 5 | Many of these lists still work in cEDH pods. They might just be not as popular compared to other options or simply need a caring owner/pilot/community to take the deck to the next level once more. cEDH is a vast format with many viable options for those willing to spend the time to learn. 6 | 7 | If you see a deck in "Competitive" that you believe should be "Database" or visa versa, please report it in [Make a Request](/request) or visit our DDB Support Discord server and participate in related forum discussions. 8 | -------------------------------------------------------------------------------- /_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | -------------------------------------------------------------------------------- /console/logout.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: none 3 | --- 4 | 5 | 9 | 10 | 11 | {% include head.html %} 12 | Log Out - cEDH Decklist Database 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% include loading.html %} 20 |
21 | {% include console-navbar.html %} 22 | 23 | 26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /console/scripts/login.js: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | (function() { "use strict"; 4 | window.addEventListener("load", init); 5 | 6 | // Initialization function 7 | async function init() { 8 | try { 9 | let jwt = getJWT(); 10 | let decoded = jwt_decode(jwt); 11 | set("jwt", jwt); 12 | set("username", decoded["cognito:username"]); 13 | set("expire", decoded["exp"]); 14 | window.location.replace("/console/"); 15 | } catch (error) { 16 | clear(); 17 | console.error(error.message); 18 | alert(error.message); 19 | window.location.replace("/console/"); 20 | } 21 | } 22 | 23 | function getJWT() { 24 | let url = window.location.href; 25 | let start = url.indexOf("=") + 1; 26 | let end = url.indexOf("&"); 27 | let jwt = url.substring(start, end); 28 | return jwt; 29 | } 30 | 31 | /* HELPER FUNCTIONS */ 32 | {% include javascript/dom.js %} 33 | {% include javascript/backend.js %} 34 | 35 | })(); 36 | -------------------------------------------------------------------------------- /_includes/markdown/admin/console.md: -------------------------------------------------------------------------------- 1 | # Database Console 2 | Welcome to the **Database Console**! This page renders similarly to the database, but all changes made here are hidden until changes are selected and published using the "Publish Selected Changes" button. 3 | 4 | Each deck on this page has a "Status". Decks which are **PUBLISHED** will appear on the site the next time changes are published, user-submitted decks are **SUBMITTED**, and deleted decks are **DELETED**. **DELETED** decks are purged after 7 days. 5 | 6 | The color of a deck on the "Published Decks" page corresponds to changes made since the last publishing. Newly-added decks are **green**, modified published decks are **blue**, and deleted decks are **red**. When changes are published, these colors will reset in order to indicate parity between the published database and the console. 7 | 8 | To edit a deck entry or change its status, expand it and click on the wrench icon in the left column. 9 | -------------------------------------------------------------------------------- /styles/copyright.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 1200px) { 2 | #content { 3 | flex-direction: column; 4 | } 5 | 6 | aside, section { 7 | width: auto !important; 8 | overflow: visible !important; 9 | min-width: auto !important; 10 | } 11 | 12 | section { 13 | margin: 4px !important; 14 | } 15 | 16 | #icons { 17 | width: 100% !important; 18 | } 19 | 20 | section svg { 21 | height: 28px !important; 22 | width: 28px !important; 23 | min-width: 28px !important; 24 | min-height: 28px !important; 25 | margin-right: 16px; 26 | } 27 | } 28 | 29 | section { 30 | overflow: auto; 31 | padding: 0 16px; 32 | flex-grow: 1; 33 | } 34 | 35 | section svg { 36 | height: 48px; 37 | width: 48px; 38 | min-width: 48px; 39 | min-height: 48px; 40 | margin-right: 16px; 41 | } 42 | 43 | #icons { 44 | flex-grow: 1; 45 | margin: 0 auto; 46 | } 47 | 48 | .discord-svg { 49 | fill: var(--discord); 50 | } 51 | 52 | #icons > div { 53 | display: flex; 54 | align-items: center; 55 | justify-content: flex-start; 56 | } 57 | -------------------------------------------------------------------------------- /_includes/console-navbar.html: -------------------------------------------------------------------------------- 1 |
2 | 22 | 23 |
24 | 25 | {% capture content %} 26 | {%- include markdown/motd.md -%} 27 | {% endcapture %} 28 | {{ content | escape }} 29 | 30 | × 31 |
32 |
33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 AverageDragon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /_includes/markdown/tables/brew.md: -------------------------------------------------------------------------------- 1 | cEDH is increasing in popularity and people want to play with new commanders. While we'd like to see these new commanders represented, many lists aren't maintained after the initial hype period. 2 | 3 | The Brewer's Corner is a solution for many of these concerns. New entries that do not fit under any of the existing ones will go into the Brewer's Corner. In terms of deck evaluation, we will mostly be concerned with optimizing the commander, rather than comparing them to existing entries. We will still have general power level guidelines (for example, it will be hard to admit Isamaru despite how optimized a list might be), but they are less strict than our guidelines for older commanders. 4 | 5 | Each review cycle, the managers and the reviewers revisit the entries in the Brewer's Corner. We'll look into how well the decks are maintained and perform, and how frequently they're played after the initial hype. 6 | 7 | Feedback on Brewer's Corner lists can be shared on the cEDH DDB Support Discord. There are forums there for either finding feedback for your list or discussion where a deck may end up when it is moved off the BC. 8 | -------------------------------------------------------------------------------- /_includes/javascript/dom.js: -------------------------------------------------------------------------------- 1 | /* Functions which interact with the DOM */ 2 | // Query Selector which searches an input element 3 | function iqs(item, query) { 4 | return item.querySelector(query); 5 | } 6 | 7 | // Query Selector All which searches an input element 8 | function iqsa(item, query) { 9 | return item.querySelectorAll(query); 10 | } 11 | 12 | /** 13 | * Returns the element that has the ID attribute with the specified value. 14 | * @param {string} idName - element ID 15 | * @returns {object} DOM object associated with id. 16 | */ 17 | function id(idName) { 18 | return document.getElementById(idName); 19 | } 20 | /** 21 | * Returns the first element that matches the given CSS selector. 22 | * @param {string} query - CSS query selector. 23 | * @returns {object} The first DOM object matching the query. 24 | */ 25 | function qs(query) { 26 | return document.querySelector(query); 27 | } 28 | 29 | /** 30 | * Returns the array of elements that match the given CSS selector. 31 | * @param {string} query - CSS query selector 32 | * @returns {object[]} array of DOM objects matching the query. 33 | */ 34 | function qsa(query) { 35 | return document.querySelectorAll(query); 36 | } 37 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | # Hello! This is where you manage which Jekyll version is used to run. 3 | # When you want to use a different version, change it below, save the 4 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 5 | # 6 | # bundle exec jekyll serve 7 | # 8 | # This will help ensure the proper Jekyll version is running. 9 | # Happy Jekylling! 10 | # This is the default theme for new Jekyll sites. You may change this to anything you like. 11 | gem "minima", "~> 2.5" 12 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 13 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 14 | gem "jekyll" 15 | #gem "github-pages", group: :jekyll_plugins 16 | # If you have any plugins, put them here! 17 | group :jekyll_plugins do 18 | gem "jekyll-feed", "~> 0.12" 19 | end 20 | 21 | # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem 22 | # and associated library. 23 | platforms :mingw, :x64_mingw, :mswin, :jruby do 24 | gem "tzinfo", "~> 1.2" 25 | gem "tzinfo-data" 26 | end 27 | 28 | # Performance-booster for watching directories on Windows 29 | gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] 30 | 31 | -------------------------------------------------------------------------------- /_includes/javascript/backend.js: -------------------------------------------------------------------------------- 1 | /* Functions which help interface with the backend */ 2 | const API_URL = "https://3rxytinw28.execute-api.us-west-2.amazonaws.com/default/DDB-API-Function"; 3 | 4 | // Sends the body to the DDB API 5 | async function sendToDDB(body) { 6 | try { 7 | showLoad(); 8 | let response; 9 | let result = fetch(API_URL, { 10 | method: "POST", 11 | headers: { "Content-Type": "application/json" }, 12 | body: JSON.stringify(body) 13 | }) 14 | .then(resp => { 15 | response = resp; 16 | return response.json(); 17 | }) 18 | .then(info => { 19 | info.success = (response.status >= 200 && response.status < 300); 20 | info.status = response.status; 21 | hideLoad(); 22 | return info; 23 | }); 24 | return result; 25 | } catch (error) { 26 | hideLoad(); 27 | return { success: false, message: error.message }; 28 | } 29 | } 30 | 31 | // Getting a value in local storage 32 | function get(input) { 33 | return window.localStorage.getItem(input); 34 | } 35 | 36 | // Setting a value in local storage 37 | function set(key, value) { 38 | window.localStorage.setItem(key, value); 39 | } 40 | 41 | // Clearing local storage 42 | function clear() { 43 | window.localStorage.clear(); 44 | } 45 | -------------------------------------------------------------------------------- /_includes/loading.html: -------------------------------------------------------------------------------- 1 | 12 | 57 | 63 | -------------------------------------------------------------------------------- /_includes/navbar.html: -------------------------------------------------------------------------------- 1 |
2 | 23 | 24 |
25 | 26 | {% capture content %} 27 | {%- include markdown/motd.md -%} 28 | {% endcapture %} 29 | {{ content | escape }} 30 | 31 | × 32 |
33 |
34 | + Message of the Day 35 |
36 |
37 | 38 | 44 | -------------------------------------------------------------------------------- /styles/about.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 1200px) { 2 | #content { 3 | flex-direction: column; 4 | } 5 | 6 | aside, section { 7 | width: auto !important; 8 | overflow: visible !important; 9 | min-width: auto !important; 10 | } 11 | 12 | section { 13 | margin: 4px !important; 14 | } 15 | } 16 | 17 | #nav-about { 18 | background-color: var(--darker); 19 | } 20 | 21 | aside { 22 | flex-grow: 1; 23 | min-width: 482px; 24 | background-color: var(--secondary-color); 25 | } 26 | 27 | section { 28 | overflow: auto; 29 | padding: 0 16px; 30 | flex-grow: 1; 31 | } 32 | 33 | aside h1 { 34 | font-size: 18pt; 35 | margin: 8px 0; 36 | } 37 | 38 | #updates { 39 | height: 100%; 40 | } 41 | 42 | .update { 43 | display: flex; 44 | flex-direction: column; 45 | height: 100%; 46 | } 47 | 48 | .controls { 49 | background-color: var(--darker); 50 | padding: 16px; 51 | display: flex; 52 | align-items: flex-start; 53 | } 54 | 55 | .controls svg { 56 | height: 48px; 57 | } 58 | 59 | .newer, .older { 60 | cursor: pointer; 61 | } 62 | 63 | .end-arrow { 64 | visibility: hidden; 65 | pointer-events: none; 66 | } 67 | 68 | .title-wrap { 69 | flex-grow: 1; 70 | margin: 0 16px; 71 | } 72 | 73 | .title { 74 | font-size: 18pt; 75 | } 76 | 77 | .date { 78 | font-size: 14pt; 79 | color: gainsboro; 80 | } 81 | 82 | .content { 83 | padding: 16px; 84 | overflow: auto; 85 | flex-grow: 1; 86 | } 87 | -------------------------------------------------------------------------------- /console/scripts/base64.js: -------------------------------------------------------------------------------- 1 | !function(e){var t="object"==typeof exports&&exports,r="object"==typeof module&&module&&module.exports==t&&module,o="object"==typeof global&&global;o.global!==o&&o.window!==o||(e=o);var a=function(e){this.message=e};(a.prototype=new Error).name="InvalidCharacterError";var n=function(e){throw new a(e)},c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",d=/<%= spaceCharacters %>/g,h={encode:function(e){e=String(e),/[^\0-\xFF]/.test(e)&&n("The string to be encoded contains characters outside of the Latin1 range.");for(var t,r,o,a,d=e.length%3,h="",i=-1,f=e.length-d;++i>18&63)+c.charAt(a>>12&63)+c.charAt(a>>6&63)+c.charAt(63&a);return 2==d?(t=e.charCodeAt(i)<<8,r=e.charCodeAt(++i),h+=c.charAt((a=t+r)>>10)+c.charAt(a>>4&63)+c.charAt(a<<2&63)+"="):1==d&&(a=e.charCodeAt(i),h+=c.charAt(a>>2)+c.charAt(a<<4&63)+"=="),h},decode:function(e){var t=(e=String(e).replace(d,"")).length;t%4==0&&(t=(e=e.replace(/==?$/,"")).length),(t%4==1||/[^+a-zA-Z0-9\/]/.test(e))&&n("Invalid character: the string to be decoded is not correctly encoded.");for(var r,o,a=0,h="",i=-1;++i>(-2*a&6)));return h},version:"<%= version %>"};if("function"==typeof define&&"object"==typeof define.amd&&define.amd)define(function(){return h});else if(t&&!t.nodeType)if(r)r.exports=h;else for(var i in h)h.hasOwnProperty(i)&&(t[i]=h[i]);else e.base64=h}(this); 2 | -------------------------------------------------------------------------------- /console/scripts/utf8.js: -------------------------------------------------------------------------------- 1 | !function(r){var n,t,o,e=String.fromCharCode;function i(r){for(var n,t,o=[],e=0,i=r.length;e=55296&&n<=56319&&e=55296&&r<=57343)throw Error("Lone surrogate U+"+r.toString(16).toUpperCase()+" is not a scalar value")}function f(r,n){return e(r>>n&63|128)}function a(r){if(0==(4294967168&r))return e(r);var n="";return 0==(4294965248&r)?n=e(r>>6&31|192):0==(4294901760&r)?(u(r),n=e(r>>12&15|224),n+=f(r,6)):0==(4292870144&r)&&(n=e(r>>18&7|240),n+=f(r,12),n+=f(r,6)),n+=e(63&r|128)}function c(){if(o>=t)throw Error("Invalid byte index");var r=255&n[o];if(o++,128==(192&r))return 63&r;throw Error("Invalid continuation byte")}function h(){var r,e;if(o>t)throw Error("Invalid byte index");if(o==t)return!1;if(r=255&n[o],o++,0==(128&r))return r;if(192==(224&r)){if((e=(31&r)<<6|c())>=128)return e;throw Error("Invalid continuation byte")}if(224==(240&r)){if((e=(15&r)<<12|c()<<6|c())>=2048)return u(e),e;throw Error("Invalid continuation byte")}if(240==(248&r)&&(e=(7&r)<<18|c()<<12|c()<<6|c())>=65536&&e<=1114111)return e;throw Error("Invalid UTF-8 detected")}r.version="3.0.0",r.encode=function(r){for(var n=i(r),t=n.length,o=-1,e="";++o65535&&(i+=e((n-=65536)>>>10&1023|55296),n=56320|1023&n),i+=e(n);return i}(f)}}("undefined"==typeof exports?this.utf8={}:exports); 2 | -------------------------------------------------------------------------------- /scripts/request.js: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | (function() {"use strict"; 4 | window.addEventListener("load", init); 5 | 6 | // Initialization function 7 | function init() { 8 | prepareListeners(); 9 | } 10 | 11 | /** Adds a click listener for all the clickable elements 12 | */ 13 | function prepareListeners() { 14 | qs("form").addEventListener("submit", submitForm); 15 | } 16 | 17 | /** Makes sure the reCaptcha is checked 18 | */ 19 | async function submitForm(event) { 20 | event.preventDefault(); 21 | if (confirm("Are you sure you want to make this request?")) { 22 | if (!grecaptcha.getResponse()) { 23 | alert("Please complete the reCaptcha."); 24 | } else { 25 | let body = scrapeForm(); 26 | let result = await sendToDDB(body); 27 | if (result.success) { 28 | alert(result.message); 29 | window.location.replace("/"); 30 | } else { 31 | if (result.data) { 32 | console.error(result.data); 33 | } 34 | console.error(result.message); 35 | alert(" There was an error:\n" + result.message); 36 | } 37 | } 38 | } 39 | } 40 | 41 | /** Converts the form into a JSON object 42 | */ 43 | function scrapeForm() { 44 | let body = {}; 45 | body.data = {}; 46 | body.data.category = id("category-select").value; 47 | body.data.description = id("description").value; 48 | body.data.username = id("username").value; 49 | 50 | body.rc = grecaptcha.getResponse(); 51 | body.method = "SUBMIT_REQUEST"; 52 | return body; 53 | } 54 | 55 | /* HELPER FUNCTIONS */ 56 | {% include javascript/dom.js %} 57 | {% include javascript/backend.js %} 58 | 59 | })(); 60 | -------------------------------------------------------------------------------- /_includes/markdown/admin/privacy.md: -------------------------------------------------------------------------------- 1 | ## Privacy Policy 2 | ### Information we collect 3 | All of our personally identifiable information (PII), which may include deck links, descriptions, usernames, or any other user-input field, will be removed upon request. This form functions as our users' main avenue for removing information from the Database. We will also respond to requests made on our [Discord server](https://discord.gg/BXPyu2P), or PMs directly to a Database Manager. 4 | 5 | We will never sell, rent, or lease your Discord usernames, IP addresses, or any other submitted information to 3rd parties. We may use Discord usernames or submitted information to contact users in order to confirm information or ask questions about the decks. 6 | 7 | ### Google Ads and Analytics 8 | 9 | Google, as a third party vendor, uses cookies to serve ads and collect analytics information. Google's use of the DART cookie enables it to serve ads to visitors based on their visit to sites they visit on the Internet. 10 | 11 | Website visitors may opt out of the use of the DART cookie by visiting the Google ad and content network privacy policy. Ad blocking software, such as uBlock Origin, will also nullify all of our 3rd-party data collection, and can function as an easy opt-out option. 12 | 13 | ### DMCA Takedowns 14 | If you would like to issue a DMCA takedown regarding your copyrighted content, the [Make a Request](/request) page of the cEDH Decklist Database serves as the official avenue for issuing takedowns. Include a link to a PDF in the body of your submission, and select the "Other" option in the dropdown, and your request will be reviewed by a DDB manager. 15 | -------------------------------------------------------------------------------- /console/styles/modify.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 1200px) { 2 | #content { 3 | flex-direction: column-reverse; 4 | } 5 | 6 | aside, section { 7 | width: auto !important; 8 | overflow: visible !important; 9 | min-width: auto !important; 10 | max-width: 100% !important; 11 | } 12 | 13 | section { 14 | margin: 4px !important; 15 | } 16 | } 17 | 18 | #content { 19 | } 20 | 21 | .dark { 22 | filter: brightness(0%); 23 | } 24 | 25 | .unavailable { 26 | filter: invert(100%) brightness(50%); 27 | } 28 | 29 | #nav-modify { 30 | background-color: var(--darker); 31 | } 32 | 33 | aside { 34 | overflow: auto; 35 | flex-grow: 1; 36 | background-color: var(--secondary-color); 37 | width: 50%; 38 | } 39 | 40 | section { 41 | overflow: auto; 42 | width: 50%; 43 | margin: 16px; 44 | } 45 | 46 | #preview-label { 47 | background-color: var(--lighter); 48 | padding: 8px; 49 | font-size: 24pt; 50 | text-align: center; 51 | display: block; 52 | } 53 | 54 | #preview { 55 | margin: 16px; 56 | overflow: auto; 57 | } 58 | 59 | textarea, select { 60 | background-color: var(--darker); 61 | color: white; 62 | border: none; 63 | } 64 | 65 | #controls { 66 | align-items: stretch; 67 | margin-bottom: 16px; 68 | } 69 | 70 | #file-select { 71 | padding: 8px 16px; 72 | } 73 | 74 | #submit { 75 | background-color: var(--accent-color); 76 | color: black; 77 | border: none; 78 | padding: 8px 16px; 79 | } 80 | 81 | #submit:hover { 82 | filter: brightness(80%) contrast(120%); 83 | } 84 | 85 | #input { 86 | padding: 8px; 87 | font-size: 12pt; 88 | font-family: monospace; 89 | flex-grow: 1; 90 | white-space: pre-wrap; 91 | } 92 | 93 | #texts { 94 | white-space: pre-wrap; 95 | } 96 | -------------------------------------------------------------------------------- /console/scripts/snarkdown.js: -------------------------------------------------------------------------------- 1 | !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):e.snarkdown=n()}(this,function(){function e(e){return e.replace(RegExp("^"+(e.match(/^(\t| )+/)||"")[0],"gm"),"")}function n(e){return(e+"").replace(/"/g,""").replace(//g,">")}function r(o){function c(e){var n=t[e.replace(/\*/g,"_")[1]||""],r=f[f.length-1]==e;return n?n[1]?(f[r?"pop":"push"](e),n[0|r]):n[0]:e}function a(){for(var e="";f.length;)e+=c(f[f.length-1]);return e}var l,u,p,s,g,i=/((?:^|\n+)(?:\n---+|\* \*(?: \*)+)\n)|(?:^```(\w*)\n([\s\S]*?)\n```$)|((?:(?:^|\n+)(?:\t| {2,}).+)+\n*)|((?:(?:^|\n)([>*+-]|\d+\.)\s+.*)+)|(?:\!\[([^\]]*?)\]\(([^\)]+?)\))|(\[)|(\](?:\(([^\)]+?)\))?)|(?:(?:^|\n+)([^\s].*)\n(\-{3,}|={3,})(?:\n+|$))|(?:(?:^|\n+)(#{1,3})\s*(.+)(?:\n+|$))|(?:`([^`].*?)`)|( \n\n*|\n{2,}|__|\*\*|[_*])/gm,f=[],m="",d=0,h={};for(o=o.replace(/^\[(.+?)\]:\s*(.+)$/gm,function(e,n,r){return h[n.toLowerCase()]=r,""}).replace(/^\n+|\n+$/g,"");p=i.exec(o);)u=o.substring(d,p.index),d=i.lastIndex,l=p[0],u.match(/[^\\](\\\\)*\\$/)||(p[3]||p[4]?l='
'+e(n(p[3]||p[4]).replace(/^\n+|\n+$/g,""))+"
":p[6]?(g=p[6],g.match(/\./)&&(p[5]=p[5].replace(/^\d+/gm,"")),s=r(e(p[5].replace(/^\s*[>*+.-]/gm,""))),">"===g?g="blockquote":(g=g.match(/\./)?"ol":"ul",s=s.replace(/^(.*)(\n|$)/gm,"
  • $1
  • ")),l="<"+g+">"+s+""):p[8]?l=''+n(p[7])+'':p[10]?(m=m.replace("",''),l=a()+""):p[9]?l="":p[12]||p[14]?(g="h"+(p[14]?p[14].length:"="===p[13][0]?1:2),l="<"+g+">"+r(p[12]||p[15])+""):p[16]?l=""+n(p[16])+"":(p[17]||p[1])&&(l=c(p[17]||"--"))),m+=u,m+=l;return(m+o.substring(d)+a()).trim()}var t={"":["",""],_:["",""],"\n":["
    "]," ":["
    "],"-":["
    "]};return r}); 2 | //# sourceMappingURL=snarkdown.umd.js.map 3 | -------------------------------------------------------------------------------- /console/scripts/modify.js: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | (function() { "use strict"; 4 | window.addEventListener("load", init); 5 | 6 | // Initialization function 7 | function init() { 8 | if (get("jwt")) { 9 | id("input").addEventListener("keyup", generatePreview); 10 | id("input").addEventListener("change", generatePreview); 11 | id("file-select").addEventListener("change", swapText); 12 | id("submit").addEventListener("click", submit); 13 | generatePreview(); 14 | } 15 | } 16 | 17 | function generatePreview() { 18 | if (id("file-select").value === "MOTD") { 19 | id("preview").innerText = id("input").value; 20 | } else { 21 | id("preview").innerHTML = snarkdown(id("input").value); 22 | } 23 | } 24 | 25 | function swapText() { 26 | const file = id("file-select").value; 27 | if (file === "MOTD") { 28 | id("preview-label").innerText = "Preview Text"; 29 | } else { 30 | id("preview-label").innerText = "Preview Markdown"; 31 | } 32 | 33 | id("input").value = qs("#texts ." + file.toLowerCase()).innerText; 34 | generatePreview(); 35 | } 36 | 37 | async function submit() { 38 | if (!confirm("Are you sure you want to submit your changes to: " + id("file-select").value)) { 39 | return; 40 | } 41 | if (!get("jwt")) { 42 | alert("You are not logged in. You must be logged in to make these changes."); 43 | return; 44 | } 45 | 46 | const body = { 47 | "jwt": get("jwt"), 48 | "method": "MODIFY_SITE", 49 | "file": id("file-select").value, 50 | "content": base64.encode(utf8.encode(id("input").value)) 51 | } 52 | const result = await sendToDDB(body); 53 | if (result.success) { 54 | alert(result.message); 55 | } else { 56 | if (result.data) { 57 | console.error(result.data); 58 | } 59 | console.error(result.message); 60 | alert(result.message); 61 | } 62 | } 63 | 64 | /* HELPER FUNCTIONS */ 65 | {% include javascript/dom.js %} 66 | {% include javascript/backend.js %} 67 | 68 | })(); 69 | -------------------------------------------------------------------------------- /console/scripts/changelog.js: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | (function() { "use strict"; 4 | window.addEventListener("load", init); 5 | 6 | // Initialization function 7 | function init() { 8 | if (get("jwt")) { 9 | id("input").addEventListener("keyup", generatePreview); 10 | id("input").addEventListener("change", generatePreview); 11 | qs("form").addEventListener("submit", submit); 12 | qsa("#changelog-posts li").forEach(item => item.addEventListener("click", switchLog)); 13 | generatePreview(); 14 | } 15 | } 16 | 17 | function generatePreview() { 18 | id("preview").innerHTML = snarkdown(id("input").value); 19 | } 20 | 21 | function switchLog() { 22 | qsa("#changelog-posts li").forEach(item => item.classList.remove("active")); 23 | this.classList.add("active"); 24 | 25 | id("title").value = iqs(this, ".title").innerText; 26 | id("input").value = iqs(this, ".markdown").innerText; 27 | generatePreview(); 28 | } 29 | 30 | async function submit(event) { 31 | event.preventDefault(); 32 | 33 | if (!confirm("Are you sure you want to update the changelog?")) { 34 | return; 35 | } 36 | if (!get("jwt")) { 37 | alert("You are not logged in. You must be logged in to make these changes."); 38 | return; 39 | } 40 | const updateId = qs("#changelog-posts .active").id; 41 | const rawDate = qs("#changelog-posts .active .rawdate").innerText; 42 | const body = { 43 | "jwt": get("jwt"), 44 | "method": "UPDATE_CHANGELOG", 45 | "file": updateId, 46 | "date": rawDate, 47 | "title": base64.encode(utf8.encode(id("title").value)), 48 | "content": base64.encode(utf8.encode(id("input").value)) 49 | } 50 | const result = await sendToDDB(body); 51 | if (result.success) { 52 | alert(result.message); 53 | } else { 54 | if (result.data) { 55 | console.error(result.data); 56 | } 57 | console.error(result.message); 58 | alert(result.message); 59 | } 60 | } 61 | 62 | /* HELPER FUNCTIONS */ 63 | {% include javascript/dom.js %} 64 | {% include javascript/backend.js %} 65 | 66 | })(); 67 | -------------------------------------------------------------------------------- /console/scripts/jwt-decode.min.js: -------------------------------------------------------------------------------- 1 | !function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g>(-2*g&6)):0)e=f.indexOf(e);return i}var f="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";d.prototype=new Error,d.prototype.name="InvalidCharacterError",b.exports="undefined"!=typeof window&&window.atob&&window.atob.bind(window)||e},{}],2:[function(a,b,c){function d(a){return decodeURIComponent(e(a).replace(/(.)/g,function(a,b){var c=b.charCodeAt(0).toString(16).toUpperCase();return c.length<2&&(c="0"+c),"%"+c}))}var e=a("./atob");b.exports=function(a){var b=a.replace(/-/g,"+").replace(/_/g,"/");switch(b.length%4){case 0:break;case 2:b+="==";break;case 3:b+="=";break;default:throw"Illegal base64url string!"}try{return d(b)}catch(c){return e(b)}}},{"./atob":1}],3:[function(a,b,c){"use strict";function d(a){this.message=a}var e=a("./base64_url_decode");d.prototype=new Error,d.prototype.name="InvalidTokenError",b.exports=function(a,b){if("string"!=typeof a)throw new d("Invalid token specified");b=b||{};var c=b.header===!0?0:1;try{return JSON.parse(e(a.split(".")[c]))}catch(f){throw new d("Invalid token specified: "+f.message)}},b.exports.InvalidTokenError=d},{"./base64_url_decode":2}],4:[function(a,b,c){(function(b){var c=a("./lib/index");"function"==typeof b.window.define&&b.window.define.amd?b.window.define("jwt_decode",function(){return c}):b.window&&(b.window.jwt_decode=c)}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./lib/index":3}]},{},[4]); -------------------------------------------------------------------------------- /styles/request.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 1200px) { 2 | #content { 3 | flex-direction: column; 4 | } 5 | 6 | aside, section{ 7 | width: auto !important; 8 | overflow: visible !important; 9 | } 10 | 11 | section { 12 | margin: 4px !important; 13 | } 14 | } 15 | 16 | #nav-request { 17 | background-color: var(--darker); 18 | } 19 | 20 | /* Section Styles */ 21 | section { 22 | margin: 16px; 23 | } 24 | 25 | aside { 26 | background-color: var(--secondary-color); 27 | padding: 16px; 28 | overflow: auto; 29 | width: 30%; 30 | } 31 | 32 | /* Form Styles */ 33 | .flex { 34 | display: flex; 35 | align-items: center; 36 | justify-content: space-between; 37 | flex-wrap: wrap; 38 | } 39 | 40 | .flex-column { 41 | display: flex; 42 | flex-direction: column; 43 | justify-content: space-between; 44 | } 45 | 46 | .subtext { 47 | font-size: 12pt; 48 | padding-top: 4px; 49 | font-style: italic; 50 | } 51 | 52 | form { 53 | font-size: 16pt; 54 | padding-left: 8px; 55 | } 56 | 57 | form > div { 58 | margin-bottom: 16px; 59 | } 60 | 61 | #edit-select { 62 | margin-top: 4px; 63 | } 64 | 65 | form input::placeholder, form textarea::placeholder { 66 | opacity: .75; 67 | font-style: italic; 68 | color: white; 69 | } 70 | 71 | form input, form textarea, form select, form button { 72 | background-color: var(--darker); 73 | color: white; 74 | border: none; 75 | } 76 | 77 | form select { 78 | font-size: 18pt; 79 | padding: 8px 16px; 80 | } 81 | 82 | form [type="text"] { 83 | font-size: 14pt; 84 | padding: 8px; 85 | } 86 | 87 | textarea { 88 | font-size: 12pt; 89 | font-family: sans-serif; 90 | padding: 8px; 91 | } 92 | 93 | #submit-wrap { 94 | display: flex; 95 | flex-wrap: wrap; 96 | justify-content: space-between; 97 | flex-grow: 1; 98 | } 99 | 100 | #submit-button { 101 | font-size: 18pt; 102 | padding-left: 24px; 103 | padding-right: 24px; 104 | background-color: var(--accent-color); 105 | color: black; 106 | } 107 | 108 | #submit-button:hover { 109 | filter: brightness(0.9); 110 | } 111 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.0) 5 | public_suffix (>= 2.0.2, < 5.0) 6 | colorator (1.1.0) 7 | concurrent-ruby (1.1.6) 8 | em-websocket (0.5.1) 9 | eventmachine (>= 0.12.9) 10 | http_parser.rb (~> 0.6.0) 11 | eventmachine (1.2.7) 12 | ffi (1.13.1) 13 | forwardable-extended (2.6.0) 14 | http_parser.rb (0.6.0) 15 | i18n (1.8.3) 16 | concurrent-ruby (~> 1.0) 17 | jekyll (4.1.1) 18 | addressable (~> 2.4) 19 | colorator (~> 1.0) 20 | em-websocket (~> 0.5) 21 | i18n (~> 1.0) 22 | jekyll-sass-converter (~> 2.0) 23 | jekyll-watch (~> 2.0) 24 | kramdown (~> 2.3.1) 25 | kramdown-parser-gfm (~> 1.0) 26 | liquid (~> 4.0) 27 | mercenary (~> 0.4.0) 28 | pathutil (~> 0.9) 29 | rouge (~> 3.0) 30 | safe_yaml (~> 1.0) 31 | terminal-table (~> 1.8) 32 | jekyll-feed (0.15.0) 33 | jekyll (>= 3.7, < 5.0) 34 | jekyll-sass-converter (2.1.0) 35 | sassc (> 2.0.1, < 3.0) 36 | jekyll-seo-tag (2.6.1) 37 | jekyll (>= 3.3, < 5.0) 38 | jekyll-watch (2.2.1) 39 | listen (~> 3.0) 40 | kramdown (2.3.1) 41 | rexml 42 | kramdown-parser-gfm (1.1.0) 43 | kramdown (~> 2.3.1) 44 | liquid (4.0.3) 45 | listen (3.2.1) 46 | rb-fsevent (~> 0.10, >= 0.10.3) 47 | rb-inotify (~> 0.9, >= 0.9.10) 48 | mercenary (0.4.0) 49 | minima (2.5.1) 50 | jekyll (>= 3.5, < 5.0) 51 | jekyll-feed (~> 0.9) 52 | jekyll-seo-tag (~> 2.1) 53 | pathutil (0.16.2) 54 | forwardable-extended (~> 2.6) 55 | public_suffix (4.0.6) 56 | rb-fsevent (0.10.4) 57 | rb-inotify (0.10.1) 58 | ffi (~> 1.0) 59 | rexml (3.3.6) 60 | strscan 61 | rouge (3.21.0) 62 | safe_yaml (1.0.5) 63 | sassc (2.4.0) 64 | ffi (~> 1.9) 65 | strscan (3.1.0) 66 | terminal-table (1.8.0) 67 | unicode-display_width (~> 1.1, >= 1.1.1) 68 | unicode-display_width (1.7.0) 69 | 70 | PLATFORMS 71 | ruby 72 | 73 | DEPENDENCIES 74 | jekyll 75 | jekyll-feed (~> 0.12) 76 | minima (~> 2.5) 77 | tzinfo (~> 1.2) 78 | tzinfo-data 79 | wdm (~> 0.1.1) 80 | 81 | BUNDLED WITH 82 | 2.1.4 83 | -------------------------------------------------------------------------------- /console/scripts/account.js: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | (function() {"use strict"; 4 | window.addEventListener("load", init); 5 | 6 | const LOGIN = "https://cedh-decklist-database.auth.us-west-2.amazoncognito.com/login?client_id=41o965ca7urqoiutm603p7khqc&response_type=token&scope=openid&redirect_uri=https://cedh-decklist-database.com/console/login"; 7 | const LOGOUT = "https://cedh-decklist-database.auth.us-west-2.amazoncognito.com/logout?client_id=41o965ca7urqoiutm603p7khqc&logout_uri=https://cedh-decklist-database.com/console/logout"; 8 | 9 | // Initialization function 10 | function init() { 11 | if (get("expire") > getTime()) { 12 | let warnTime = (get("expire") - getTime() - 310) * 1000; 13 | window.setTimeout(warn, warnTime); 14 | let expireTime = (get("expire") - getTime() - 10) * 1000; 15 | window.setTimeout(expire, expireTime); 16 | 17 | id("nav-account").innerText = get("username") + " - Log Out"; 18 | id("nav-account").href = LOGOUT; 19 | } else { 20 | cover(); 21 | clear(); 22 | id("motd").innerText = "You are not logged in. Log in to access curator controls. "; 23 | const a = document.createElement("a"); 24 | a.setAttribute("href", "/"); 25 | a.innerText = "Click here to navigate back to the Database."; 26 | id("motd").appendChild(a); 27 | qs("header").style.backgroundColor = "pink"; 28 | id("nav-account").href = LOGIN; 29 | } 30 | } 31 | 32 | // Covers the page when not logged in 33 | function cover() { 34 | const div = document.createElement("div"); 35 | div.id = "login-notice"; 36 | id("content").appendChild(div); 37 | } 38 | 39 | function warn() { 40 | alert("Your session will expire in 5 minutes. If you would like to continue, please save your changes, log out, and log in again."); 41 | } 42 | 43 | function expire() { 44 | clear(); 45 | id("motd").innerText = "You are not logged in. Log in to access curator controls."; 46 | qs("header").style.backgroundColor = "pink"; 47 | id("nav-account").href = LOGIN; 48 | id("nav-account").innerText = "Curator Login"; 49 | alert("Your session has expired. Your changes will not be saved. Please log in again to continue"); 50 | } 51 | 52 | function getTime() { 53 | let now = Date.now().toString(); 54 | return parseInt(now.substring(0, now.length - 3)); 55 | } 56 | 57 | /* HELPER FUNCTIONS */ 58 | {% include javascript/dom.js %} 59 | {% include javascript/backend.js %} 60 | 61 | })(); 62 | -------------------------------------------------------------------------------- /about.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: none 3 | --- 4 | 5 | 9 | 10 | 11 | {% include head.html %} 12 | About the DDB - cEDH Decklist Database 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
    21 | {% include navbar.html %} 22 | 23 |
    24 | 48 |
    49 |
    50 | {% capture content %} 51 | {%- include markdown/about.md -%} 52 | {% endcapture %} 53 | {{ content | escape | markdownify | replace: "id=", "data-content-id=" }} 54 |
    55 |
    56 | {% capture content %} 57 | {%- include markdown/admin/privacy.md -%} 58 | {% endcapture %} 59 | {{ content | escape | markdownify | replace: "id=", "data-content-id=" }} 60 |
    61 | {% include legal.html %} 62 |
    63 |
    64 |
    65 | 66 | 67 | -------------------------------------------------------------------------------- /copyright.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: none 3 | --- 4 | 5 | 9 | 10 | 11 | {% include head.html %} 12 | Copyright - cEDH Decklist Database 13 | 14 | 15 | 16 | 17 | 18 | 19 |
    20 | {% include navbar.html %} 21 | 22 |
    23 |
    24 |
    25 |
    {% include svg/discord-logo.svg %} Copyright Discord
    26 |
    27 |
    {% include svg/forward.svg %} MIT License, sourced from: Tabler Icons
    28 |
    {% include svg/primer.svg %} MIT License, sourced from: Tabler Icons
    29 |
    {% include svg/recommend.svg %} MIT License, sourced from: Tabler Icons
    30 |
    {% include svg/edit.svg %} MIT License, sourced from: Tabler Icons
    31 |
    {% include svg/colors/w.svg %} Modified, Copyright: CC BY 4.0 Dave Grandy
    32 |
    {% include svg/colors/u.svg %} Modified, Copyright: Open Font License from Material Design Light
    33 |
    {% include svg/colors/b.svg %} Modified, Copyright: Open Font License from Material Design Light
    34 |
    {% include svg/colors/r.svg %} Modified, Copyright: Open Font License from Material Design Light
    35 |
    {% include svg/colors/g.svg %} Modified, Copyright: Open Font License from WebHostingHub Glyphs
    36 | 37 |
    38 |
    {% include legal.html %}
    39 |
    40 |
    41 |
    42 | 43 | 44 | -------------------------------------------------------------------------------- /_data/decklist_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Magic The Gathering Decklist File", 4 | "description": "A schema for validating MTG Commander decklists with specific properties for commanders, discord, updated date, section, colors, decklists, description, id, recommended, and title.", 5 | "type": "array", 6 | "items": { 7 | "type": "object", 8 | "properties": { 9 | "commander": { 10 | "type": "array", 11 | "minItems": 1, 12 | "items": { 13 | "type": "object", 14 | "properties": { 15 | "name": { "type": "string" }, 16 | "link": { "type": "string", "format": "uri" } 17 | }, 18 | "required": ["name", "link"], 19 | "additionalProperties": false 20 | } 21 | }, 22 | "discord": { 23 | "anyOf": [ 24 | { 25 | "type": "object", 26 | "properties": { 27 | "title": { "type": "string" }, 28 | "link": { "type": "string", "format": "uri" } 29 | }, 30 | "required": ["title", "link"], 31 | "additionalProperties": true 32 | }, 33 | { 34 | "type": "null" 35 | } 36 | ] 37 | }, 38 | "updated": { 39 | "type": "string", 40 | "format": "date-time" 41 | }, 42 | "section": { 43 | "type": "string", 44 | "enum": ["BREW", "COMPETITIVE", "DEPRECATED"] 45 | }, 46 | "colors": { 47 | "type": "array", 48 | "minItems": 0, 49 | "items": { "type": "string" } 50 | }, 51 | "decklists": { 52 | "type": "array", 53 | "minItems": 1, 54 | "items": { 55 | "type": "object", 56 | "properties": { 57 | "link": { "type": "string", "format": "uri" }, 58 | "title": { "type": "string" }, 59 | "primer": { "type": "boolean" } 60 | }, 61 | "required": ["link", "title", "primer"], 62 | "additionalProperties": false 63 | } 64 | }, 65 | "description": { 66 | "type": "string" 67 | }, 68 | "id": { 69 | "type": "string" 70 | }, 71 | "title": { 72 | "type": "string" 73 | } 74 | }, 75 | "required": [ 76 | "commander", 77 | "discord", 78 | "updated", 79 | "section", 80 | "colors", 81 | "decklists", 82 | "description", 83 | "id", 84 | "title" 85 | ], 86 | "additionalProperties": true 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /_includes/markdown/submit.md: -------------------------------------------------------------------------------- 1 | # Submit a Deck 2 | Lists that do not follow the requirements set in the ‘Website’ and ‘Description’ sections will not be reviewed. The decklist requirements are displayed to give you an idea of what types of decks we're looking for. 3 | 4 | > __Although the option exists to include multiple decklists in a single submission, please do not submit more than one. Submit your list, refresh the page, and then submit your second if necessary.__ 5 | 6 | ## Decklist requirements: 7 | - The list must be commander legal ‘out of the box’ (spoiled cards are fine, but only if they will be released before or right after the update). 8 | - A strategy that has an expected winrate of around 25% in a competitive environment. 9 | - Inclusion of all relevant staple cards for the strategy. 10 | - No inclusions that don’t enhance the deck’s gameplan in some way. 11 | - A commander that isn’t easily replaced by a stronger alternative. 12 | - The ability to consistently interact with your opponents, or win the game, around turn three. 13 | - Preferably no overreliance on the commander. 14 | 15 | If you are submitting a "Historic" list for our records, please add "[HISTORIC]" to your deck title. 16 | 17 | ## Decklist website requirements: 18 | - Needs to display ‘Last updated’ information. 19 | - Does not require an account or additional steps to view lists, including custom filters and views that aren’t easily overwritten. 20 | - List must be on Moxfield. 21 | 22 | ## Description requirements: 23 | - Explain the main game plan and backup plan, preferably in relation to different parts of the game such as "establish draw engines in early turns". 24 | - Explain the deck’s strengths, preferably by comparing it to other options. 25 | - Explain the deck’s weaknesses, preferably by comparing it to other options. 26 | - No more than 600 characters (to prevent scrolling). 27 | - Do not refer to the deck in the third-person. If your list is accepted, this description will be used on the DDB. 28 | 29 | Example: Narset Enlightened Exile tries to go for an Underworld Breach combo as soon as possible, preferably at turn two or three. If that fails, it utilizes Narset's recursion ability to establish a safer board state to go for another win attempt. In comparison to other decks in these colors, this deck struggles in the sheer card advantage department but can take that advantage from decks it interacts with. Narset is incredibly flexible, being able to pivot between game plans in the hands of a skilled pilot. 30 | 31 | ## When will my deck be reviewed? 32 | You can see the next submission deadline, as well as estimated update dates, on our [Discord server](https://discord.gg/BXPyu2P). Your deck can be reviewed at any point between its submission and the deadline. 33 | -------------------------------------------------------------------------------- /_includes/javascript/scryfall.js: -------------------------------------------------------------------------------- 1 | /* Functions which work with Scryfall */ 2 | const SCRYFALL_URL = "https://api.scryfall.com/cards/named?fuzzy="; 3 | 4 | // Checks that a commander is valid 5 | async function checkCommander(name) { 6 | try { 7 | const url = SCRYFALL_URL + encodeURI(name); 8 | let response; 9 | 10 | const result = await fetch(url) 11 | .then(resp => { 12 | response = resp; 13 | return response.json(); 14 | }) 15 | .then(info => { 16 | let body = {} 17 | body.success = (response.status >= 200 && response.status < 300); 18 | body.info = info; 19 | return body; 20 | }); 21 | 22 | if (result.success) { 23 | return { success: true, data: result.info }; 24 | } else { 25 | return { success: false, message: result.info.details }; 26 | } 27 | } catch (error) { 28 | console.error(error.message); 29 | return { success: false, message: "Error fetching from scryfall." }; 30 | } 31 | } 32 | 33 | // Checks that the commanders are actual cards 34 | async function getCommanderInfo() { 35 | try { 36 | const commanders = qsa("#commander-wrap input"); 37 | const value = { success: true, commanders: [], colors: [] }; 38 | 39 | const promises = [] 40 | 41 | for (let i = 0; i < commanders.length; i++) { 42 | const commander = commanders[i]; 43 | if (!commander.classList.contains("hidden")) { 44 | const res = checkCommander(commander.value).then(check => { 45 | if (!check.success) { 46 | return { success: false, message: commander.value + " is not a valid card name." }; 47 | } 48 | const scry = check.data; 49 | commander.value = scry.name; 50 | const obj = {}; 51 | obj.name = scry.name; 52 | if (scry.image_uris) { 53 | obj.link = scry.image_uris.normal; 54 | } else if (scry.card_faces) { 55 | obj.link = scry.card_faces[0].image_uris.normal; 56 | } else { 57 | obj.link = "error-getting-image.link"; 58 | } 59 | const colors = []; 60 | for (let j = 0; j < scry.color_identity.length; j++) { 61 | colors.push(scry.color_identity[j].toLowerCase()); 62 | } 63 | return { commander: obj, colors: colors, success: true }; 64 | }); 65 | promises.push(res); 66 | } 67 | } 68 | 69 | showLoad(); 70 | const results = await Promise.all(promises); 71 | hideLoad(); 72 | for (let i = 0; i < results.length; i++) { 73 | const res = results[i]; 74 | if (!res.success) { 75 | return res; 76 | } 77 | value.commanders.push(res.commander); 78 | for (let j = 0; j < res.colors.length; j++) { 79 | if (!value.colors.includes(res.colors[j])) { 80 | value.colors.push(res.colors[j]); 81 | } 82 | } 83 | } 84 | return value; 85 | } catch (error) { 86 | hideLoad(); 87 | console.log(error); 88 | return { success: false, message: error.message }; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /request.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: none 3 | --- 4 | 5 | 9 | 10 | 11 | {% include head.html %} 12 | Make a Request - cEDH Decklist Database 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% include loading.html %} 23 |
    24 | {% include navbar.html %} 25 | 26 |
    27 | 35 |
    36 |
    37 |
    38 | 39 |
    40 |
    If you would like to get the status of a request you have made, please join our Discord server, which is linked in the top right.
    41 | 50 |
    51 | 52 |
    53 | 54 | 55 |
    56 | 57 |
    58 | 59 | 60 |
    61 | 62 |
    63 |
    64 | 65 |
    66 |
    67 | {% include legal.html %} 68 |
    69 |
    70 |
    71 | 72 |
    73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /console/changelog.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: none 3 | --- 4 | 5 | 9 | 10 | 11 | {% include head.html %} 12 | Edit Changelog - cEDH Decklist Database 13 | 14 | 15 | 16 | 17 | 18 | {% include loading.html %} 19 |
    20 | {% include console-navbar.html %} 21 | 22 |
    23 | 48 |
    49 |
    50 |
    Edit
    51 | 52 | 53 | 54 | 55 |
    56 |
    57 | 58 |
    59 |
    60 |
    61 |
    62 |
    63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /console/modify.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: none 3 | --- 4 | 5 | 9 | 10 | 11 | {% include head.html %} 12 | Modify Site - cEDH Decklist Database 13 | 14 | 15 | 16 | 17 | 18 | {% include loading.html %} 19 | 29 | 30 |
    31 | {% include console-navbar.html %} 32 |
    33 | 38 |
    39 |
    40 | {% capture content %} 41 | {%- include markdown/admin/modify.md -%} 42 | {% endcapture %} 43 | {{ content | escape | markdownify | replace: "id=", "data-content-id=" }} 44 |
    45 |
    46 | 56 | 57 |
    58 | 59 |
    60 |
    61 |
    62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /styles/submit.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 1200px) { 2 | #content { 3 | flex-direction: column; 4 | } 5 | 6 | aside, section{ 7 | width: auto !important; 8 | overflow: visible !important; 9 | } 10 | 11 | section { 12 | margin: 4px !important; 13 | } 14 | } 15 | 16 | #nav-submit { 17 | background-color: var(--darker); 18 | } 19 | 20 | /* Section Styles */ 21 | section { 22 | flex-grow: 1; 23 | padding: 16px; 24 | overflow: auto; 25 | } 26 | 27 | aside { 28 | background-color: var(--secondary-color); 29 | padding: 16px; 30 | overflow: auto; 31 | width: 30%; 32 | } 33 | 34 | /* Form Styles */ 35 | .flex { 36 | display: flex; 37 | align-items: center; 38 | justify-content: space-between; 39 | flex-wrap: wrap; 40 | } 41 | 42 | .flex-column { 43 | display: flex; 44 | flex-direction: column; 45 | justify-content: space-between; 46 | } 47 | 48 | form { 49 | font-size: 16pt; 50 | padding-left: 8px; 51 | } 52 | 53 | form > div { 54 | margin-bottom: 16px; 55 | } 56 | 57 | form label { 58 | margin-bottom: 4px; 59 | } 60 | 61 | #title-wrap, #commander-wrap input, #discord-info input { 62 | flex-grow: 1; 63 | } 64 | 65 | #commander-wrap input:last-child, #discord-info input:last-child { 66 | margin-left: 16px; 67 | } 68 | 69 | form ul { 70 | margin: 0; 71 | margin-top: 8px; 72 | padding: 0; 73 | } 74 | 75 | #list-wrap { 76 | display: flex; 77 | flex-direction: column; 78 | } 79 | 80 | #add-deck { 81 | margin-top: 8px; 82 | padding: 12px; 83 | } 84 | 85 | form li { 86 | list-style-type: none; 87 | margin-bottom: 8px; 88 | } 89 | 90 | .list-title, .list-link { 91 | margin-right: 16px; 92 | } 93 | 94 | form button { 95 | font-size: 16pt; 96 | color: white; 97 | } 98 | 99 | form input::placeholder, form textarea::placeholder { 100 | opacity: .75; 101 | font-style: italic; 102 | color: white; 103 | } 104 | 105 | form input, form textarea, form select, form button { 106 | background-color: var(--darker); 107 | color: white; 108 | border: none; 109 | } 110 | 111 | form li div > input { 112 | flex-grow: 1; 113 | } 114 | 115 | .delete-list { 116 | cursor: pointer; 117 | transition: 0.3s; 118 | font-size: 26pt; 119 | line-height: 16px; 120 | margin-right: 8px; 121 | color: red; 122 | } 123 | 124 | form select { 125 | font-size: 18pt; 126 | padding: 8px 16px; 127 | } 128 | 129 | form [type="text"] { 130 | font-size: 14pt; 131 | padding: 8px; 132 | } 133 | 134 | textarea { 135 | font-size: 12pt; 136 | font-family: sans-serif; 137 | padding: 8px; 138 | } 139 | #color-select { 140 | display: flex; 141 | } 142 | 143 | #color-select div { 144 | cursor: pointer; 145 | opacity: 0.3; 146 | height: 36px; 147 | width: 36px; 148 | margin-left: 4px; 149 | } 150 | 151 | #color-select input + div:hover { 152 | opacity: 0.5; 153 | } 154 | 155 | #color-select input:checked + div { 156 | opacity: 1; 157 | } 158 | 159 | #color-select input:checked + div:hover { 160 | opacity: 1; 161 | } 162 | 163 | #discord-wrap { 164 | margin-bottom: 4px; 165 | } 166 | 167 | #list-wrap ul li:only-child .delete-list { 168 | visibility: hidden; 169 | } 170 | 171 | #submit-wrap { 172 | display: flex; 173 | flex-wrap: wrap; 174 | justify-content: space-between; 175 | flex-grow: 1; 176 | } 177 | 178 | #submit-button { 179 | font-size: 18pt; 180 | padding-left: 24px; 181 | padding-right: 24px; 182 | background-color: var(--accent-color); 183 | color: black; 184 | } 185 | 186 | #submit-button:hover { 187 | filter: brightness(0.9); 188 | } 189 | #disclaimer { 190 | font-size: 12pt 191 | } 192 | -------------------------------------------------------------------------------- /console/styles/changelog.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 1200px) { 2 | #content { 3 | flex-wrap: wrap; 4 | } 5 | 6 | aside, section { 7 | width: auto !important; 8 | overflow: visible !important; 9 | min-width: auto !important; 10 | max-width: 100% !important; 11 | } 12 | 13 | section { 14 | margin: 4px !important; 15 | flex-wrap: wrap; 16 | } 17 | 18 | #changelog-posts { 19 | max-height: 200px; 20 | } 21 | 22 | #input-form { 23 | min-width: 450px; 24 | } 25 | 26 | #input { 27 | min-height: 400px; 28 | } 29 | 30 | #preview-wrap { 31 | min-width: 100% !important; 32 | } 33 | } 34 | 35 | #content { 36 | } 37 | 38 | #nav-changelog { 39 | background-color: var(--darker); 40 | } 41 | 42 | aside { 43 | background-color: var(--secondary-color); 44 | max-width: 400px; 45 | display: flex; 46 | flex-direction: column; 47 | } 48 | 49 | section { 50 | display: flex; 51 | flex-grow: 1; 52 | } 53 | 54 | #description { 55 | margin: 0 16px; 56 | margin-top: 8px; 57 | } 58 | 59 | #changelog-posts { 60 | margin: 16px; 61 | list-style-type: none; 62 | padding: 0; 63 | overflow: auto; 64 | } 65 | 66 | #changelog-posts li { 67 | padding: 16px; 68 | border-top: 1px solid darkgray; 69 | display: flex; 70 | align-items: center; 71 | font-size: 16pt; 72 | cursor: pointer; 73 | } 74 | 75 | #changelog-posts li:last-child { 76 | border-bottom: 1px solid darkgray; 77 | } 78 | 79 | .active { 80 | background-color: var(--backdrop-color); 81 | } 82 | 83 | #changelog-posts li:not(.active):hover { 84 | background-color: var(--canvas-color); 85 | } 86 | 87 | .old { 88 | display: flex; 89 | flex-wrap: wrap; 90 | } 91 | 92 | .old div { 93 | width: 100%; 94 | } 95 | 96 | .date { 97 | font-size: 12pt; 98 | color: gainsboro; 99 | } 100 | 101 | .markdown { 102 | white-space: pre-wrap; 103 | } 104 | 105 | /* Section Styles */ 106 | #preview-label, #input-label { 107 | background-color: var(--secondary-color); 108 | padding: 8px; 109 | font-size: 24pt; 110 | text-align: center; 111 | display: block; 112 | } 113 | 114 | #input-label { 115 | border-right: 2px solid var(--darker); 116 | border-left: 2px solid var(--darker); 117 | } 118 | 119 | #input-form { 120 | flex-grow: 1; 121 | } 122 | 123 | #title { 124 | margin: 16px; 125 | background-color: var(--darker); 126 | color: white; 127 | padding: 8px; 128 | font-size: 14pt; 129 | border: none; 130 | } 131 | 132 | #title::placeholder { 133 | color: white; 134 | opacity: .7; 135 | } 136 | 137 | #input { 138 | padding: 8px; 139 | font-size: 12pt; 140 | font-family: monospace; 141 | flex-grow: 1; 142 | white-space: pre-wrap; 143 | background-color: var(--darker); 144 | color: white; 145 | border: none; 146 | margin: 0px 16px; 147 | margin-bottom: 16px; 148 | } 149 | 150 | #submit-wrap { 151 | display: flex; 152 | align-items: center; 153 | justify-content: flex-end; 154 | margin-right: 16px; 155 | margin-top: 16px; 156 | } 157 | 158 | #submit { 159 | background-color: var(--accent-color); 160 | color: black; 161 | border: none; 162 | padding: 8px 16px; 163 | font-size: 16pt; 164 | margin: 0px 16px; 165 | margin-bottom: 16px; 166 | } 167 | 168 | #submit:hover { 169 | filter: brightness(80%) contrast(120%); 170 | } 171 | 172 | #preview-wrap { 173 | min-width: 450px; 174 | max-width: 450px; 175 | background-color: var(--lighter); 176 | } 177 | 178 | #preview { 179 | padding: 16px; 180 | flex-grow: 1; 181 | overflow: auto; 182 | } 183 | 184 | #preview h1 { 185 | font-size: 18pt; 186 | margin: 8px 0; 187 | } 188 | -------------------------------------------------------------------------------- /_includes/database.html: -------------------------------------------------------------------------------- 1 |
      2 | {% assign decks = (site.data.database | sort: 'updated') | reverse %} 3 | {% for d in decks %} 4 | 5 | {% capture primers %} 6 | {% for x in d.decklists %} 7 | {{x.primer}} 8 | {% endfor %} 9 | {% endcapture %} 10 | 11 | 12 |
    • 17 |
      18 |
      19 | {% if d.colors contains "w" %} 20 | {% include svg/colors/w.svg %}{% else %}{% include svg/colors/x.svg %} 21 | {% endif %} 22 | 23 | {% if d.colors contains "u" %} 24 | {% include svg/colors/u.svg %}{% else %}{% include svg/colors/x.svg %} 25 | {% endif %} 26 | 27 | {% if d.colors contains "b" %} 28 | {% include svg/colors/b.svg %}{% else %}{% include svg/colors/x.svg %} 29 | {% endif %} 30 | 31 | {% if d.colors contains "r" %} 32 | {% include svg/colors/r.svg %}{% else %}{% include svg/colors/x.svg %} 33 | {% endif %} 34 | 35 | {% if d.colors contains "g" %} 36 | {% include svg/colors/g.svg %}{% else %}{% include svg/colors/x.svg %} 37 | {% endif %} 38 |
      39 |
      40 | {{d.title | escape}} 41 |
      42 |
      43 | {% if primers contains true %} 44 | {% include svg/primer.svg %}{% else %}{% include svg/not/primer.svg %} 45 | {% endif %} 46 | 47 | {% unless d.discord == null %} 48 | {% include svg/discord.svg %}{% else %}{% include svg/not/discord.svg %} 49 | {% endunless %} 50 |
      51 |
      52 | 53 | 100 |
    • 101 | 102 | {% endfor %} 103 |
    104 | -------------------------------------------------------------------------------- /scripts/submit.js: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | (function() {"use strict"; 4 | window.addEventListener("load", init); 5 | 6 | // Initialization function 7 | function init() { 8 | prepareListeners(); 9 | togglePartner(); 10 | toggleDiscord(); 11 | } 12 | 13 | /** Adds a click listener for all the clickable elements 14 | */ 15 | function prepareListeners() { 16 | id("two-commanders").addEventListener("change", togglePartner); 17 | 18 | id("has-discord").addEventListener("change", toggleDiscord); 19 | 20 | qs(".delete-list").addEventListener("click", deleteDecklist); 21 | 22 | id("add-deck").addEventListener("click", addDecklist); 23 | 24 | qs("form").addEventListener("submit", submitForm); 25 | } 26 | 27 | /** Makes sure the reCaptcha is checked 28 | */ 29 | async function submitForm(event) { 30 | event.preventDefault(); 31 | let scry = await getCommanderInfo(); 32 | if (!scry.success) { 33 | alert(scry.message); 34 | } else if (confirm("Are you sure you want to submit this deck for review?")) { 35 | if (!grecaptcha.getResponse()) { 36 | alert("Please complete the reCaptcha."); 37 | } else { 38 | let body = scrapeForm(scry); 39 | let result = await sendToDDB(body); 40 | if (result.success) { 41 | alert(result.message); 42 | window.location.replace("/"); 43 | } else { 44 | if (result.data) { 45 | console.error(result.data); 46 | } 47 | console.error(result.message); 48 | alert(" There was an error:\n" + result.message); 49 | } 50 | } 51 | } 52 | } 53 | 54 | /** Converts the form into a JSON object 55 | */ 56 | function scrapeForm(scry) { 57 | let body = {}; 58 | let data = {}; 59 | data.section = id("table-select").value; 60 | data.title = id("deck-title").value; 61 | data.description = id("description").value; 62 | data.discord = scrapeDiscord(); 63 | data.decklists = scrapeDecklists(); 64 | data.commander = scry.commanders; 65 | data.colors = scry.colors; 66 | 67 | body.data = data; 68 | body.rc = grecaptcha.getResponse(); 69 | body.method = "SUBMIT_DECK"; 70 | return body; 71 | } 72 | 73 | function scrapeDecklists() { 74 | const decklists = []; 75 | let lists = qsa(".list-entry"); 76 | for (let i = 0; i < lists.length; i++) { 77 | let list = lists[i]; 78 | let d = {}; 79 | d.primer = list.querySelector(".has-primer").checked; 80 | d.title = list.querySelector(".list-title").value; 81 | d.link = list.querySelector(".list-link").value; 82 | decklists.push(d); 83 | } 84 | return decklists; 85 | } 86 | 87 | function scrapeDiscord() { 88 | if (id("has-discord").checked) { 89 | const discord = {}; 90 | discord.title = id("discord-title").value; 91 | discord.link = id("discord-link").value; 92 | return discord; 93 | } else { 94 | return null; 95 | } 96 | } 97 | 98 | /** Activates or deactivates the partner text box 99 | */ 100 | function togglePartner() { 101 | if (id("two-commanders").checked) { 102 | id("commander2").classList.remove("hidden"); 103 | id("commander2").required = true; 104 | } else { 105 | id("commander2").classList.add("hidden"); 106 | id("commander2").required = false; 107 | } 108 | } 109 | 110 | /** Activates or deactivates the Discord server text boxes 111 | */ 112 | function toggleDiscord() { 113 | if (id("has-discord").checked) { 114 | id("discord-info").classList.remove("hidden"); 115 | id("discord-title").required = true; 116 | id("discord-link").required = true; 117 | } else { 118 | id("discord-info").classList.add("hidden"); 119 | id("discord-title").required = false; 120 | id("discord-link").required = false; 121 | } 122 | } 123 | 124 | /** Removes a decklist entry option from the form 125 | */ 126 | function deleteDecklist() { 127 | if (this.parentElement.parentElement.parentElement.childElementCount > 1) { 128 | this.parentElement.parentElement.remove(); 129 | } 130 | } 131 | 132 | /** Adds a new decklist entry to the list of decks 133 | */ 134 | function addDecklist() { 135 | let newList = qs("#list-wrap ul li:first-child").cloneNode(true); 136 | newList.querySelector(".list-title").value = ""; 137 | newList.querySelector(".list-link").value = ""; 138 | newList.querySelector(".has-primer").checked = false; 139 | newList.querySelector(".delete-list").addEventListener("click", deleteDecklist); 140 | qs("#list-wrap ul").appendChild(newList); 141 | } 142 | 143 | /* HELPER FUNCTIONS */ 144 | {% include javascript/dom.js %} 145 | {% include javascript/backend.js %} 146 | {% include javascript/scryfall.js %} 147 | 148 | })(); 149 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: none 3 | --- 4 | 5 | 9 | 10 | 11 | {% include head.html %} 12 | cEDH Decklist Database - Competitive Commander Decks 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
    22 | {% include navbar.html %} 23 |
    24 | + Show Filters 25 |
    26 | 27 |
    28 | 29 | 65 | 66 |
    67 |
    68 |
    69 | 70 | 75 |
    76 |
    77 | 78 | 83 |
    84 |
    85 | 86 | 87 |
    88 |
    89 |
    90 | Competitive Decks 91 |
    92 |
    93 |
    94 | {% capture competitive %} 95 | {%- include markdown/tables/competitive.md -%} 96 | {% endcapture %} 97 | {{ competitive | escape | markdownify | replace: "id=", "data-content-id=" }} 98 |
    99 | 105 | 111 |
    112 | 113 | {% include database.html %} 114 | 115 | {% include legal.html %} 116 |
    117 |
    118 |
    119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /_includes/markdown/about.md: -------------------------------------------------------------------------------- 1 | # About the DDB 2 | 3 | Welcome to the cEDH Decklist Database (DDB). The DDB is a showcase of different decks from cEDH. **The goal of the DDB is to highlight the variety of different decks that exist within that space to help new players acclimate to cEDH concepts and possibilities while also helping established players find inspiration for brewing and playing**. *The DDB is not a tier list*, nor a collection of tournament decks, and should not be considered in any way a list of the “best” decks in cEDH nor an exhaustive list of decks in cEDH. **It is a collection of decks that the DDB Managers and Reviewers believe underscore just some of the variety of options available to players within the world of cEDH.** 4 | 5 | 6 | The decks shown on the DDB are looked over by an experienced team of reviewers before getting final approval to be on the DDB by the manager team. The following is a list of some of the different items that are taken into account when reviewing: 7 | 8 | 9 | -A strategy that can hold its own in a cEDH environment 10 | 11 | -Inclusion of all relevant cards for the deck’s strategy 12 | 13 | -A chosen commander that enhances the deck’s performance 14 | 15 | -The ability to relevantly impact the game state in the earliest turns of a game 16 | 17 | -Listed on an approved decklist site 18 | 19 | 20 | Notably, the DDB does not attempt to use gameplay performance data as a major arbiter of deck evaluation due to the difficult and nebulous nature of acquiring accurate gameplay data for cEDH. Although some team members may be involved in other projects addressing data within cEDH, those projects are not primarily considered when making decisions regarding the DDB. In prior forms of feedback, we would often ask both reviewers and the general cEDH community about their exposure to certain lists. As with performance, this is not a primary factor that is considered with submissions. 21 | 22 | 23 | A submitted deck not being accepted to the DDB does **not** mean that the deck is not competitive, not fun, or not worth playing. It simply means the team was unable to find a good fit for it at that time. One of the more common reasons a decklist is rejected is that the strategy or deck itself is already adequately represented on the DDB. As cEDH shifts, new cards are printed, and opinions change, resubmitted decks often find their way onto the DDB, so a deck not being on the DDB in a given review cycle is not the final word from the DDB team. 24 | 25 | 26 | ## Brewer’s Corner vs. Main Page 27 | 28 | 29 | **With the goal of highlighting different decks within cEDH, the DDB has a Brewer’s Corner page for new decks. The Brewer’s Corner is designed to showcase decks that have had a recent playability surge within the cEDH metagame or have the potential to accomplish such recognition as the deck and pilot-pool develop. The community may not be familiar with these decks at first glance, but those incredibly active within cEDH may recognize these upcoming lists.** 30 | 31 | 32 | New deck archetypes accepted to the DDB will start on the Brewer’s Corner, and the decks on the Brewer’s Corner are all evaluated to move to either the main page or database. If a decklist is maintained, and the strategy has proven itself to be strong enough, decks will move from the Brewer’s Corner to the main page. Decks moving from the BC to the Main Page are rare as it would imply a new deck or strategy taking immediate hold of the larger cEDH metagame. Most decks on the Brewer's Corner, after an unspecific amount of time has passed for their showcase, will be moved to the general Database as long as they remain updated and relevant. 33 | 34 | 35 | ## Database 36 | 37 | 38 | The Database section (currently named 'Database') is meant to display decks viable within cEDH but may not represent the same format-wide impact as those seen on the Main Page. The database also contains decks that have historical significance to the progression of cEDH throughout the years. These are labeled with a [HISTORIC] tag. 39 | 40 | 41 | As the cEDH meta grows, shifts, and reacts to changes, decks will freely swap between the Main Page and Database. A deck's location is a welcome topic of civil debate on the DDB support server. *Decks not updated in six months will be deleted regardless of section.* We use 'Last updated' fields on decklist sites to determine this duration. 42 | 43 | 44 | ## The DDB Team 45 | 46 | As mentioned above, the DDB is maintained by a team of managers and reviewers. The review team provides initial feedback on all the lists submitted to the DDB, in addition to providing ongoing feedback about the main DDB page, Brewer’s Corner lists, and the website itself. We aim to keep the review team large so analysis can be fresh and varied. 47 | 48 | The management team oversees the review team’s work and uses their feedback to make final decisions regarding the DDB, in addition to handling the non-technical work on the DDB and Discord. The current management team consists of: 49 | 50 | -Natux 51 | 52 | -Squirrelmob 53 | 54 | -Mormonator 55 | 56 | Additionally, the DDB would not function without the technical work of our volunteer web director, Average Dragon. 57 | 58 | 59 | ## Frequently Asked Questions 60 | - [Please check out our FAQ in this Google Document](https://docs.google.com/document/d/14AmpkuN2-YUcyU3xpyEurGMde_0aMbfQ5PMbLGOqkF4/edit) 61 | 62 | - If you are interested in joining the DDB team, we periodically reach out to add new reviewers. New reviewer applications typically occur 1-2 times a year. Please visit our support Discord for more information. 63 | -------------------------------------------------------------------------------- /_includes/styles/database.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 800px) { 2 | .main { 3 | flex-wrap: wrap; 4 | } 5 | 6 | .ddb-title { 7 | order: 1; 8 | margin-left: 0 !important; 9 | margin-top: 8px; 10 | width: 80%; 11 | } 12 | 13 | .sub { 14 | flex-wrap: wrap; 15 | } 16 | 17 | .ddb-links { 18 | max-width: 100% !important; 19 | min-width: 0% !important; 20 | width: 100%; 21 | margin-right: 0 !important; 22 | } 23 | 24 | .ddb-section { 25 | display: none; 26 | } 27 | 28 | .ddb-description { 29 | order: 1; 30 | } 31 | 32 | #decks .btn:hover { 33 | background-color: var(--backdrop-color); 34 | } 35 | 36 | .sub { 37 | max-height: 100% !important; 38 | } 39 | } 40 | 41 | @media (max-width: 1200px) { 42 | .ddb-images { 43 | display: none; 44 | } 45 | 46 | .ddb-commanders { 47 | order: -1; 48 | } 49 | } 50 | 51 | @media (min-width: 1201px) { 52 | .ddb-commanders { 53 | display: none; 54 | } 55 | } 56 | 57 | @media (max-width: 800px) { 58 | .ddb-info { 59 | order: -1; 60 | } 61 | 62 | .ddb-commanders { 63 | font-size: 14pt; 64 | } 65 | 66 | .ddb-date-wrap { 67 | order: 1; 68 | } 69 | 70 | .ddb-colors svg, .ddb-icons svg { 71 | height: 24px !important; 72 | width: 24px !important; 73 | min-height: 24px !important; 74 | min-width: 24px !important; 75 | } 76 | 77 | .ddb-title { 78 | font-size: 14pt !important; 79 | } 80 | 81 | .ddb-decklists { 82 | margin-bottom: 8px; 83 | } 84 | } 85 | 86 | #decks, #decks ul { 87 | list-style-type: none; 88 | } 89 | 90 | #decks { 91 | margin: 0; 92 | padding: 0; 93 | } 94 | 95 | /* Main Row Styles */ 96 | .main { 97 | border-top: 2px solid var(--darker); 98 | padding: 8px 16px; 99 | cursor: pointer; 100 | text-align: left; 101 | } 102 | 103 | .ddb-colors svg { 104 | height: 28px; 105 | width: 28px; 106 | min-height: 28px; 107 | min-width: 28px; 108 | margin-right: 4px; 109 | } 110 | 111 | .ddb-title { 112 | margin-left: 90px; 113 | flex-grow: 1; 114 | font-size: 16pt; 115 | } 116 | 117 | .main .ddb-status { 118 | font-size: 18pt; 119 | margin: 0 16px; 120 | font-family: monospace; 121 | overflow-wrap: normal; 122 | } 123 | 124 | .ddb-icons svg { 125 | height: 28px; 126 | width: 28px; 127 | min-height: 28px; 128 | min-width: 28px; 129 | margin-left: 4px; 130 | } 131 | 132 | /* Sub Row Styles */ 133 | .sub { 134 | background-color: var(--lighter); 135 | align-items: stretch; 136 | padding: 12px; 137 | max-height: 334px; 138 | } 139 | 140 | /* Column 1 */ 141 | .ddb-images { 142 | position: relative; 143 | margin-right: 12px; 144 | min-width: 240px; 145 | max-width: 240px; 146 | min-height: 334px; 147 | max-height: 334px; 148 | } 149 | 150 | .ddb-images img { 151 | border-radius: 12px; 152 | width: 91%; 153 | position: absolute; 154 | } 155 | 156 | .ddb-images img:hover { 157 | z-index: 2; 158 | } 159 | 160 | .ddb-images img:nth-child(2) { 161 | margin-top: 31px; 162 | margin-left: 20px; 163 | } 164 | 165 | .ddb-images img:only-child { 166 | width: 100%; 167 | } 168 | 169 | /* Column 2 */ 170 | .ddb-links { 171 | background-color: var(--canvas-color); 172 | min-width: 320px; 173 | max-width: 320px; 174 | margin-right: 12px; 175 | } 176 | 177 | .ddb-discord { 178 | font-size: 14pt; 179 | padding: 4px 8px; 180 | display: flex; 181 | align-items: center; 182 | color: white; 183 | text-decoration: none; 184 | text-align: left; 185 | } 186 | 187 | .ddb-discord:hover { 188 | text-decoration: underline; 189 | } 190 | 191 | .ddb-discord.disabled:hover { 192 | text-decoration: none; 193 | background-color: var(--backdrop-color); 194 | } 195 | 196 | .ddb-discord svg { 197 | height: 36px; 198 | width: 36px; 199 | min-height: 36px; 200 | min-width: 36px; 201 | fill: var(--discord); 202 | margin-right: 12px; 203 | } 204 | 205 | .ddb-decklists { 206 | flex-grow: 1; 207 | padding: 0 8px; 208 | overflow: auto; 209 | } 210 | 211 | .ddb-decklists li { 212 | margin-top: 8px; 213 | border-bottom: 2px solid var(--lighter); 214 | } 215 | 216 | .ddb-decklists a { 217 | color: skyblue; 218 | display: flex; 219 | align-items: center; 220 | text-decoration: none; 221 | font-size: 13pt; 222 | } 223 | 224 | .ddb-decklists a:hover { 225 | text-decoration: underline; 226 | } 227 | 228 | .ddb-decklists svg { 229 | height: 28px; 230 | width: 28px; 231 | min-height: 28px; 232 | min-width: 28px; 233 | margin-right: 8px; 234 | } 235 | 236 | .ddb-commanders { 237 | padding: 0; 238 | } 239 | 240 | .ddb-commanders li { 241 | border-bottom: 2px solid var(--darker); 242 | text-align: left; 243 | padding: 4px 12px; 244 | } 245 | 246 | .ddb-commanders a { 247 | color: skyblue; 248 | line-height: 1.5; 249 | font-size: 14pt; 250 | } 251 | 252 | /* Column 3 */ 253 | .ddb-info { 254 | flex-grow: 1; 255 | background-color: var(--canvas-color); 256 | } 257 | 258 | .ddb-section { 259 | min-height: 44px; 260 | padding: 0 8px; 261 | text-align: left; 262 | font-size: 16pt; 263 | background-color: var(--backdrop-color); 264 | } 265 | 266 | .ddb-description { 267 | overflow: auto; 268 | padding: 8px; 269 | color: gainsboro; 270 | flex-grow: 1; 271 | } 272 | 273 | .ddb-updates { 274 | } 275 | 276 | .ddb-date { 277 | text-align: right; 278 | flex-grow: 1; 279 | font-size: 14pt; 280 | font-weight: bold; 281 | padding: 8px; 282 | } 283 | -------------------------------------------------------------------------------- /submit.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: none 3 | --- 4 | 5 | 9 | 10 | 11 | {% include head.html %} 12 | Submit a Deck - cEDH Decklist Database 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% include loading.html %} 23 |
    24 | {% include navbar.html %} 25 | 26 |
    27 | 35 |
    36 |
    37 |
    38 | 43 |
    44 | 45 |
    46 |
    47 | 48 | 49 |
    50 |
    51 | 52 |
    53 |
    54 | 55 |
    56 | 57 |
    59 |
    60 |
    61 | 62 | 63 |
    64 |
    65 | 66 |
    67 | 68 | 69 |
    70 | 71 |
    72 |
    73 | 74 | 75 |
    76 | 80 |
    81 | 82 |
    83 |
      84 |
    • 85 |
      86 | × 87 | 88 | 89 |
      90 | 94 |
      95 |
      96 |
    • 97 |
    98 | 99 |
    100 | 101 |
    102 |
    103 | 104 |
    105 | By clicking submit, you certify that you are the sole author of any of the content being submitted or that you have permission from any other authors to submit 106 |
    107 |
    108 |
    109 | {% include legal.html %} 110 |
    111 |
    112 | 113 |
    114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /_includes/styles/main.css: -------------------------------------------------------------------------------- 1 | /* Advertisement Size Adjustment */ 2 | @media (min-width: 1201px) { 3 | #content { 4 | overflow: hidden; 5 | } 6 | 7 | #aside { 8 | position: absolute; 9 | } 10 | 11 | #advertisement { 12 | margin-top: 16px; 13 | } 14 | } 15 | 16 | /* Mobile Adjustments */ 17 | @media (max-width: 1200px) { 18 | #content { 19 | flex-wrap: wrap; 20 | } 21 | 22 | aside { 23 | width: 100% !important; 24 | height: auto !important; 25 | min-width: 0 !important; 26 | } 27 | 28 | #advertisement { 29 | height: auto !important; 30 | width: 100% !important; 31 | margin: 0 !important; 32 | padding: 0 !important; 33 | position: fixed; 34 | bottom: 0; 35 | left: 0; 36 | z-index: 10; 37 | } 38 | 39 | #advertisement { 40 | background-color: var(--darker); 41 | } 42 | 43 | #database-controls { 44 | flex-wrap: wrap; 45 | } 46 | 47 | #db-search { 48 | padding: 8px !important; 49 | margin: 8px; 50 | } 51 | 52 | #db-section, #db-sort { 53 | margin: 8px !important; 54 | flex-grow: 1; 55 | } 56 | } 57 | 58 | @media (min-width: 801px) { 59 | .color-inactive:hover { 60 | transition: 0.0s; 61 | opacity: 0.6; 62 | } 63 | 64 | .filter-inactive:hover { 65 | transition: 0.0s; 66 | background-color: var(--canvas-color); 67 | } 68 | } 69 | 70 | @media (max-width: 800px) { 71 | #mobile-filters { 72 | top: 74px; 73 | position: fixed; 74 | width: 100%; 75 | z-index: 101; 76 | } 77 | 78 | #content { 79 | padding-top: 108px !important; 80 | } 81 | 82 | .color-inactive { 83 | transition: 0.0s !important; 84 | } 85 | 86 | .filter-inactive { 87 | transition: 0.0s !important; 88 | } 89 | 90 | section { 91 | display: flex; 92 | flex-direction: column; 93 | } 94 | 95 | aside { 96 | position: fixed; 97 | padding-left: 0 !important; 98 | z-index: 4; 99 | } 100 | 101 | aside h2 { 102 | display: none; 103 | } 104 | 105 | #mobile-filters { 106 | padding: 8px 16px; 107 | font-size: 12pt; 108 | text-align: center; 109 | background-color: var(--darker); 110 | } 111 | 112 | #filter-wrap { 113 | padding: 0 16px; 114 | padding-top: 16px; 115 | z-index: 13; 116 | } 117 | 118 | #database-controls { 119 | position: fixed; 120 | background-color: var(--secondary-color); 121 | margin-top: 240px; 122 | z-index: 12; 123 | left: 0; 124 | padding-bottom: 16px; 125 | } 126 | 127 | #database-controls label { 128 | display: none; 129 | } 130 | 131 | #database-controls select, #database-controls .flex-column { 132 | flex-grow: 1; 133 | } 134 | 135 | #mobile-desc { 136 | padding: 16px 16px; 137 | font-size: 14pt; 138 | text-align: center; 139 | background-color: var(--darker); 140 | } 141 | 142 | #decks { 143 | padding-top: 16px !important; 144 | } 145 | 146 | #db-description { 147 | font-size: 12pt; 148 | } 149 | } 150 | 151 | #copyright { 152 | margin-top: 256px; 153 | margin-bottom: 16px; 154 | } 155 | 156 | /* Aside Styles */ 157 | aside { 158 | width: 300px; 159 | min-width: 300px; 160 | padding: 0 8px; 161 | z-index: 0; 162 | background-color: var(--secondary-color); 163 | display: flex; 164 | flex-direction: column; 165 | } 166 | 167 | aside h2 { 168 | font-size: 16pt; 169 | padding: 16px; 170 | margin: 0; 171 | } 172 | 173 | aside ul { 174 | margin: 16px 8px; 175 | } 176 | 177 | aside li { 178 | padding: 16px; 179 | border-top: 1px solid darkgray; 180 | display: flex; 181 | align-items: center; 182 | height: 30px; 183 | cursor: pointer; 184 | } 185 | 186 | aside li:last-child { 187 | border-bottom: 1px solid darkgray; 188 | } 189 | 190 | aside li span { 191 | margin-left: 16px; 192 | } 193 | 194 | aside li svg { 195 | width: 36px; 196 | height: 36px; 197 | min-width: 36px; 198 | min-height: 36px; 199 | } 200 | 201 | #color-filters { 202 | display: flex; 203 | align-items: center; 204 | justify-content: space-between; 205 | padding-left: 24px; 206 | padding-right: 24px; 207 | } 208 | 209 | .color-inactive { 210 | opacity: 0.3; 211 | transition: 0.3s; 212 | } 213 | 214 | .filter-active { 215 | background-color: var(--backdrop-color); 216 | } 217 | 218 | .filter-inactive { 219 | transition: 0.3s; 220 | } 221 | 222 | /* Section Styles */ 223 | section { 224 | overflow: auto; 225 | padding: 16px; 226 | flex-grow: 1; 227 | } 228 | 229 | #database-controls { 230 | display: flex; 231 | justify-content: space-between; 232 | } 233 | 234 | #database-controls { 235 | align-items: stretch; 236 | } 237 | 238 | #db-section, #db-search, #db-sort { 239 | background-color: var(--darker); 240 | color: white; 241 | border: none; 242 | padding: 8px; 243 | font-size: 14pt; 244 | } 245 | 246 | #database-controls label { 247 | font-size: 14pt; 248 | margin-bottom: 4px; 249 | } 250 | 251 | #db-search { 252 | flex-grow: 1; 253 | font-size: 14pt; 254 | padding: 0 8px; 255 | color: white; 256 | border: none; 257 | background-color: var(--darker); 258 | } 259 | 260 | #search-wrap { 261 | flex-grow: 1; 262 | } 263 | 264 | #db-search::placeholder { 265 | opacity: .9; 266 | color: white; 267 | } 268 | 269 | #db-section, #db-sort { 270 | font-size: 16pt; 271 | padding: 8px; 272 | margin-right: 16px; 273 | color: white; 274 | border: 0; 275 | min-width: 220px; 276 | } 277 | 278 | #db-description { 279 | margin: 16px 0; 280 | line-height: 1.5; 281 | font-size: 12pt; 282 | } 283 | 284 | /* Database Styles */ 285 | {% include styles/database.css %} 286 | -------------------------------------------------------------------------------- /_includes/scripts/main.js: -------------------------------------------------------------------------------- 1 | (function() {"use strict"; 2 | window.addEventListener("load", init); 3 | 4 | // MTG Color ordering converted to binary 5 | const COLOR_ORDER = [ 6 | 0, 7 | 1,2,4,8,16, 8 | 3,6,12,24,17, 9 | 5,10,20,9,18, 10 | 19,7,14,28,25, 11 | 13,26,21,11,22, 12 | 15,30,29,27,23, 13 | 31 14 | ] 15 | 16 | // Initialization function 17 | function init() { 18 | prepareAd(); 19 | sortTable(); 20 | id("db-sort").addEventListener("change", sortTable); 21 | id("db-search").addEventListener("change", applyFilters); 22 | id("db-section").addEventListener("change", changeSection); 23 | qsa(".main").forEach(item => item.addEventListener("click", toggleSub)); 24 | id("mobile-filters").addEventListener("click", toggleMobileFilters); 25 | id("mobile-desc").addEventListener("click", toggleMobileDescription); 26 | id("db-search").addEventListener("click", mobileSearchInput); 27 | 28 | qsa("#color-filters > div").forEach(item => item.addEventListener("click", () => { 29 | item.classList.toggle("color-inactive"); 30 | item.classList.toggle("color-active"); 31 | applyFilters(); 32 | })); 33 | 34 | qsa("#entry-filters > li").forEach(item => item.addEventListener("click", () => { 35 | item.classList.toggle("filter-inactive"); 36 | item.classList.toggle("filter-active"); 37 | applyFilters(); 38 | })); 39 | } 40 | 41 | function prepareAd() { 42 | if (window.innerWidth > 1200) { 43 | id("desktop-ad").classList.add("adsbygoogle"); 44 | id("mobile-ad").classList.add("hidden"); 45 | } else { 46 | id("desktop-ad").classList.add("hidden"); 47 | id("mobile-ad").classList.add("adsbygoogle"); 48 | } 49 | try { 50 | (adsbygoogle = window.adsbygoogle || []).push({}); 51 | } catch (error) { 52 | console.log("Adblocker detected"); 53 | } 54 | } 55 | 56 | function mobileSearchInput() { 57 | const search = id("db-search"); 58 | if (window.innerWidth <= 800) { 59 | search.value = prompt("Search the Database:", search.value); 60 | search.blur(); 61 | applyFilters(); 62 | } 63 | } 64 | 65 | function toggleMobileDescription() { 66 | id("db-description").classList.toggle("mhide"); 67 | } 68 | 69 | function toggleMobileFilters() { 70 | if (id("mobile-filters").classList.contains("shown")) { 71 | id("mobile-filters").classList.remove("shown"); 72 | id("filter-wrap").classList.add("mhide"); 73 | id("database-controls").classList.add("mhide"); 74 | id("mobile-filters").innerText = "+ Show Filters"; 75 | } else { 76 | id("mobile-filters").classList.add("shown"); 77 | id("filter-wrap").classList.remove("mhide"); 78 | id("database-controls").classList.remove("mhide"); 79 | id("mobile-filters").innerText = "- Hide Filters"; 80 | } 81 | } 82 | 83 | // Toggles the visibility of a subrow 84 | function toggleSub() { 85 | const entry = iqs(this.parentElement, ".sub"); 86 | entry.classList.toggle("hidden"); 87 | iqsa(entry, ".ddb-images img").forEach(card => { 88 | if (!card.src && window.innerWidth > 1200) { 89 | card.src = card.dataset.src; 90 | } 91 | }); 92 | } 93 | 94 | // Sorts a table with the available sort values being: NEW, TITLE, COLOR 95 | function sortTable() { 96 | const sort = id("db-sort").value; 97 | const entries = [...qsa("#decks > li")]; 98 | 99 | const sorted = entries.sort((a, b) => { 100 | if (sort === "NEW") { return b.dataset.updated.localeCompare(a.dataset.updated); } 101 | if (sort === "TITLE") { return a.dataset.title.localeCompare(b.dataset.title); } 102 | if (sort === "COLOR") { return convertColor(a.dataset.colors) - convertColor(b.dataset.colors); } 103 | return 0; 104 | }); 105 | 106 | const decks = id("decks"); 107 | sorted.forEach(item => decks.appendChild(item)); 108 | } 109 | 110 | // Converts a color array or color string into a number for sorting purposes 111 | function convertColor(colors) { 112 | let val = 0; 113 | if (colors.includes("w")) { val += 1 } 114 | if (colors.includes("u")) { val += 2 } 115 | if (colors.includes("b")) { val += 4 } 116 | if (colors.includes("r")) { val += 8 } 117 | if (colors.includes("g")) { val += 16 } 118 | return COLOR_ORDER.indexOf(val); 119 | } 120 | 121 | function applyFilters() { 122 | const primer = id("primer-only").classList.contains("filter-active"); 123 | const discord = id("discord-only").classList.contains("filter-active"); 124 | const section = id("db-section").value; 125 | const search = id("db-search").value.toLowerCase(); 126 | const colors = []; 127 | qsa(".color-active").forEach(color => colors.push(color.dataset.c)); 128 | 129 | qsa("#decks > li").forEach(deck => { 130 | let hide = false; 131 | if (primer && iqs(deck, ".ddb-icons .primer-svg").classList.contains("unavailable")) { 132 | hide = true; 133 | } else if (discord && iqs(deck, ".ddb-icons .discord-svg").classList.contains("unavailable")) { 134 | hide = true; 135 | } else if (section !== iqs(deck, ".ddb-section").innerText.trim()) { 136 | hide = true; 137 | } else if (search && !deck.textContent.toLowerCase().includes(search)) { 138 | hide = true; 139 | } else if (colors) { 140 | colors.forEach(color => { 141 | if (!deck.dataset.colors.includes(color)) { 142 | hide = true; 143 | return; 144 | } 145 | }); 146 | } 147 | 148 | hide ? deck.classList.add("hidden") : deck.classList.remove("hidden"); 149 | }); 150 | } 151 | 152 | function changeSection() { 153 | applyFilters(); 154 | const section = id("db-section").value.toLowerCase(); 155 | id("mobile-desc").innerText = section.charAt(0).toUpperCase() + section.slice(1) + " Decks"; 156 | qsa("#db-description > div").forEach(item => { 157 | item.id.includes(section) ? item.classList.remove("hidden") : item.classList.add("hidden"); 158 | }); 159 | } 160 | 161 | /* HELPER FUNCTIONS */ 162 | {% include javascript/dom.js %} 163 | 164 | })(); 165 | -------------------------------------------------------------------------------- /_includes/styles/global.css: -------------------------------------------------------------------------------- 1 | /* Utility Classes */ 2 | :root { 3 | --backdrop-color: #383c4a; 4 | --canvas-color: #404552; 5 | --secondary-color: #4b5162; 6 | --accent-color: #8ebbd1; 7 | --darker: #323643; 8 | --lighter: #454b58; 9 | --discord: #8C9EFF; 10 | scrollbar-color: #222222 #424242; 11 | } 12 | 13 | * { 14 | overflow-wrap: anywhere; 15 | } 16 | 17 | .bw, .bu, .bb, .br, .bg, .bx { 18 | font-family: 'courier new', monospace; 19 | } 20 | 21 | .ns { 22 | -webkit-user-select: none !important; /* Safari */ 23 | -moz-user-select: none !important; /* Firefox */ 24 | -ms-user-select: none !important; /* IE10+/Edge */ 25 | user-select: none !important; /* Standard */ 26 | } 27 | 28 | .hidden { 29 | display: none !important; 30 | } 31 | 32 | .flex { 33 | display: flex; 34 | align-items: center; 35 | justify-content: space-between; 36 | } 37 | 38 | .flex-column { 39 | display: flex; 40 | flex-direction: column; 41 | justify-content: space-between; 42 | } 43 | 44 | .icon { 45 | width: 36px; 46 | height: 36px; 47 | min-width: 36px; 48 | min-height: 36px; 49 | } 50 | 51 | .unavailable { 52 | } 53 | 54 | .disabled { 55 | cursor: not-allowed !important; 56 | } 57 | 58 | .btn { 59 | text-align: center; 60 | cursor: pointer; 61 | transition: 0.4s; 62 | background-color: var(--backdrop-color); 63 | } 64 | 65 | .btn:hover { 66 | transition: 0.0s; 67 | background-color: var(--darker); 68 | } 69 | 70 | a { 71 | color: skyblue; 72 | } 73 | 74 | nav a { 75 | color: inherit; 76 | text-decoration: none; 77 | } 78 | 79 | #motd a { 80 | color: blue; 81 | } 82 | 83 | .list { 84 | padding: 0; 85 | list-style-type: none; 86 | } 87 | 88 | .pt { 89 | cursor: pointer; 90 | } 91 | 92 | /* Markdown Styles */ 93 | h1 { 94 | font-size: 22pt; 95 | margin: 16px 0; 96 | } 97 | 98 | h2 { 99 | font-size: 18pt; 100 | margin: 8px 0; 101 | } 102 | 103 | h3 { 104 | font-size: 14pt; 105 | margin: 8px 0; 106 | } 107 | 108 | /* Global Styles */ 109 | html { 110 | height: 100%; 111 | } 112 | 113 | body { 114 | width: 100%; 115 | font-family: sans-serif; 116 | background-color: var(--backdrop-color); 117 | color: white; 118 | height: 100%; 119 | margin: 0; 120 | } 121 | 122 | #wrapper { 123 | background-color: var(--canvas-color); 124 | display: flex; 125 | flex-direction: column; 126 | height: 100%; 127 | max-width: 1600px; 128 | margin: 0 auto; 129 | } 130 | 131 | /* Top bar styles */ 132 | #top-bar { 133 | flex: 0 1 auto; 134 | } 135 | 136 | nav { 137 | display: flex; 138 | align-items: center; 139 | flex-wrap: wrap; 140 | justify-content: space-between; 141 | border-bottom: 3px solid var(--backdrop-color); 142 | background-color: var(--secondary-color); 143 | } 144 | 145 | #nav-space { 146 | flex-grow: 1; 147 | } 148 | 149 | #logo { 150 | display: flex; 151 | align-items: center; 152 | width: 400px; 153 | } 154 | 155 | #logo h1 { 156 | font-size: 12pt; 157 | margin: 0; 158 | font-family: sans-serif; 159 | font-weight: bold; 160 | } 161 | 162 | #logo span { 163 | font-size: 20pt; 164 | margin: 0; 165 | font-family: sans-serif; 166 | font-weight: bold; 167 | } 168 | 169 | #logo svg { 170 | height: 40px; 171 | width: 40px; 172 | margin: 0 12px; 173 | } 174 | 175 | nav .btn { 176 | font-size: 14pt; 177 | padding: 16px; 178 | min-width: 170px; 179 | background-color: var(--secondary-color); 180 | } 181 | 182 | #nav-discord svg { 183 | margin: 0px 16px; 184 | height: 48px; 185 | fill: var(--discord); 186 | } 187 | 188 | /* Message of the Day */ 189 | header { 190 | padding: 8px; 191 | margin: 0; 192 | display: flex; 193 | align-items: center; 194 | justify-content: space-between; 195 | background-color: var(--accent-color); 196 | color: black; 197 | line-height: 1.5; 198 | } 199 | 200 | #motd-close { 201 | cursor: pointer; 202 | transition: 0.5s; 203 | margin-left: 16px; 204 | margin-right: 8px; 205 | font-size: 28pt; 206 | line-height: 16px; 207 | color: black; 208 | } 209 | 210 | #motd-close:hover { 211 | color: red; 212 | transition: 0s; 213 | } 214 | 215 | /* Universal content styles */ 216 | #login-notice { 217 | position: absolute; 218 | z-index: 1000; 219 | width: 100%; 220 | height: 100%; 221 | background-color: rgba(0, 0, 0, .4); 222 | } 223 | 224 | #content { 225 | display: flex; 226 | flex: 1 1 auto; 227 | overflow: auto; 228 | bottom: 0; 229 | position: relative; 230 | } 231 | 232 | #copyright { 233 | color: lightgray; 234 | font-size: 10pt; 235 | padding-bottom: 16px; 236 | } 237 | 238 | /* Mobile Adjustments */ 239 | @media (max-width: 800px) { 240 | #top-bar { 241 | position: fixed; 242 | width: 100%; 243 | z-index: 102; 244 | } 245 | 246 | #content { 247 | overflow: visible; 248 | padding-top: 74px; 249 | } 250 | 251 | .mhide { 252 | display: none !important; 253 | } 254 | 255 | #mobile-motd { 256 | padding: 4px 16px; 257 | font-size: 11pt; 258 | text-align: center; 259 | background-color: var(--accent-color); 260 | color: black; 261 | } 262 | 263 | 264 | } 265 | 266 | @media (min-width: 801px) { 267 | .mobile { 268 | display: none; 269 | } 270 | } 271 | 272 | @media (max-width: 1200px) { 273 | nav { 274 | padding: 8px; 275 | align-items: stretch !important; 276 | } 277 | 278 | #nav-space { 279 | flex-grow: 0; 280 | } 281 | 282 | #nav-discord { 283 | display: flex; 284 | align-items: center; 285 | justify-content: center; 286 | } 287 | 288 | #nav-discord svg { 289 | margin: 0 4px; 290 | height: 30px; 291 | } 292 | 293 | #logo { 294 | width: auto; 295 | height: 100%; 296 | } 297 | 298 | #logo h1, #logo span { 299 | display: none; 300 | } 301 | 302 | #logo svg { 303 | height: 26px !important; 304 | margin: 0 4px; 305 | } 306 | 307 | nav .btn { 308 | font-size: 12pt !important; 309 | padding: 0; 310 | min-width: auto; 311 | flex-grow: 1; 312 | display: flex; 313 | align-items: center; 314 | justify-content: center; 315 | } 316 | 317 | nav .btn span { 318 | vertical-align: middle; 319 | } 320 | 321 | nav .m-hide { 322 | display: none; 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /console/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: none 3 | --- 4 | 5 | 9 | 10 | 11 | {% include head.html %} 12 | Console - cEDH Decklist Database 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | {% include loading.html %} 23 |
    24 | {% include console-navbar.html %} 25 | 26 |
    27 | 41 |
    42 | 43 |
    44 |
    45 | 46 | 52 |
    53 | 54 |
    55 |
    56 | 57 | 58 |
    59 | 60 | 61 |
    62 |
    63 |
    64 | {% capture content %} 65 | {%- include markdown/admin/console.md -%} 66 | {% endcapture %} 67 | {{ content | escape | markdownify | replace: "id=", "data-content-id=" }} 68 |
    69 |
    70 |
    71 | 72 | 78 |
    79 |
    80 | 81 | 86 |
    87 |
    88 | 89 | 90 |
    91 |
    92 |
      93 |
    94 |
    95 |
    96 | 97 | 178 | 179 |
    180 | 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /console/styles/edit.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 1300px) { 2 | #content { 3 | flex-wrap: wrap; 4 | } 5 | 6 | aside, section { 7 | width: auto !important; 8 | overflow: visible !important; 9 | min-width: 60% !important; 10 | max-width: 100% !important; 11 | } 12 | 13 | section { 14 | margin: 4px !important; 15 | } 16 | } 17 | 18 | #content { 19 | } 20 | 21 | aside { 22 | overflow: auto; 23 | flex-grow: 1; 24 | padding: 16px; 25 | max-width: 650px; 26 | min-width: 650px; 27 | background-color: var(--secondary-color); 28 | } 29 | 30 | section { 31 | overflow: auto; 32 | padding: 16px; 33 | flex-grow: 1; 34 | } 35 | 36 | /* Aside Styles */ 37 | #edit-info { 38 | font-weight: bold; 39 | margin-top: 16px; 40 | font-size: 18pt; 41 | } 42 | 43 | #edit-comments { 44 | margin-top: 16px; 45 | font-size: 14pt; 46 | } 47 | 48 | #prev-controls { 49 | margin-bottom: 16px; 50 | font-size: 16pt; 51 | display: flex; 52 | align-items: center; 53 | } 54 | 55 | #prev-controls label { 56 | margin-left: 8px; 57 | } 58 | 59 | #decks { 60 | list-style-type: none; 61 | margin: 0; 62 | padding: 0; 63 | } 64 | 65 | /* Main Row Styles */ 66 | .GREEN .main, select.GREEN { 67 | background-color: #1d4a1c; 68 | } 69 | 70 | .RED .main, select.RED { 71 | background-color: #551919; 72 | } 73 | 74 | .BLUE .main, select.BLUE { 75 | background-color: #014d73; 76 | } 77 | 78 | .NEUTRAL .main { 79 | background-color: var(--backdrop-color); 80 | } 81 | 82 | select.NEUTRAL { 83 | background-color: var(--canvas-color); 84 | } 85 | 86 | .main { 87 | border-top: 2px solid var(--darker); 88 | padding: 8px 16px; 89 | cursor: pointer; 90 | text-align: left; 91 | } 92 | 93 | .main > div { 94 | align-items: flex-start; 95 | } 96 | 97 | .ddb-colors svg { 98 | height: 28px; 99 | width: 28px; 100 | min-height: 28px; 101 | min-width: 28px; 102 | margin-right: 4px; 103 | } 104 | 105 | .ddb-title { 106 | flex-grow: 1; 107 | font-size: 16pt; 108 | margin-top: 8px; 109 | } 110 | 111 | .main .ddb-status { 112 | font-size: 18pt; 113 | font-family: monospace; 114 | margin-left: 16px; 115 | margin-top: 8px; 116 | overflow-wrap: normal; 117 | } 118 | 119 | .ddb-icons svg { 120 | height: 28px; 121 | width: 28px; 122 | min-height: 28px; 123 | min-width: 28px; 124 | margin-left: 4px; 125 | } 126 | 127 | /* Sub Row Styles */ 128 | .sub { 129 | background-color: var(--lighter); 130 | align-items: stretch; 131 | padding: 12px; 132 | max-height: 300px; 133 | } 134 | 135 | /* Column 1 */ 136 | .ddb-links { 137 | background-color: var(--canvas-color); 138 | min-width: 320px; 139 | max-width: 320px; 140 | margin-right: 16px; 141 | } 142 | 143 | .ddb-links ul { 144 | list-style-type: none; 145 | padding: 0; 146 | } 147 | 148 | .ddb-discord { 149 | font-size: 14pt; 150 | padding: 4px 8px; 151 | display: flex; 152 | align-items: center; 153 | color: white; 154 | text-decoration: none; 155 | text-align: left; 156 | } 157 | 158 | .ddb-discord:hover { 159 | text-decoration: underline; 160 | } 161 | 162 | .ddb-discord.disabled:hover { 163 | text-decoration: none; 164 | background-color: var(--backdrop-color); 165 | } 166 | 167 | .ddb-discord svg { 168 | height: 36px; 169 | width: 36px; 170 | min-height: 36px; 171 | min-width: 36px; 172 | fill: var(--discord); 173 | margin-right: 12px; 174 | } 175 | 176 | .ddb-decklists { 177 | flex-grow: 1; 178 | margin: 0 8px; 179 | overflow: auto; 180 | } 181 | 182 | .ddb-decklists li { 183 | margin-top: 8px; 184 | border-bottom: 2px solid var(--lighter); 185 | } 186 | 187 | .ddb-decklists a { 188 | color: skyblue; 189 | display: flex; 190 | align-items: center; 191 | text-decoration: none; 192 | } 193 | 194 | .ddb-decklists a:hover { 195 | text-decoration: underline; 196 | } 197 | 198 | .ddb-decklists svg { 199 | height: 24px; 200 | width: 24px; 201 | min-height: 24px; 202 | min-width: 24px; 203 | margin-right: 8px; 204 | } 205 | 206 | .ddb-commanders { 207 | padding: 0; 208 | order: -1; 209 | } 210 | 211 | .ddb-commanders li { 212 | border-bottom: 2px solid var(--darker); 213 | text-align: left; 214 | padding: 4px 12px; 215 | } 216 | 217 | .ddb-commanders a { 218 | color: skyblue; 219 | text-decoration: none; 220 | line-height: 1.5; 221 | font-size: 14pt; 222 | } 223 | 224 | /* Column 2 */ 225 | .ddb-info { 226 | flex-grow: 1; 227 | background-color: var(--canvas-color); 228 | } 229 | 230 | .ddb-section { 231 | min-height: 44px; 232 | padding: 0 8px; 233 | text-align: left; 234 | font-size: 16pt; 235 | background-color: var(--backdrop-color); 236 | } 237 | 238 | .ddb-description { 239 | overflow: auto; 240 | padding: 8px; 241 | color: gainsboro; 242 | flex-grow: 1; 243 | } 244 | 245 | .ddb-updates { 246 | font-size: 14pt; 247 | font-weight: bold; 248 | padding: 8px; 249 | } 250 | 251 | /* Section Styles */ 252 | #curator-controls { 253 | background-color: var(--darker); 254 | padding: 16px; 255 | } 256 | 257 | #recommend-wrap div { 258 | background-color: var(--canvas-color); 259 | position: relative; 260 | } 261 | 262 | #recommend-wrap svg { 263 | height: 36px; 264 | padding-left: 8px; 265 | position: absolute; 266 | pointer-events: none; 267 | } 268 | 269 | #in-recommend { 270 | padding-left: 52px; 271 | } 272 | 273 | #comment-wrap label { 274 | margin-top: 16px; 275 | } 276 | 277 | #comments { 278 | flex-grow: 1; 279 | } 280 | 281 | #curator-controls textarea, #in-recommend { 282 | background-color: var(--canvas-color); 283 | } 284 | 285 | #commander-wrap input, #discord-info input { 286 | flex-grow: 1; 287 | } 288 | 289 | #commander-wrap input:last-child, #discord-info input:last-child { 290 | margin-top: 8px; 291 | } 292 | 293 | form { 294 | font-size: 16pt; 295 | } 296 | 297 | form > div { 298 | margin-bottom: 16px; 299 | } 300 | 301 | form label { 302 | margin-bottom: 4px; 303 | } 304 | 305 | form ul { 306 | margin: 0; 307 | margin-top: 8px; 308 | padding: 0; 309 | } 310 | 311 | #list-wrap { 312 | display: flex; 313 | flex-direction: column; 314 | } 315 | 316 | #add-deck { 317 | margin-top: 8px; 318 | padding: 12px; 319 | } 320 | 321 | form li { 322 | list-style-type: none; 323 | margin-bottom: 8px; 324 | } 325 | 326 | .list-title, .list-link { 327 | margin-right: 16px; 328 | } 329 | 330 | form button { 331 | font-size: 16pt; 332 | color: white; 333 | } 334 | 335 | form input::placeholder, form textarea::placeholder { 336 | opacity: .75; 337 | font-style: italic; 338 | color: white; 339 | } 340 | 341 | form input, form textarea, form select, form button { 342 | background-color: var(--darker); 343 | color: white; 344 | border: none; 345 | } 346 | 347 | form li div > input { 348 | flex-grow: 1; 349 | } 350 | 351 | .delete-list { 352 | cursor: pointer; 353 | transition: 0.3s; 354 | font-size: 26pt; 355 | line-height: 16px; 356 | margin-right: 8px; 357 | color: red; 358 | } 359 | 360 | form select { 361 | font-size: 18pt; 362 | padding: 8px 16px; 363 | } 364 | 365 | form [type="text"] { 366 | font-size: 14pt; 367 | padding: 8px; 368 | } 369 | 370 | textarea { 371 | font-size: 12pt; 372 | font-family: sans-serif; 373 | padding: 8px; 374 | } 375 | #color-select { 376 | display: flex; 377 | } 378 | 379 | #color-select div { 380 | cursor: pointer; 381 | opacity: 0.3; 382 | height: 36px; 383 | width: 36px; 384 | margin-left: 4px; 385 | } 386 | 387 | #color-select input + div:hover { 388 | opacity: 0.5; 389 | } 390 | 391 | #color-select input:checked + div { 392 | opacity: 1; 393 | } 394 | 395 | #color-select input:checked + div:hover { 396 | opacity: 1; 397 | } 398 | 399 | #discord-wrap { 400 | margin-bottom: 4px; 401 | } 402 | 403 | .list-entry .flex-column { 404 | flex-grow: 1; 405 | } 406 | 407 | .list-entry > div { 408 | display: flex; 409 | align-items: center; 410 | } 411 | 412 | .list-link { 413 | margin-top: 4px; 414 | } 415 | 416 | #list-wrap ul li:only-child .delete-list { 417 | visibility: hidden; 418 | } 419 | 420 | #submit-wrap { 421 | height: 100%; 422 | } 423 | 424 | #submit-button { 425 | height: 100%; 426 | padding: 8px 24px; 427 | margin-left: 16px; 428 | font-size: 18pt; 429 | background-color: var(--accent-color); 430 | color: black; 431 | } 432 | 433 | #submit-button:hover { 434 | filter: brightness(0.9); 435 | } 436 | -------------------------------------------------------------------------------- /console/styles/console.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 1300px) { 2 | #content { 3 | overflow: auto !important; 4 | flex-wrap: wrap; 5 | } 6 | 7 | aside, section { 8 | width: auto !important; 9 | overflow: visible !important; 10 | min-width: auto !important; 11 | max-width: 100% !important; 12 | } 13 | 14 | section { 15 | order: -1; 16 | } 17 | } 18 | 19 | .template { 20 | display: none !important; 21 | } 22 | 23 | #content { 24 | } 25 | 26 | .dark { 27 | filter: brightness(0%); 28 | } 29 | 30 | .unavailable { 31 | filter: invert(100%) brightness(50%); 32 | } 33 | 34 | /* Request Styles */ 35 | aside { 36 | overflow: auto; 37 | flex-grow: 1; 38 | min-width: 380px; 39 | max-width: 380px; 40 | padding: 16px; 41 | background-color: var(--secondary-color); 42 | } 43 | 44 | #req-controls { 45 | margin-bottom: 16px; 46 | font-size: 16pt; 47 | display: flex; 48 | align-items: center; 49 | } 50 | 51 | #req-controls label { 52 | margin-left: 8px; 53 | } 54 | 55 | aside ul { 56 | list-style-type: none; 57 | padding: 0; 58 | } 59 | 60 | aside li { 61 | background-color: var(--canvas-color); 62 | padding: 8px; 63 | margin-bottom: 16px; 64 | } 65 | 66 | .req-wrap { 67 | align-items: flex-start; 68 | } 69 | 70 | aside button { 71 | background-color: firebrick; 72 | border: none; 73 | width: 32px; 74 | height: 32px; 75 | color: white; 76 | font-weight: bold; 77 | font-size: 16pt; 78 | cursor: pointer; 79 | } 80 | 81 | aside button:hover { 82 | background-color: red; 83 | } 84 | 85 | .req-category { 86 | font-size: 13pt; 87 | font-weight: bold; 88 | } 89 | 90 | .req-description { 91 | margin: 8px 0; 92 | color: gainsboro; 93 | font-size: 12pt; 94 | } 95 | 96 | .req-date { 97 | text-align: right; 98 | font-size: 12pt; 99 | font-family: monospace; 100 | } 101 | 102 | .req-username { 103 | font-weight: bold; 104 | padding-top: 4px; 105 | } 106 | 107 | /* Section Styles */ 108 | section { 109 | overflow: auto; 110 | padding: 16px 16px; 111 | flex-grow: 1; 112 | } 113 | 114 | #console-controls, #database-controls { 115 | align-items: stretch; 116 | } 117 | 118 | #console-controls select, #database-controls select, #db-search { 119 | background-color: var(--darker); 120 | color: white; 121 | border: none; 122 | padding: 8px; 123 | font-size: 14pt; 124 | } 125 | 126 | #console-controls label, #database-controls label { 127 | font-size: 14pt; 128 | margin-bottom: 4px; 129 | } 130 | 131 | #console-controls button { 132 | color: black; 133 | cursor: pointer; 134 | border: none; 135 | font-size: 14pt; 136 | } 137 | 138 | #console-controls button:not(:disabled):hover { 139 | filter: brightness(80%); 140 | } 141 | 142 | #console-controls button:disabled { 143 | filter: grayscale(50%) brightness(60%); 144 | cursor: not-allowed; 145 | } 146 | 147 | #select-wrap { 148 | margin-right: 8px; 149 | height: 100%; 150 | } 151 | 152 | #select-wrap button { 153 | background-color: #8dc18b; 154 | } 155 | 156 | #publish-wrap { 157 | display: flex; 158 | align-items: center; 159 | justify-content: stretch; 160 | } 161 | 162 | #publish-changes { 163 | background-color: var(--accent-color); 164 | height: 100%; 165 | padding: 16px; 166 | } 167 | 168 | #search-wrap { 169 | flex-grow: 1; 170 | } 171 | 172 | #db-section, #db-sort { 173 | margin-right: 16px; 174 | } 175 | 176 | #decks { 177 | margin-bottom: 200px; 178 | } 179 | 180 | #decks, #decks ul { 181 | list-style-type: none; 182 | padding: 0; 183 | } 184 | 185 | .GREEN .main, .GREEN .ddb-edit { 186 | background-color: #1d4a1c; 187 | } 188 | 189 | .RED .main, .RED .ddb-edit { 190 | background-color: #551919; 191 | } 192 | 193 | .BLUE .main, .BLUE .ddb-edit { 194 | background-color: #014d73; 195 | } 196 | 197 | .PURPLE .main, .PURPLE .ddb-edit { 198 | background-color: #551357; 199 | } 200 | 201 | /* Main Row Styles */ 202 | .main { 203 | border-top: 2px solid var(--darker); 204 | padding: 8px 16px; 205 | cursor: pointer; 206 | text-align: left; 207 | } 208 | 209 | .ddb-publish { 210 | margin-right: 16px; 211 | display: flex; 212 | align-items: center; 213 | justify-content: center; 214 | cursor: pointer; 215 | } 216 | 217 | .ddb-publish input { 218 | display: none; 219 | } 220 | 221 | .ddb-publish .checkbox { 222 | width: 26px; 223 | height: 26px; 224 | background-color: white; 225 | } 226 | 227 | .ddb-publish:hover .checkbox { 228 | background-color: #ccc; 229 | width: 18px; 230 | height: 18px; 231 | border: 4px solid white; 232 | } 233 | 234 | .ddb-publish input:checked + .checkbox { 235 | background-color: #15a5ec; 236 | width: 18px; 237 | height: 18px; 238 | border: 4px solid white; 239 | } 240 | 241 | .ddb-colors svg { 242 | height: 28px; 243 | width: 28px; 244 | min-height: 28px; 245 | min-width: 28px; 246 | margin-right: 4px; 247 | } 248 | 249 | .ddb-title { 250 | margin-left: 64px; 251 | flex-grow: 1; 252 | font-size: 16pt; 253 | } 254 | 255 | .ddb-main-section { 256 | font-size: 14pt; 257 | margin: 0 16px; 258 | font-family: monospace; 259 | font-weight: bold; 260 | overflow-wrap: normal; 261 | } 262 | 263 | .ddb-main-date { 264 | margin-right: 4px; 265 | } 266 | 267 | .ddb-icons svg { 268 | height: 28px; 269 | width: 28px; 270 | min-height: 28px; 271 | min-width: 28px; 272 | margin-left: 4px; 273 | } 274 | 275 | /* Sub Row Styles */ 276 | .sub { 277 | background-color: var(--lighter); 278 | align-items: stretch; 279 | padding: 12px; 280 | max-height: 300px; 281 | } 282 | 283 | /* Column 1 */ 284 | .ddb-controls { 285 | max-width: 300px; 286 | min-width: 300px; 287 | margin-right: 16px; 288 | } 289 | 290 | .ddb-edit { 291 | color: inherit; 292 | text-decoration: none; 293 | font-size: 20pt; 294 | display: flex; 295 | align-items: center; 296 | padding: 4px 8px; 297 | } 298 | 299 | .ddb-edit svg { 300 | width: 32px; 301 | height: 32px; 302 | min-width: 32px; 303 | min-height: 32px; 304 | padding: 2px 0; 305 | margin-right: 12px; 306 | } 307 | 308 | .ddb-comments { 309 | flex-grow: 1; 310 | padding: 8px; 311 | background-color: var(--canvas-color); 312 | overflow: auto; 313 | color: gainsboro; 314 | } 315 | 316 | .ddb-username { 317 | background-color: var(--canvas-color); 318 | padding: 8px; 319 | font-weight: bold; 320 | } 321 | 322 | /* Column 2 */ 323 | .ddb-links { 324 | background-color: var(--canvas-color); 325 | min-width: 320px; 326 | max-width: 320px; 327 | margin-right: 16px; 328 | } 329 | 330 | .ddb-discord { 331 | font-size: 14pt; 332 | padding: 4px 8px; 333 | display: flex; 334 | align-items: center; 335 | color: white; 336 | text-decoration: none; 337 | text-align: left; 338 | } 339 | 340 | .ddb-discord:hover { 341 | text-decoration: underline; 342 | } 343 | 344 | .ddb-discord.disabled:hover { 345 | text-decoration: none; 346 | background-color: var(--backdrop-color); 347 | } 348 | 349 | .ddb-discord svg { 350 | height: 36px; 351 | width: 36px; 352 | min-height: 36px; 353 | min-width: 36px; 354 | fill: var(--discord); 355 | margin-right: 12px; 356 | } 357 | 358 | .ddb-decklists { 359 | flex-grow: 1; 360 | margin: 0 8px; 361 | overflow: auto; 362 | } 363 | 364 | .ddb-decklists li { 365 | margin-top: 8px; 366 | border-bottom: 2px solid var(--lighter); 367 | } 368 | 369 | .ddb-decklists a { 370 | color: skyblue; 371 | display: flex; 372 | align-items: center; 373 | text-decoration: none; 374 | } 375 | 376 | .ddb-decklists a:hover { 377 | text-decoration: underline; 378 | } 379 | 380 | .ddb-decklists svg { 381 | height: 24px; 382 | width: 24px; 383 | min-height: 24px; 384 | min-width: 24px; 385 | margin-right: 8px; 386 | } 387 | 388 | .ddb-commanders { 389 | padding: 0; 390 | order: -1; 391 | } 392 | 393 | .ddb-commanders li { 394 | border-bottom: 2px solid var(--darker); 395 | text-align: left; 396 | padding: 4px 12px; 397 | } 398 | 399 | .ddb-commanders a { 400 | color: skyblue; 401 | text-decoration: none; 402 | line-height: 1.5; 403 | font-size: 14pt; 404 | } 405 | 406 | 407 | /* Column 3 */ 408 | .ddb-info { 409 | flex-grow: 1; 410 | background-color: var(--canvas-color); 411 | } 412 | 413 | .ddb-section { 414 | min-height: 44px; 415 | padding: 0 8px; 416 | text-align: left; 417 | font-size: 16pt; 418 | background-color: var(--backdrop-color); 419 | } 420 | 421 | .ddb-description { 422 | overflow: auto; 423 | padding: 8px; 424 | color: gainsboro; 425 | flex-grow: 1; 426 | } 427 | 428 | .ddb-updates { 429 | font-size: 14pt; 430 | font-weight: bold; 431 | padding: 8px; 432 | } 433 | 434 | .ddb-date { 435 | text-align: right; 436 | flex-grow: 1; 437 | } 438 | -------------------------------------------------------------------------------- /console/edit.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: none 3 | --- 4 | 5 | 9 | 10 | 11 | {% include head.html %} 12 | Edit Deck - cEDH Decklist Database 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | {% include loading.html %} 23 |
    24 | {% include console-navbar.html %} 25 | 26 | 50 | 51 |
    52 | 147 |
    148 |
    149 |
    150 |
    151 | 152 |
    153 | 154 | 159 |
    160 |
    161 | 162 |
    163 | 164 |
    165 | 166 |
    167 | 168 |
    169 |
    170 |
    171 |
    172 | 173 |
    174 | 175 |
    176 | 181 |
    182 |
    183 | 184 |
    185 |
    186 | 187 | 188 |
    189 |
    190 | 191 |
    192 |
    193 | 194 |
    195 | 196 |
    198 |
    199 |
    200 | 201 | 202 |
    203 |
    204 | 205 |
    206 | 207 | 208 |
    209 | 210 |
    211 |
    212 | 213 | 214 |
    215 | 219 |
    220 | 221 |
    222 |
      223 |
    • 224 |
      225 | × 226 |
      227 | 228 | 229 |
      230 | 231 | 232 |
      233 | 237 |
      238 | 239 |
      240 |
      241 |
      242 |
    • 243 |
    244 | 245 |
    246 |
    247 |
    248 |
    249 |
    250 | 251 | 252 | 253 | 254 | 255 | -------------------------------------------------------------------------------- /console/scripts/edit.js: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | (function() { "use strict"; 4 | window.addEventListener("load", init); 5 | 6 | // Initialization function 7 | async function init() { 8 | if (get("jwt")) { 9 | prepareListeners(); 10 | await getDeck(); 11 | } 12 | } 13 | 14 | function prepareListeners() { 15 | id("two-commanders").addEventListener("change", togglePartner); 16 | id("has-discord").addEventListener("change", toggleDiscord); 17 | qs(".delete-list").addEventListener("click", deleteDecklist); 18 | id("add-deck").addEventListener("click", addDecklist); 19 | id("preview-toggle").addEventListener("change", togglePreview); 20 | qs("form").addEventListener("submit", submitForm); 21 | qs("form").addEventListener("change", updatePreview); 22 | qs("form").addEventListener("keyup", updatePreview); 23 | id("in-destination").addEventListener("change", swapBackground); 24 | } 25 | 26 | function swapBackground() { 27 | const item = id("in-destination"); 28 | const dest = item.value; 29 | const status = qs("form").dataset.status; 30 | item.classList.remove("RED", "BLUE", "GREEN", "PURPLE", "NEUTRAL"); 31 | if (dest === "PUBLISHED" && status === "PUBLISHED") { 32 | item.classList.add("BLUE"); 33 | } else if (dest === "PUBLISHED") { 34 | item.classList.add("GREEN"); 35 | } else if (dest === "DELETED") { 36 | item.classList.add("RED"); 37 | } else if (dest === "SUBMITTED") { 38 | item.classList.add("PURPLE"); 39 | } else { 40 | item.classList.add("NEUTRAL"); 41 | } 42 | } 43 | 44 | // Activates or deactivates the partner text box 45 | function togglePartner() { 46 | if (id("two-commanders").checked) { 47 | id("commander2").classList.remove("hidden"); 48 | id("commander2").required = true; 49 | } else { 50 | id("commander2").classList.add("hidden"); 51 | id("commander2").required = false; 52 | } 53 | } 54 | 55 | // Activates or deactivates the Discord server text boxes 56 | function toggleDiscord() { 57 | if (id("has-discord").checked) { 58 | id("discord-info").classList.remove("hidden"); 59 | id("discord-title").required = true; 60 | id("discord-link").required = true; 61 | } else { 62 | id("discord-info").classList.add("hidden"); 63 | id("discord-title").required = false; 64 | id("discord-link").required = false; 65 | } 66 | } 67 | 68 | // Removes a decklist entry option from the form 69 | function deleteDecklist() { 70 | if (this.parentElement.parentElement.parentElement.childElementCount > 1) { 71 | this.parentElement.parentElement.remove(); 72 | } 73 | } 74 | 75 | async function updatePreview() { 76 | build.preview(scrape.form()); 77 | } 78 | 79 | // Adds a new decklist entry to the list of decks 80 | function addDecklist() { 81 | let newList = qs("#list-wrap ul li:first-child").cloneNode(true); 82 | newList.querySelector(".list-title").value = ""; 83 | newList.querySelector(".list-link").value = ""; 84 | newList.querySelector(".has-primer").checked = false; 85 | newList.querySelector(".delete-list").addEventListener("click", deleteDecklist); 86 | qs("#list-wrap ul").appendChild(newList); 87 | } 88 | 89 | function togglePreview() { 90 | if (id("preview-toggle").checked) { 91 | id("old-deck").classList.remove("hidden"); 92 | id("preview-deck").classList.add("hidden"); 93 | id("deck-label").innerText = "Original Listing"; 94 | } else { 95 | id("preview-deck").classList.remove("hidden"); 96 | id("old-deck").classList.add("hidden"); 97 | id("deck-label").innerText = "Preview"; 98 | } 99 | } 100 | 101 | // Gets the list of decks from the API 102 | async function getDeck() { 103 | const jwt = get("jwt"); 104 | const id = new URLSearchParams(window.location.search).get("id"); 105 | const body = { 106 | "jwt": jwt, 107 | "method": "GET_DECK", 108 | "id": id 109 | }; 110 | const result = await sendToDDB(body); 111 | if (result.success) { 112 | const deck = result.data; 113 | qs("form").dataset.timestamp = deck.updated; 114 | qs("form").dataset.status = deck.status; 115 | build.old(deck); 116 | build.preview(deck); 117 | fill.form(deck); 118 | } else { 119 | console.error(result.message); 120 | if (result.data) { 121 | console.error(result.data); 122 | } 123 | alert(" There was an error while fetching decks:\n" + result.message); 124 | } 125 | } 126 | 127 | async function submitForm() { 128 | event.preventDefault(); 129 | let scry = await getCommanderInfo(); 130 | if (!scry.success) { 131 | alert(scry.message); 132 | } else if (!confirm("Are you sure you want to submit these changes?")) { 133 | 134 | } else if (!get("jwt")) { 135 | alert("You are logged out. You must be logged in to submit the form."); 136 | } else { 137 | let body = scrape.body(scry); 138 | let result = await sendToDDB(body); 139 | if (result.success) { 140 | alert(result.message); 141 | window.location.replace("/console/"); 142 | } else { 143 | console.log(result); 144 | if (result.status = 409) { 145 | qs("form").dataset.timestamp = result.data.updated; 146 | build.old(result.data); 147 | id("preview-toggle").checked = true; 148 | togglePreview(); 149 | id("deck-label").innerText = result.data.editor + "'s Version"; 150 | alert(result.message); 151 | } else { 152 | if (result.data) { 153 | console.error(result.data); 154 | } 155 | console.error(result.message); 156 | alert(" There was an error:\n" + result.message); 157 | } 158 | } 159 | } 160 | } 161 | 162 | const scrape = { 163 | form: function(scry = null) { 164 | const data = {}; 165 | data.comments = id("comments").value; 166 | data.status = id("old-deck").dataset.status; 167 | data.destination = id("in-destination").value; 168 | data.editor = get("username"); 169 | data.updated = new Date().toISOString(); 170 | data.id = id("old-deck").dataset.id; 171 | 172 | data.section = id("in-section").value; 173 | data.title = id("deck-title").value; 174 | data.description = id("description").value; 175 | data.discord = scrape.discord(); 176 | data.decklists = scrape.decklists(); 177 | if (scry) { 178 | data.commander = scry.commanders; 179 | data.colors = scry.colors; 180 | } else { 181 | data.commander = scrape.commanders(); 182 | data.colors = scrape.colors(); 183 | } 184 | 185 | return data; 186 | }, 187 | 188 | body: function(scry) { 189 | const body = {}; 190 | body.data = scrape.form(scry); 191 | body.jwt = get("jwt"); 192 | body.method = "UPDATE_DECK"; 193 | body.timestamp = qs("form").dataset.timestamp; 194 | return body; 195 | }, 196 | 197 | decklists: function() { 198 | const decklists = []; 199 | let lists = qsa(".list-entry"); 200 | for (let i = 0; i < lists.length; i++) { 201 | let list = lists[i]; 202 | let d = {}; 203 | d.primer = list.querySelector(".has-primer").checked; 204 | d.title = list.querySelector(".list-title").value; 205 | d.link = list.querySelector(".list-link").value; 206 | decklists.push(d); 207 | } 208 | return decklists; 209 | }, 210 | 211 | discord: function() { 212 | if (id("has-discord").checked) { 213 | const discord = {}; 214 | discord.title = id("discord-title").value; 215 | discord.link = id("discord-link").value; 216 | return discord; 217 | } else { 218 | return null; 219 | } 220 | }, 221 | 222 | commanders: function() { 223 | const commanders = []; 224 | commanders.push({ name: id("commander").value, link: "example.com" }); 225 | if (id("two-commanders").checked) { 226 | commanders.push({ name: id("commander2").value, link: "example2.com" }); 227 | } 228 | return commanders; 229 | }, 230 | 231 | colors: function() { 232 | return id("preview-deck").dataset.colors; 233 | } 234 | } 235 | 236 | // Helper object which fills out the form 237 | const fill = { 238 | form: function(deck) { 239 | id("deck-title").value = deck.title; 240 | id("description").value = deck.description; 241 | fill.section(deck); 242 | fill.edit(deck); 243 | fill.commander(deck); 244 | fill.discord(deck); 245 | fill.decklists(deck); 246 | swapBackground(); 247 | }, 248 | 249 | section: function(deck) { 250 | id("in-section").value = deck.section; 251 | if (!deck.destination) { 252 | id("in-destination").value = deck.status; 253 | } else { 254 | id("in-destination").value = deck.destination; 255 | } 256 | }, 257 | 258 | edit: function(deck) { 259 | let edit = " on " + deck.updated.substring(0, 10); 260 | edit = deck.editor ? "" + deck.editor + " Edited" + edit : "Submitted" + edit; 261 | id("edit-info").innerHTML = edit; 262 | 263 | id("comments").value = deck.comments; 264 | id("edit-comments").innerText = deck.comments; 265 | }, 266 | 267 | commander: function(deck) { 268 | id("commander").value = deck.commander[0].name; 269 | if (deck.commander.length > 1) { 270 | id("two-commanders").checked = true; 271 | togglePartner(); 272 | id("commander2").value = deck.commander[1].name; 273 | } 274 | }, 275 | 276 | discord: function(deck) { 277 | if (deck.discord) { 278 | id("has-discord").checked = true; 279 | toggleDiscord(); 280 | id("discord-title").value = deck.discord.title; 281 | id("discord-link").value = deck.discord.link; 282 | } 283 | }, 284 | 285 | decklists: function(deck) { 286 | for (let i = 0; i < deck.decklists.length; i++) { 287 | const decklist = deck.decklists[i]; 288 | if (i > 0) { 289 | addDecklist(); 290 | } 291 | const item = qs(".list-entry:nth-child(" + (i + 1) + ")"); 292 | iqs(item, ".list-title").value = decklist.title; 293 | iqs(item, ".list-link").value = decklist.link; 294 | iqs(item, ".has-primer").checked = decklist.primer; 295 | } 296 | } 297 | } 298 | 299 | // Helper object which builds a deck entry 300 | const build = { 301 | deck: function(item, deck) { 302 | let id = deck.id; 303 | item.dataset.id = id; 304 | item.dataset.status = deck.status; 305 | if (deck.destination) { 306 | item.dataset.destination = deck.destination; 307 | } 308 | iqs(item, ".main").id = "m" + id; 309 | iqs(item, ".sub").id = "s" + id; 310 | iqs(item, ".ddb-title").innerText = deck.title; 311 | iqs(item, ".ddb-section").innerText = deck.section; 312 | iqs(item, ".ddb-description").innerText = deck.description; 313 | iqs(item, ".ddb-date").innerText = deck.updated.substring(0, 10); 314 | build.status(item, deck); 315 | build.colors(item, deck); 316 | build.icons(item, deck); 317 | build.discord(item, deck); 318 | build.decklists(item, deck); 319 | build.commanders(item, deck); 320 | }, 321 | 322 | old: function(deck) { 323 | const item = id("old-deck"); 324 | build.deck(item, deck); 325 | }, 326 | 327 | preview: function(deck) { 328 | const item = id("preview-deck"); 329 | build.deck(item, deck); 330 | }, 331 | 332 | status: function(item, deck) { 333 | item.classList.remove("RED", "BLUE", "GREEN", "PURPLE"); 334 | if (deck.destination === "PUBLISHED" && deck.status === "PUBLISHED") { 335 | item.classList.add("BLUE"); 336 | } else if (deck.destination === "PUBLISHED") { 337 | item.classList.add("GREEN"); 338 | } else if (deck.destination === "DELETED") { 339 | item.classList.add("RED"); 340 | } else if (deck.destination === "SUBMITTED") { 341 | item.classList.add("PURPLE"); 342 | } 343 | 344 | item.dataset.status = deck.status; 345 | item.dataset.destination = deck.destination; 346 | let val = deck.destination ? deck.destination : deck.status; 347 | const statuses = iqsa(item, ".ddb-status"); 348 | for (let j = 0; j < statuses.length; j++) { 349 | statuses[j].innerText = val; 350 | } 351 | }, 352 | 353 | colors: function(item, deck) { 354 | const colors = ["w", "u", "b", "r", "g"]; 355 | const ddbColors = iqs(item, ".ddb-colors"); 356 | ddbColors.innerHTML = ""; 357 | item.dataset.colors = deck.colors; 358 | for (let j = 0; j < colors.length; j++) { 359 | let symbol; 360 | if (deck.colors.includes(colors[j])) { 361 | symbol = qs("#template-" + colors[j] + " svg").cloneNode(true); 362 | } else { 363 | symbol = qs("#template-x svg").cloneNode(true); 364 | } 365 | ddbColors.appendChild(symbol); 366 | } 367 | }, 368 | 369 | icons: function(item, deck) { 370 | if (deck.decklists.every(d => d.primer === false)) { 371 | iqs(item, ".ddb-icons .primer-svg").classList.add("unavailable"); 372 | } else { 373 | iqs(item, ".ddb-icons .primer-svg").classList.remove("unavailable"); 374 | } 375 | if (!deck.discord) { 376 | iqs(item, ".ddb-icons .discord-svg").classList.add("unavailable"); 377 | } else { 378 | iqs(item, ".ddb-icons .discord-svg").classList.remove("unavailable"); 379 | } 380 | }, 381 | 382 | discord: function(item, deck) { 383 | if (deck.discord) { 384 | iqs(item, ".ddb-discord").href = deck.discord.link; 385 | iqs(item, ".ddb-discord-title").innerText = deck.discord.title; 386 | iqs(item, ".ddb-discord").classList.remove("disabled"); 387 | iqs(item, ".ddb-discord svg").classList.remove("dark"); 388 | } else { 389 | iqs(item, ".ddb-discord").classList.add("disabled"); 390 | iqs(item, ".ddb-discord-title").innerText = "[No Discord Server]"; 391 | iqs(item, ".ddb-discord svg").classList.add("dark"); 392 | } 393 | }, 394 | 395 | decklists: function(item, deck) { 396 | const decklists = iqs(item, ".ddb-decklists"); 397 | decklists.innerHTML = ""; 398 | for (let i = 0; i < deck.decklists.length; i++) { 399 | const decklist = deck.decklists[i]; 400 | const li = id("decklist-template").cloneNode(true); 401 | li.id = ""; 402 | if (!decklist.primer) { 403 | iqs(li, ".primer-svg").classList.add("unavailable"); 404 | } 405 | iqs(li, ".ddb-decklist-title").innerText = decklist.title; 406 | iqs(li, "a").href = decklist.link; 407 | decklists.appendChild(li); 408 | } 409 | }, 410 | 411 | commanders: function(item, deck) { 412 | const commanders = iqs(item, ".ddb-commanders"); 413 | commanders.innerHTML = ""; 414 | for (let i = 0; i < deck.commander.length; i++) { 415 | const commander = deck.commander[i]; 416 | const a = document.createElement("a"); 417 | a.innerText = commander.name; 418 | a.href = commander.link; 419 | 420 | const li = document.createElement("li"); 421 | li.classList.add("btn"); 422 | li.appendChild(a); 423 | commanders.appendChild(li); 424 | } 425 | } 426 | } 427 | 428 | /* HELPER FUNCTIONS */ 429 | {% include javascript/dom.js %} 430 | {% include javascript/backend.js %} 431 | {% include javascript/scryfall.js %} 432 | 433 | })(); 434 | -------------------------------------------------------------------------------- /console/scripts/console.js: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | (function() {"use strict"; 4 | window.addEventListener("load", init); 5 | 6 | // MTG Color ordering converted to binary 7 | const COLOR_ORDER = [ 8 | 0, 9 | 1,2,4,8,16, 10 | 3,6,12,24,17, 11 | 5,10,20,9,18, 12 | 19,7,14,28,25, 13 | 13,26,21,11,22, 14 | 15,30,29,27,23, 15 | 31 16 | ] 17 | 18 | // Initialization function 19 | async function init() { 20 | if (get("jwt")) { 21 | const deckRead = readDecks().then(() => { 22 | filterDecks(); 23 | id("select-all").addEventListener("click", selectAll); 24 | id("unselect-all").addEventListener("click", unselectAll); 25 | showLoad(); 26 | }); 27 | const reqRead = readRequests().then(() => { 28 | id("show-deleted").addEventListener("change", toggleRequests); 29 | showLoad(); 30 | }); 31 | await Promise.all([deckRead, reqRead]); 32 | hideLoad(); 33 | id("view-select").addEventListener("change", filterDecks); 34 | id("db-section").addEventListener("change", filterDecks); 35 | id("db-search").addEventListener("change", filterDecks); 36 | id("db-sort").addEventListener("change", sortTable); 37 | id("publish-changes").addEventListener("click", publishChanges); 38 | } 39 | } 40 | 41 | function selectAll() { 42 | qsa("#decks .ddb-publish input").forEach(item => { 43 | item.checked = true; 44 | }); 45 | markForPublish(); 46 | } 47 | 48 | function unselectAll() { 49 | qsa("#decks .ddb-publish input").forEach(item => { 50 | item.checked = false; 51 | }); 52 | markForPublish(); 53 | } 54 | 55 | function changeSection() { 56 | const section = id("db-section").value; 57 | qsa("#decks > li").forEach(deck => { 58 | if (section !== iqs(deck, ".ddb-section").innerText.trim()) { 59 | deck.classList.add("hidden") 60 | } 61 | }); 62 | } 63 | 64 | // Sends the request to the API that initiates a Github commit 65 | async function publishChanges() { 66 | if (!get("jwt")) { 67 | alert("You are not logged in. You must be logged in to do this."); 68 | } else if (confirm("Are you sure you want to publish all changes? This cannot be reversed, and it will take several minutes. If you haven't already, you should confirm this decision with the other curators.")) { 69 | if (!get("jwt")) { 70 | alert("You are not logged in. You must be logged in to do this."); 71 | } else { 72 | const jwt = get("jwt"); 73 | const changes = []; 74 | qsa("#decks .ddb-publish input:checked").forEach(item => { 75 | const mainId = item.parentElement.parentElement.id; 76 | changes.push(mainId.substring(1, mainId.length)); 77 | }); 78 | const body = { 79 | "jwt": jwt, 80 | "method": "PUBLISH_CHANGES", 81 | "changes": JSON.stringify(changes) 82 | }; 83 | const result = await sendToDDB(body); 84 | if (result.success) { 85 | console.log(result.data); 86 | alert("Success! The changes should be visible on the website in a few minutes."); 87 | window.location.reload(); 88 | } else { 89 | console.error(result.message); 90 | if (result.data) { 91 | console.error(result.data); 92 | } 93 | alert(" There was an error while publishing changes:\n" + result.message); 94 | } 95 | } 96 | } 97 | } 98 | 99 | // Gets the list of decks from the API 100 | async function readDecks() { 101 | const jwt = get("jwt"); 102 | const body = { 103 | "jwt": jwt, 104 | "method": "READ_DECKS" 105 | }; 106 | const result = await sendToDDB(body); 107 | if (result.success) { 108 | const decks = JSON.parse(result.data); 109 | populateDecks(decks); 110 | console.log(decks); 111 | } else { 112 | console.error(result.message); 113 | if (result.data) { 114 | console.error(result.data); 115 | } 116 | alert(" There was an error while fetching decks:\n" + result.message); 117 | } 118 | } 119 | 120 | // Takes an array of decks and transforms it into HTML 121 | function populateDecks(decks) { 122 | const sortedDecks = decks.sort((a, b) => { return b.updated.localeCompare(a.updated); }); 123 | for (let i = 0; i < sortedDecks.length; i++) { 124 | const deck = sortedDecks[i]; 125 | const item = id("deck-template").cloneNode(true); 126 | build.deck(item, deck); 127 | id("decks").appendChild(item); 128 | } 129 | } 130 | 131 | function markForPublish(event = null) { 132 | if (event) { 133 | event.stopPropagation(); 134 | } 135 | id("publish-changes").disabled = qsa(".ddb-publish input:checked").length < 1 ? true : false; 136 | id("unselect-all").disabled = qsa(".ddb-publish input:checked").length < 1 ? true : false; 137 | } 138 | 139 | function sortTable() { 140 | const sort = id("db-sort").value; 141 | const entries = [...qsa("#decks > li")]; 142 | 143 | const sorted = entries.sort((a, b) => { 144 | if (sort === "NEW") { return b.dataset.updated.localeCompare(a.dataset.updated); } 145 | if (sort === "TITLE") { return a.dataset.title.localeCompare(b.dataset.title); } 146 | if (sort === "COLOR") { return convertColor(a.dataset.colors) - convertColor(b.dataset.colors); } 147 | return 0; 148 | }); 149 | 150 | const decks = id("decks"); 151 | sorted.forEach(item => decks.appendChild(item)); 152 | } 153 | 154 | function convertColor(colors) { 155 | let val = 0; 156 | if (colors.includes("w")) { val += 1 } 157 | if (colors.includes("u")) { val += 2 } 158 | if (colors.includes("b")) { val += 4 } 159 | if (colors.includes("r")) { val += 8 } 160 | if (colors.includes("g")) { val += 16 } 161 | return COLOR_ORDER.indexOf(val); 162 | } 163 | 164 | function filterDecks() { 165 | const decks = qsa("#decks > li"); 166 | const section = id("db-section").value; 167 | const search = id("db-search").value.toLowerCase(); 168 | 169 | qsa("#decks > li").forEach(deck => { 170 | let hide = false; 171 | if (!checkView(deck)) { 172 | hide = true; 173 | } else if (section !== "ALL" && section !== iqs(deck, ".ddb-section").innerText.trim()) { 174 | hide = true; 175 | } else if (search && !deck.textContent.toLowerCase().includes(search)) { 176 | hide = true; 177 | } 178 | 179 | hide ? deck.classList.add("hidden") : deck.classList.remove("hidden"); 180 | }); 181 | } 182 | 183 | function checkView(deck) { 184 | const view = id("view-select").value; 185 | const status = deck.dataset.status; 186 | const dest = deck.dataset.destination; 187 | switch (view) { 188 | case "SUBMITTED": 189 | return (status === "SUBMITTED" || dest === "SUBMITTED"); 190 | case "PUBLISHED": 191 | return (status === "PUBLISHED"); 192 | case "DELETED": 193 | return (status === "DELETED" || dest === "DELETED"); 194 | case "CHANGES": 195 | return (dest !== "null"); 196 | default: 197 | return true; 198 | } 199 | } 200 | 201 | const build = { 202 | deck: function(item, deck) { 203 | let id = deck.id; 204 | item.id = "d" + id; 205 | item.classList.add(deck.status); 206 | iqs(item, ".main").id = "m" + id; 207 | iqs(item, ".sub").id = "s" + id; 208 | iqs(item, ".main").addEventListener("click", toggleSub); 209 | iqs(item, ".ddb-title").innerText = deck.title; 210 | item.dataset.title = deck.title; 211 | iqs(item, ".ddb-edit").href = "/console/edit.html?id=" + id; 212 | iqs(item, ".ddb-section").innerText = deck.section; 213 | iqs(item, ".ddb-main-section").innerText = deck.section; 214 | iqs(item, ".ddb-description").innerText = deck.description; 215 | iqs(item, ".ddb-date").innerText = deck.updated.substring(0, 10); 216 | iqs(item, ".ddb-main-date").innerText = deck.updated.substring(0, 10); 217 | item.dataset.updated = deck.updated; 218 | build.status(item, deck); 219 | build.colors(item, deck); 220 | build.icons(item, deck); 221 | build.comments(item, deck); 222 | build.discord(item, deck); 223 | build.decklists(item, deck); 224 | build.commanders(item, deck); 225 | build.editor(item, deck); 226 | }, 227 | 228 | status: function(item, deck) { 229 | item.classList.remove("RED", "BLUE", "GREEN", "PURPLE"); 230 | iqs(item, ".ddb-publish").addEventListener("change", function(event) { 231 | markForPublish(event); 232 | }); 233 | if (deck.destination === "PUBLISHED" && deck.status === "PUBLISHED") { 234 | item.classList.add("BLUE"); 235 | } else if (deck.destination === "PUBLISHED") { 236 | item.classList.add("GREEN"); 237 | } else if (deck.destination === "DELETED") { 238 | item.classList.add("RED"); 239 | } else if (deck.destination === "SUBMITTED") { 240 | item.classList.add("PURPLE"); 241 | } else { 242 | const input = iqs(item, ".ddb-publish"); 243 | input.parentNode.removeChild(input); 244 | } 245 | 246 | item.dataset.status = deck.status; 247 | item.dataset.destination = deck.destination; 248 | let val = deck.destination ? deck.destination : deck.status; 249 | const statuses = iqsa(item, ".ddb-status"); 250 | for (let j = 0; j < statuses.length; j++) { 251 | statuses[j].innerText = val; 252 | } 253 | }, 254 | 255 | colors: function(item, deck) { 256 | const colors = ["w", "u", "b", "r", "g"]; 257 | item.dataset.colors = JSON.stringify(deck.colors); 258 | const ddbColors = iqs(item, ".ddb-colors"); 259 | for (let j = 0; j < colors.length; j++) { 260 | let symbol; 261 | if (deck.colors.includes(colors[j])) { 262 | symbol = qs("#template-" + colors[j] + " svg").cloneNode(true); 263 | } else { 264 | symbol = qs("#template-x svg").cloneNode(true); 265 | } 266 | ddbColors.appendChild(symbol); 267 | } 268 | }, 269 | 270 | icons: function(item, deck) { 271 | if (deck.decklists.every(d => d.primer === false)) { 272 | iqs(item, ".ddb-icons .primer-svg").classList.add("unavailable"); 273 | } 274 | if (!deck.discord) { 275 | iqs(item, ".ddb-icons .discord-svg").classList.add("unavailable"); 276 | } 277 | }, 278 | 279 | comments: function(item, deck) { 280 | if (deck.comments !== "") { 281 | iqs(item, ".ddb-comments").innerText = deck.comments; 282 | } else { 283 | iqs(item, ".ddb-comments").innerText = "This deck does not have any Curator comments."; 284 | } 285 | }, 286 | 287 | discord: function(item, deck) { 288 | if (deck.discord) { 289 | iqs(item, ".ddb-discord").href = deck.discord.link; 290 | iqs(item, ".ddb-discord-title").innerText = deck.discord.title; 291 | } else { 292 | iqs(item, ".ddb-discord").classList.add("disabled"); 293 | iqs(item, ".ddb-discord-title").innerText = "[No Discord Server]"; 294 | iqs(item, ".ddb-discord svg").classList.add("dark"); 295 | } 296 | }, 297 | 298 | decklists: function(item, deck) { 299 | const decklists = iqs(item, ".ddb-decklists"); 300 | for (let i = 0; i < deck.decklists.length; i++) { 301 | const decklist = deck.decklists[i]; 302 | const li = id("decklist-template").cloneNode(true); 303 | li.id = ""; 304 | if (!decklist.primer) { 305 | iqs(li, ".primer-svg").classList.add("unavailable"); 306 | } 307 | iqs(li, ".ddb-decklist-title").innerText = decklist.title; 308 | iqs(li, "a").href = decklist.link; 309 | decklists.appendChild(li); 310 | } 311 | }, 312 | 313 | commanders: function(item, deck) { 314 | const commanders = iqs(item, ".ddb-commanders"); 315 | for (let i = 0; i < deck.commander.length; i++) { 316 | const commander = deck.commander[i]; 317 | const a = document.createElement("a"); 318 | a.innerText = commander.name; 319 | a.href = commander.link; 320 | 321 | const li = document.createElement("li"); 322 | li.classList.add("btn"); 323 | li.appendChild(a); 324 | commanders.appendChild(li); 325 | } 326 | }, 327 | 328 | editor: function(item, deck) { 329 | if (deck.editor) { 330 | iqs(item, ".ddb-username").innerText = deck.editor; 331 | } else { 332 | iqs(item, ".ddb-username").innerText = "[Never Edited]"; 333 | } 334 | } 335 | } 336 | 337 | function toggleSub() { 338 | const mainId = this.id; 339 | const subId = "s" + (mainId.substring(1, mainId.length)); 340 | id(subId).classList.toggle("hidden"); 341 | } 342 | 343 | async function readRequests() { 344 | const jwt = get("jwt"); 345 | const body = { 346 | "jwt": jwt, 347 | "method": "READ_REQUESTS" 348 | }; 349 | const result = await sendToDDB(body); 350 | if (result.success) { 351 | const requests = result.data; 352 | populateRequests(requests); 353 | } else { 354 | console.error(result.message); 355 | if (result.data) { 356 | console.error(result.data); 357 | } 358 | alert(" There was an error while fetching requests:\n" + result.message); 359 | } 360 | } 361 | 362 | function populateRequests(requests) { 363 | const sortedRequests = requests.sort((a, b) => { return b.date.localeCompare(a.date); }); 364 | const requestList = id("requests"); 365 | const template = id("req-template"); 366 | for (let i = 0; i < sortedRequests.length; i++) { 367 | const req = sortedRequests[i]; 368 | const item = template.cloneNode(true); 369 | if (req.deleted) { 370 | item.classList.add("deleted", "hidden"); 371 | item.querySelector(".req-delete").classList.add("hidden"); 372 | } 373 | iqs(item, ".req-username").innerText = req.username ? req.username : ""; 374 | item.classList.add("r" + req.id); 375 | item.querySelector(".req-category").innerText = req.category; 376 | item.querySelector(".req-description").innerText = req.description; 377 | item.querySelector(".req-date").innerText = req.date.substring(0, 10); 378 | item.querySelector(".req-delete").id = req.id; 379 | item.querySelector(".req-delete").addEventListener("click", deleteRequest); 380 | requestList.appendChild(item); 381 | } 382 | } 383 | 384 | function toggleRequests() { 385 | const reqs = qsa("#requests li"); 386 | for (let i = 0; i < reqs.length; i++) { 387 | reqs[i].classList.toggle("hidden"); 388 | } 389 | } 390 | 391 | async function deleteRequest() { 392 | const jwt = get("jwt"); 393 | const id = this.id; 394 | const body = { 395 | "jwt": jwt, 396 | "method": "DELETE_REQUEST", 397 | "id": id 398 | }; 399 | qs(".r" + id).style.backgroundColor = "darkred"; 400 | const result = await sendToDDB(body); 401 | if (result.success) { 402 | console.log(result.message); 403 | console.log(result.data); 404 | qs(".r" + id).style.backgroundColor = "var(--canvas-color)"; 405 | qs(".r" + id).classList.add("deleted", "hidden"); 406 | qs(".r" + id + " .req-delete").classList.add("hidden"); 407 | } else { 408 | console.error(result.message); 409 | if (result.data) { 410 | console.error(result.data); 411 | } 412 | alert(" There was an error while deleting that request:\n" + result.message); 413 | } 414 | } 415 | 416 | /* HELPER FUNCTIONS */ 417 | {% include javascript/dom.js %} 418 | {% include javascript/backend.js %} 419 | 420 | })(); 421 | --------------------------------------------------------------------------------