diff --git a/src/commands/recipe.ts b/src/commands/recipe.ts index 2f325fc..1527fc1 100644 --- a/src/commands/recipe.ts +++ b/src/commands/recipe.ts @@ -23,7 +23,7 @@ export default { if (sub === 'create') { interaction.reply({ ephemeral: true, - content: `To create a recipe, go here: ${interaction.client.config.siteURL}/create-recipe/\nOnce done, click the button below and paste the resulting string in.` + content: `To create a recipe, go here: ${interaction.client.config.siteURL}/create-recipe/?guild=${interaction.guildId}\nOnce done, click the button below and paste the resulting string in.` }); } } diff --git a/src/web.ts b/src/web.ts index bccaebd..b2c625a 100644 --- a/src/web.ts +++ b/src/web.ts @@ -1,10 +1,28 @@ import express from 'express'; import * as log from './lib/log'; +import { CustomItem, db } from './lib/db'; +import { defaultItems } from './lib/rpg/items'; export async function startServer(port: number) { const app = express(); app.use(express.static('static/')); + app.get('/api/items', async (req, res) => { + const guildID = req.query.guild; + + let customItems : Partial[]; + if (guildID) { + customItems = await db('customItems') + .select('emoji', 'name', 'id', 'description') + .where('guild', guildID) + .limit(25); + } else { + customItems = []; + } + + res.json([...defaultItems, ...customItems]); + }); + app.listen(port, () => log.info(`web interface listening on ${port}`)); } \ No newline at end of file diff --git a/static/assets/jillo_small.png b/static/assets/jillo_small.png new file mode 100644 index 0000000..5d89a8d Binary files /dev/null and b/static/assets/jillo_small.png differ diff --git a/static/create-recipe/index.html b/static/create-recipe/index.html index 32f95c0..827fdde 100644 --- a/static/create-recipe/index.html +++ b/static/create-recipe/index.html @@ -1 +1,58 @@ -hi \ No newline at end of file + + + + + + jillo + + + + + + + + + + + + +
+
+
+
+ jillo +
+ +
+ +

Recipe Creator

+ +
+ loading available items... +
+ +

Drag items from above into the below lists:

+ +

Inputs Ingredients necessary to create the outputs

+
+
+

Requirements Unlike inputs, these are not consumed, but are necessary

+
+
+

Outputs The result of the recipe

+
+
+ +

Drag an item out of the list in order to remove it

+ +

Recipe string

+

+      Copy-paste this into Jillo to create your recipe
+    
+ + \ No newline at end of file diff --git a/static/create-recipe/script.js b/static/create-recipe/script.js new file mode 100644 index 0000000..3b498a7 --- /dev/null +++ b/static/create-recipe/script.js @@ -0,0 +1,145 @@ +let resolveLoaded; +const loaded = new Promise(resolve => resolveLoaded = resolve); + +function e(unsafeText) { + let div = document.createElement('div'); + div.innerText = unsafeText; + return div.innerHTML; +} + +/** + * @type {import('../../src/lib/rpg/items').Item | null} + */ +let draggedItem = null; +/** + * @type {Record} + */ +let itemLists = {}; + +function listToString(list) { + return list.map(stack => `${stack.item.id},${stack.quantity}`).join(';'); +} +function updateString() { + document.querySelector('#recipe-string').innerText = [ + listToString(itemLists['inputs'] || []), + listToString(itemLists['requirements'] || []), + listToString(itemLists['outputs'] || []), + ].join('|'); +} + +/** + * @param {import('../../src/lib/rpg/items').Item} item + */ +function renderItem(item) { + const i = document.createElement('div'); + i.innerHTML = ` +
+ ${e(item.emoji)} +
+
+
${e(item.name)}
+
${item.description ? e(item.description) : 'No description'}
+
+ `; + i.classList.add('item'); + i.draggable = true; + i.addEventListener('dragstart', event => { + draggedItem = item; + event.target.classList.add('dragging'); + }); + i.addEventListener('dragend', event => { + draggedItem = null; + event.target.classList.remove('dragging'); + }); + return i; +} +function renderItemStack(item, quantity, type) { + const i = document.createElement('div'); + i.innerHTML = ` +
+ ${e(item.emoji)} +
+
+ x${quantity} +
+ `; + i.classList.add('itemstack'); + i.draggable = true; + i.addEventListener('dragstart', event => { + event.target.classList.add('dragging'); + }); + i.addEventListener('dragend', event => { + event.target.classList.remove('dragging'); + itemLists[type] = itemLists[type] || []; + const items = itemLists[type]; + const stackIdx = items.findIndex(n => n.item.id === item.id); + if (stackIdx !== -1) items.splice(stackIdx, 1); + document.querySelector(`.item-list[data-type="${type}"]`).replaceWith(renderItemList(items, type)); + updateString(); + }); + return i; +} +function renderItemList(items, type) { + const i = document.createElement('div'); + i.textContent = ''; + items.forEach(itemStack => { + i.appendChild(renderItemStack(itemStack.item, itemStack.quantity, type)); + }); + i.dataset.type = type; + i.classList.add('item-list'); + + // prevent default to allow drop + i.addEventListener('dragover', (event) => event.preventDefault(), false); + + i.addEventListener('dragenter', event => draggedItem && event.target.classList.add('dropping')); + i.addEventListener('dragleave', event => draggedItem && event.target.classList.remove('dropping')); + + i.addEventListener('drop', (event) => { + event.preventDefault(); + event.target.classList.remove('dropping'); + + if (!draggedItem) return; + + itemLists[type] = itemLists[type] || []; + const items = itemLists[type]; + + const itemStack = items.find(v => v.item.id === draggedItem.id); + + if (!itemStack) { + items.push({ + item: draggedItem, + quantity: 1 + }); + } else { + itemStack.quantity = itemStack.quantity + 1; + } + + updateString(); + + draggedItem = null; + + event.target.replaceWith(renderItemList(items, type)); + }); + + return i; +} + +Promise.all([ + fetch(`/api/items${document.location.search}`) + .then(res => res.json()), + loaded +]).then(items => { + const itemsContainer = document.querySelector('.items'); + itemsContainer.textContent = ''; + items[0].forEach(item => + itemsContainer.appendChild(renderItem(item)) + ); + document.querySelectorAll('.item-list').forEach(elem => { + const type = elem.dataset.type; + elem.replaceWith(renderItemList([], type)); + }); + + updateString(); +}); + +document.addEventListener('DOMContentLoaded', () => resolveLoaded()); \ No newline at end of file diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..28a8153 Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/style.css b/static/style.css index 399f018..db33617 100644 --- a/static/style.css +++ b/static/style.css @@ -11,10 +11,9 @@ body { font-family: 'Balsamiq Sans', sans-serif; font-weight: 300; width: 100%; - min-height: 100vh; text-underline-offset: 3px; font-size: 16px; - color-scheme: dark; + color-scheme: light dark; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -23,13 +22,19 @@ body { :root { --text-color: #111; + --text-color-light: #444; --background-color: #fefefd; + --background-color-dark: #fafafb; + --background-color-dark-2: #f8f8f9; } @media (prefers-color-scheme: dark) { :root { --text-color: #eee; + --text-color-light: #aaa; --background-color: #111110; + --background-color-dark: #151514; + --background-color-dark-2: #171718; } } @@ -77,4 +82,169 @@ a:hover { @keyframes popup { 0% { transform: scale(0) rotate(40deg) } 100% { transform: scale(1) rotate(0deg) } +} + +#content { + max-width: 1000px; + width: 100%; + margin: auto; + margin-bottom: 6rem; +} + +.header { + height: 3rem; + display: flex; + align-items: stretch; + padding: 0rem 1rem; + flex-direction: row; + text-shadow: 2px 2px 2px #000000; + margin-bottom: 2rem; +} +.header .bg { + position: absolute; + left: 0%; + right: 0%; + height: 3rem; + background: linear-gradient(#444, #222); + z-index: -1; + border-bottom: 2px ridge #aaa; +} +.header .left { + font-size: 1.5rem; + flex: 1 1 0px; + display: flex; + align-items: center; +} +.header .left a { + text-decoration: none !important; + color: #fff; +} + +.header .links { + flex: 0 0 auto; + text-align: right; + display: flex; + flex-direction: row; + align-items: center; + gap: 0.25rem; +} +.header .links img { + display: block; + width: auto; + height: 100%; +} + +.items { + max-width: 500px; + padding: 1em; + height: 200px; + overflow: auto; + background: linear-gradient(var(--background-color-dark), var(--background-color-dark-2)); + border-radius: 1em; + display: flex; + flex-direction: column; +} +.item { + display: flex; + flex-direction: row; + height: 3rem; + gap: 0.5rem; + outline: 0px solid rgba(255, 255, 255, 0.0); + transition: outline 0.1s; + padding: 0.5rem; + border-radius: 2rem; + cursor: grab; +} +.item:hover { + outline: 1px solid var(--text-color-light); +} +.item.dragging { + outline: 2px dashed var(--text-color-light); + transition: none; + background-color: var(--background-color); +} +.item .icon { + flex: 0 0 auto; + font-size: 1rem; + line-height: 1; + background-color: rgba(199, 199, 199, 0.25); + border-radius: 4rem; + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 1 / 1; + user-select: none; +} +.item .right { + flex: 1 1 0px; + display: flex; + flex-direction: column; + line-height: 1; +} +.item .right, .item .right > * { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.item .right .description { + font-size: 75%; + color: var(--text-color-light); +} +.item-list { + min-height: 2rem; + display: flex; + flex-direction: row; + flex-wrap: wrap; +} +.item-list.dropping::after { + display: flex; + align-items: center; + justify-content: center; + content: '+'; + height: 1.5rem; + width: 1.5rem; + outline: 2px dashed var(--text-color-light); + border-radius: 2rem; + padding: 0.5rem; + margin: 0.25rem; +} +.itemstack { + display: flex; + align-items: center; + height: 1.5rem; + line-height: 1; + padding: 0.5rem; + margin: 0.25rem; + outline: 1px solid var(--text-color-light); + background-color: var(--background-color-dark); + border-radius: 2rem; + gap: 0.5rem; +} +.itemstack.dragging { + outline: 2px dashed var(--text-color-light); + transition: none; + background-color: var(--background-color); +} + +.subtitle { + color: var(--text-color-light); + font-size: 1rem; + font-weight: normal; + margin-left: 0.5rem; +} +.subtitle::before { + content: 'ยท'; + margin-right: 0.5rem; +} + +pre { + background: linear-gradient(var(--background-color-dark), var(--background-color-dark-2)); + overflow: auto; + word-break: normal; + padding: 0.5rem; +} + +.note { + font-style: italic; + color: var(--text-color-light); } \ No newline at end of file