commit 0a782e0096f3b6ec17c8a23204175eebb997e1aa Author: bakonpancakz Date: Sun May 24 21:58:23 2026 -0700 Initial Release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e5e365 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.development \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..becc7c8 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "golang.go" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..22144ff --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,26 @@ +{ + "editor.tabSize": 4, + "editor.insertSpaces": true, + "editor.formatOnSave": true, + "editor.rulers": [ + 120 + ], + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "files.associations": {}, + "[css]": { + "editor.defaultFormatter": "vscode.css-language-features" + }, + "[javascript]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..bdcf269 --- /dev/null +++ b/README.txt @@ -0,0 +1,24 @@ +------------------------------------------------------------------------------- +ABOUT +------------------------------------------------------------------------------- + +Official website for the suzzy project, an amateur game dev trio. + +@bakonpancakz 'bakon' + * https://x.com/bakonpancakz + +Andrew 'DakotaLmfao' Lopez + * https://x.com/DanteLmfao + +@suldotpwp 'sul' + * https://x.com/suldotpwp + +------------------------------------------------------------------------------- +HISTORY +------------------------------------------------------------------------------- + +* suzzy-v0-standalone: + Abandonware, see README. + +* suzzy-v1-revival: + Welcome back, based off pancakz-v1-caution. diff --git a/suzzy-v0-standalone/.gitignore b/suzzy-v0-standalone/.gitignore new file mode 100644 index 0000000..53c37a1 --- /dev/null +++ b/suzzy-v0-standalone/.gitignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/suzzy-v0-standalone/README.old.md b/suzzy-v0-standalone/README.old.md new file mode 100644 index 0000000..ece9eba --- /dev/null +++ b/suzzy-v0-standalone/README.old.md @@ -0,0 +1,62 @@ +# 🌎 @suzzy/site +Homepage for blogs, documentation, accounts, and projects. + + +## 📄 Articles +This application has support for a custom article rendering system using XML +and JSON syntax. Create a folder in the **views** directory and organize your +folder as show below: + +```py +• +|__ /articles +| |__ ... # Your articles here +|__ manifest.json # Your Manifest with Authors, Categories, and Paths +|__ browser.pug # View for Article Directory +|__ post.pug # View for Article Rendering +``` + +### Article Syntax +Articles follow HTML/XML like Syntax, a root `
` tag must be created +with all following content tags placed inside. + +The following attributes **MUST** be present on the article tag: +| Name | Description | +| :--------: | :-------------------------------------------------- | +| id | Unique Article ID for CategoryId | +| authorId | Relevant Author ID | +| categoryId | Relevant Category ID | +| title | Article Title | +| created | Article Date (Should be parsable with `new Date()`) | +| snippet | Article Snippet | +| noindex | If value is `true` this article will not be indexed | + +Afterwards you can place all your content tags inside below are all currently +supported tags: + +### Tag: text +A text element with support for formatting using the following subtags: +- `: Hint` Makes text grey and small +- `: Code` Highlights text and adds a dark background +- `: Link` Similar to Anchor, supports **target** and **href** attributes +- `: Bold` Renders text as bold or extra thicc +- `: Italic` Render Text as Italicized +- `: Strike` Renders text with a line in the middle of characters (̶E̶x̶a̶m̶p̶l̶e) + +### Tag: header +Extra large highlighted text that will appear in the sidebar or chapter select. +Will be rendered with a normalized ID attribute for anchors. + +### Tag: subheader +Large highlighted text. +Will be rendered with a normalized ID attribute for anchors. + +### Tag: quote +Prefixes text content with `>` and renders it as grey with small text. + +### Tag: code +Renders text as a big code block with syntax highlighting using the `syntax` +attribute. Supports horizontal overflow and adds support for has JavaScript +for a **Copy to Cliboard** action. + +### Tag: image diff --git a/suzzy-v0-standalone/README.txt b/suzzy-v0-standalone/README.txt new file mode 100644 index 0000000..218cec4 --- /dev/null +++ b/suzzy-v0-standalone/README.txt @@ -0,0 +1,19 @@ + +Originally an indie studio was going to be made between me and another friend +under the moniker "suzzy games". We planned to make multiplayer games, mostly +ones that ran in the web browser so I researched and designed infastructure to +allow that. However this idea was scrapped and I opted to use my personal +brand 'pancakz' instead. + +This website is forever stuck in limbo due to an unfinished refactor. + +A lot of it's code was canabalized. Deleted and reafactored into 'template-auth' +which was itself was also canabalized and now feeds into every other project +that requires an authentication framework. + +If you wish to see what this website looks like for yourself run the following +commands: + +* npm install +* npm run preview +* explorer.exe http://localhost:8080 diff --git a/suzzy-v0-standalone/_todo/__oauth2.pug b/suzzy-v0-standalone/_todo/__oauth2.pug new file mode 100644 index 0000000..5019d20 --- /dev/null +++ b/suzzy-v0-standalone/_todo/__oauth2.pug @@ -0,0 +1,56 @@ +extends _TemplateSpecial.pug +block pageHeader + link(rel='stylesheet' href='file://styles/ui-account.css') + link(rel='stylesheet' href='file://styles/unique-oauth2.css') + meta(property='og:title' content='suzzy - Authorize App') + meta(property='og:description' content='Connect an application to your suzzy games account.') + meta(name='description' content='Connect an application to your suzzy games account.') + title suzzy - Authorize App +block pageContent + div(class='modal-wrapper modal-static') + + //- Error Modal + div(id='oauth2-fail' class='oauth2-centered oauth2-container modal-container' hidden) + div(class='modal-content') + img(class='oauth2-icon' aria-hidden='true' src='/assets/images/icons/icon-cross.svg' alt='Icon for Cross') + p(class='text-header') An Error has Occurred + p(class='text-tip' id='fail-reason') Unknown Error + + //- Loading Modal + div(id='oauth2-load' class='oauth2-centered oauth2-container modal-container') + div(class='oauth2-centered modal-content') + img(aria-hidden='true' src='/assets/images/icons/diagram-loading.svg' alt='Icon for Spinner' height=64) + p Loading Application + + //- Content Modal + div(id='oauth2-flow' class='oauth2-container modal-container' hidden) + div(class='oauth2-content modal-content') + + //- Header + div(class='divider-spacer') + div(class='oauth2-header') + div(class='oauth2-icons') + img(class='application-icon' id='image-user' onload='this.style.opacity=1') + img(aria-hidden='true' class='oauth2-diagram' src='/assets/images/icons/diagram-connection.svg' alt='Diagram for Connection') + img(class='application-icon' id='image-app' onload='this.style.opacity=1') + p(id='content-aname' class='text-header') + button(id='content-login' class='small' onclick='SignOut(false, true)') + div(class='divider-spacer') + div(class='divider-line') + + //- Information + p(class='text-tip') This application will be allowed to: + div(id='content-scopes' class='scopes-wrapper text-box') + div(class='divider-line') + p(id='content-created' class='text-tip text-muted') + | • Active since + p(id='content-redirect' class='text-tip text-muted') + | • You'll be redirected to: #[br] + + //- Actions + div(class='modal-actions') + p(class='modal-tooltip text-error' id='action-error') + button(class='default' id='action-cancel') Cancel + button(class='default' id='action-authorize') Authorize +block pageEscaped + script(src='/assets/scripts/login-oauth2.js') \ No newline at end of file diff --git a/suzzy-v0-standalone/_todo/_scripts/account-applications.js b/suzzy-v0-standalone/_todo/_scripts/account-applications.js new file mode 100644 index 0000000..408e143 --- /dev/null +++ b/suzzy-v0-standalone/_todo/_scripts/account-applications.js @@ -0,0 +1,291 @@ +(() => { + // Support Functions + const trimDataURL = (s, x = undefined) => s.slice(s.indexOf(',') + 1, x) + const redirectsToString = s => s.sort().join() + function PrepareTemplate(selector) { + const elem = $(selector) + elem.removeAttribute('hidden') + elem.removeAttribute('id') + elem.remove() + return elem + } + + // No Applications Warning + let appCount = 0 + function DisplayNoApps(newCount) { + _ActionCreate.removeAttribute('hidden') + appCount = newCount + appCount === 0 + ? _AlertEmpty.removeAttribute('hidden') + : _AlertEmpty.setAttribute('hidden', true) + } + + // Displays Modal with Secret Key + function DisplaySecret(secretKey) { + CreateModal({ + behaviour: { + title: 'Your Secret Key', + tooltip: 'Do not share this key with anyone!', + submitText: 'Close', + cancelText: '', + }, + _secret: { + type: 'text', + title: '', + value: secretKey, + readonly: true, + } + }) + } + + // Use Application Template + const + _AlertEmpty = $('#alert-empty'), + _AlertLoad = $('#alert-load'), + _ActionCreate = $('#action-create'), + _Collection = $('#application-collection'), + _LinkTemplate = PrepareTemplate('.input-redirect'), + _ItemTemplate = PrepareTemplate('#application-template') + + function DisplayApplication(a) { + let removingIcon = false + let unsavedChanges = false + const + item = _ItemTemplate.cloneNode(true), + elemStatus = $$(item, '#status-message'), + elemOptions = $$(item, '.application-options'), + elemRedirects = $$(item, '#container-redirects'), + previewIcon = $$(item, '#app-icon'), + previewName = $$(item, '#app-name'), + previewDesc = $$(item, '#app-desc'), + inputName = $$(item, '#input-name'), + inputDesc = $$(item, '#input-desc'), + inputIcon = $$(item, '#input-icon'), + inputIconFile = $$(item, '#input-icon-file'), + inputIconRemove = $$(item, '#input-icon-remove'), + inputRedirectNew = $$(item, '#input-redirect-new'), + inputSave = $$(item, '#input-save'), + inputDiscard = $$(item, '#input-discard'), + inputResetSecret = $$(item, '#input-reset'), + inputDelete = $$(item, '#input-delete'), + inputEdit = $$(item, '#input-edit') + + function CreateRedirect(givenURL) { + const elem = _LinkTemplate.cloneNode(true) + const inputUrl = elem.firstChild + inputUrl.value = givenURL + inputUrl.addEventListener('change', UpdateForm) + elem.lastChild.addEventListener('click', () => { + elem.remove() + UpdateForm() + }) + elemRedirects.appendChild(elem) + } + function ResetForm() { + removingIcon = false + previewName.textContent = a.name?.trim() || 'Untitled' + previewDesc.textContent = a.description?.trim() || 'No Description Provided.' + previewIcon.src = ImageURL('icons', a.id, a.icon, false, 128) + previewIcon.alt = `Icon for ${a.name}` + inputIconFile.value = null + inputDesc.value = a.description + inputName.value = a.name + elemRedirects.innerHTML = '' + a.redirects.forEach(CreateRedirect) + UpdateForm() + } + function UpdateForm() { + // New Redirect + elemRedirects.childElementCount >= 10 + ? inputRedirectNew.setAttribute('hidden', true) + : inputRedirectNew.removeAttribute('hidden') + + // Remove Icon + !removingIcon && (a.icon !== null || inputIconFile.files.length > 0) + ? inputIconRemove.removeAttribute('disabled') + : inputIconRemove.setAttribute('disabled', true) + + // Save/Discard Changes + unsavedChanges = + Object.keys(CollectBody()).length && + Array.from(elemRedirects.childNodes).every(e => e.firstChild.checkValidity()) + if (unsavedChanges) { + inputDiscard.removeAttribute('disabled') + inputSave.removeAttribute('disabled') + } else { + inputDiscard.setAttribute('disabled', true) + inputSave.setAttribute('disabled', true) + } + } + function CollectBody() { + const body = {} + if ((inputName.value.trim() || a.name) !== a.name) + body['name'] = inputName.value + if (inputDesc.value.trim() !== (a.description || '')) + body['description'] = inputDesc.value + if (removingIcon && body.icon) + body['icon'] = '' + if (inputIconFile.files.length) + body['icon'] = trimDataURL(previewIcon.src) + const collectedRedirects = Array + .from(elemRedirects.childNodes) + .filter(e => e.firstChild.checkValidity()) + .map(e => e.firstChild.value) + if (redirectsToString(collectedRedirects) !== redirectsToString(a.redirects)) { + body.redirects = collectedRedirects + } + return body + } + + // Set/Remove Application Icon + inputIconRemove.addEventListener('click', () => { + previewIcon.src = ImageURL('icon', a.id, null) + inputIconFile.value = null + removingIcon = true + UpdateForm() + }) + inputIconFile.addEventListener('change', ev => { + const file = ev.target.files[0] + if (!file) return + const reader = new FileReader() + reader.onload = e => { + previewIcon.src = e.target.result + removingIcon = false + UpdateForm() + } + reader.readAsDataURL(file) + }) + + // Field Updating + inputName.addEventListener('input', () => previewName.textContent = inputName.value.trim() || a.name) + inputDesc.addEventListener('input', () => previewDesc.textContent = inputDesc.value.trim() || a.description) + for (const needsListener of [inputName, inputDesc]) { + needsListener.addEventListener('change', UpdateForm) + } + // Button Listeners + inputRedirectNew.addEventListener('click', + () => CreateRedirect('') + ) + inputDiscard.addEventListener('click', + () => ResetForm() + ) + inputIcon.addEventListener('click', + () => inputIconFile.click() + ) + inputEdit.addEventListener('click', () => { + elemOptions.classList.toggle('options-hidden') + ResetForm() + }) + inputSave.addEventListener('click', () => { + if (!unsavedChanges) return + const unlock = LockInteraction() + elemStatus.textContent = 'Saving Changes...' + SendRequest('PATCH', `/api/v1/users/@me/applications/${a.id}`, CollectBody(), 1) + .then(([_, patchedBody]) => { + elemStatus.textContent = 'Changes Saved!' + elemStatus.classList.remove('text-error') + a = patchedBody + unlock() + ResetForm() + }) + .catch(e => { + elemStatus.textContent = e + elemStatus.classList.add('text-error') + unlock() + }) + // we dont use finally block here because race condition :p + }) + inputResetSecret.addEventListener('click', () => CreateModal({ + behaviour: { + title: 'Reset Secret Key?', + tooltip: `${a.name} will stop functioning until you update the secret key in your code.`, + submitText: 'Reset', + onSubmit: () => new Promise(ok => { + SendRequest('DELETE', `/api/v1/users/@me/applications/${a.id}/reset`, undefined, 2) + .then(([_, body]) => { + DisplaySecret(body.secret_key) + ok() + }) + .catch(ok) + }) + } + })) + inputDelete.addEventListener('click', () => CreateModal({ + behaviour: { + title: 'Delete Application?', + tooltip: `${a.name} will be deleted and all connections will be severed. This action cannot be undone.`, + submitText: 'Delete', + onSubmit: () => new Promise(ok => { + SendRequest('DELETE', `/api/v1/users/@me/applications/${a.id}`, undefined, 2) + .then(() => { + item.remove() + DisplayNoApps(appCount - 1) + ok() + }) + .catch(ok) + }) + } + })) + + // Append to top + // Official Application? + if ((a.flags & 1 << 3) !== 0) { + $$(item, '#verified-message').removeAttribute('hidden') + } + $$(item, '#input-clientid').textContent += a.id + ResetForm() + _Collection.appendChild(item) + } + + // Fetch Connections + + SendRequest('GET', '/api/v1/users/@me/applications', null, 1) + .then(([_, body]) => { + // Display Applications + body + .sort((a, b) => new Date(b.created) - new Date(a.created)) + .forEach(DisplayApplication) + DisplayNoApps(body.length) + + // Bind Create Application + _ActionCreate.addEventListener('click', function ActionCreate() { + CreateModal({ + behaviour: { + title: 'Create Application', + tooltip: 'Name it something cool like Phase Connect.', + onSubmit: ({ name }) => new Promise(ok => { + SendRequest('POST', '/api/v1/users/@me/applications', { name }, 1) + .then(([_, body]) => { + DisplaySecret(body.secret_key) + DisplayNoApps(appCount + 1) + DisplayApplication(body) + ok() + }) + .catch(ok) + }) + }, + _divider: { type: 'divider-line' }, + name: { + type: 'text', + required: true, + title: 'Name', + validator: function ValidateName(v) { + if (v.length > 24) return 'Application name cannot be longer than 32 characters' + if (v.length < 1) return 'Application name cannot be shorter than 1 character' + return true + } + }, + }) + }) + }) + .catch(e => { + // Display Error Message + $('#alert-error').removeAttribute('hidden') + $('#error-message').textContent = e + console.log(e) + }) + .finally(() => { + // Hide Loading Symbol + _AlertLoad.setAttribute('hidden', true) + }) +})() \ No newline at end of file diff --git a/suzzy-v0-standalone/_todo/_scripts/account-connections.js b/suzzy-v0-standalone/_todo/_scripts/account-connections.js new file mode 100644 index 0000000..8572eb1 --- /dev/null +++ b/suzzy-v0-standalone/_todo/_scripts/account-connections.js @@ -0,0 +1,94 @@ +(async () => { + const _AlertEmpty = $('#alert-empty') + const _AlertLoad = $('#alert-load') + const _Collection = $('#application-collection') + + // Prepare Templates + function prepareTemplate(name) { + const elem = $(name) + elem.removeAttribute('hidden') + elem.removeAttribute('id') + elem.remove() + return elem + } + const _ItemTemplate = prepareTemplate('#application-template') + const _ScopeTemplate = prepareTemplate('#scope-template') + + // Fetch Connections + SendRequest('GET', '/api/v1/users/@me/connections', null, 1) + .then(([_, body]) => { + if (body.length === 0) { + _AlertEmpty.removeAttribute('hidden') + return + } + body + .sort((a, b) => new Date(b.created) - new Date(a.created)) + .forEach(connection => { + + // Metadata + const { name, flags, description, id, icon } = connection.application + const Template = _ItemTemplate.cloneNode(true) + $$(Template, '.text-header').textContent = name + $$(Template, '.text-tip').textContent = description || 'No Description Provided.' + + // Images + const Image = $$(Template, '.application-icon') + Image.src = ImageURL('icons', id, icon, false, 128) + Image.alt = `Icon for ${name}` + + // Scopes + const ScopesParent = Template.querySelector('.scopes') + _Scopes.forEach(s => { + if ((connection.scopes & s[0]) !== 0) { + const Scope = _ScopeTemplate.cloneNode(true) + Scope.lastChild.textContent = s[1] + ScopesParent.appendChild(Scope) + } + }) + + // Official Application? + if ((flags & 1 << 3) !== 0) { + $$(Template, '#verified-message').removeAttribute('hidden') + } + + // Buttons + const Button = $$(Template, 'button') + Button.title = `Disconnect Application ${name}` + Button.addEventListener('click', async () => { + await CreateModal({ + behaviour: { + title: 'Disconnect Application?', + tooltip: 'The application ' + name + ' will be disconnected from your account.' + } + }) + SendRequest('DELETE', `/api/v1/users/@me/connections/${connection.id}`, null, 2) + .then(() => { + // Remove Item from List + body.length-- + Template.remove() + if (body.length === 0) { + _AlertEmpty.removeAttribute('hidden') + _AlertLoad.setAttribute('hidden', true) + return + } + }) + .catch(e => { + $$(Template, '#error-message').textContent = e + }) + }) + + // Append + _Collection.appendChild(Template) + }) + }) + .catch(e => { + // Display Error Message + $('#alert-error').removeAttribute('hidden') + $('#error-message').textContent = e + console.log(e) + }) + .finally(() => { + // Hide Loading Symbol + _AlertLoad.setAttribute('hidden', true) + }) +})() \ No newline at end of file diff --git a/suzzy-v0-standalone/_todo/_scripts/account-devices.js b/suzzy-v0-standalone/_todo/_scripts/account-devices.js new file mode 100644 index 0000000..63fdb7e --- /dev/null +++ b/suzzy-v0-standalone/_todo/_scripts/account-devices.js @@ -0,0 +1,80 @@ +(async () => { + const _Collection = $('#application-collection') + const _Template = $('#device-template') + _Template.removeAttribute('hidden') + _Template.removeAttribute('id') + _Template.remove() + + // Fetch Sessions + SendRequest('GET', '/api/v1/users/@me/security/sessions', null, 1) + .then(([_, body]) => { + + // There's at minimum one session so we + // don't need a the 'No Connections' type message here. + + body.sessions + .sort((a, b) => new Date(b.last_used) - new Date(a.last_used)) + .forEach(session => { + // Metadata + const Template = _Template.cloneNode(true) + const Me = (session.fingerprint === body.fingerprint) + if (Me) $$(Template, '#me').removeAttribute('hidden') + $$(Template, '#browser').textContent = session.browser + $$(Template, '#location').textContent = session.location + $$(Template, '#lastseen').textContent = (() => { + const secondsAgo = (new Date() - new Date(session.last_used)) / 1000 | 0 + if (secondsAgo < 60) { + return 'just now' + } else { + const timeUnits = [ + ['minute', 60], + ['hour', 3600], + ['day', 86400], + ['week', 604800], + ['month', 2592000], + ]; + for (const [unit, divisor] of timeUnits) { + if (secondsAgo < (divisor * 60)) { + const count = Math.floor(secondsAgo / divisor) + return `${count} ${unit}${count > 1 ? 's' : ''} ago` + } + } + } + })() + + // Buttons + const Button = $$(Template, '.device-action') + Button.title = `Revoke Access for '${session.browser}' from '${session.location}'` + $$(Template, '.device-action').onclick = async () => { + await CreateModal({ + behaviour: { + title: 'Revoke Session?', + tooltip: 'You\'ll have to manually log back in on this device.' + } + }) + SendRequest('DELETE', `/api/v1/users/@me/security/sessions/${session.id}`, null, 2) + .then(() => { + // Redirect User to Login page if revoked self. + Template.remove() + if (Me) window.location.href = '/login' + }) + .catch(e => { + $$(Template, '#error-message').textContent = e + }) + } + + // Append + _Collection.appendChild(Template) + }) + }) + .catch(e => { + // Display Error Message + $('#alert-error').removeAttribute('hidden') + $('#error-message').textContent = e + console.log(e) + }) + .finally(() => { + // Hide Loading Symbol + $('#alert-load').setAttribute('hidden', true) + }) +})() \ No newline at end of file diff --git a/suzzy-v0-standalone/_todo/_scripts/account-profile.js b/suzzy-v0-standalone/_todo/_scripts/account-profile.js new file mode 100644 index 0000000..b929521 --- /dev/null +++ b/suzzy-v0-standalone/_todo/_scripts/account-profile.js @@ -0,0 +1,263 @@ +(() => { + const + _Profile = $('.profile-wrapper'), + _Preview = $('.profile-container'), + _Status = $('#profile-status'), + // Option Elements + iDisplayname = $('#input-displayname'), + iSubtitle = $('#input-subtitle'), + iBio = $('#input-bio'), + iAvatarFile = $('#input-avatar-file'), + iBannerFile = $('#input-banner-file'), + iBannerColor = $('#input-banner'), + iBorderColor = $('#input-border'), + iBackgroundColor = $('#input-background'), + iSubmit = $('#button-submit'), + iCancel = $('#button-cancel'), + // Preview Elements + pBanner = $('#profile-banner'), + pAvatar = $('#profile-avatar'), + pBadges = $('#profile-badges'), + pDisplayname = $('#profile-displayname'), + pUsername = $('#profile-username'), + pSubtitle = $('#profile-subtitle'), + pDivider = $('#profile-divider'), + pBio = $('#profile-bio'), + // Buttons + bAvatar = $('#button-avatar-remove'), + bBanner = $('#button-banner-remove') + + // Elements that the user interacts with in real time + const _InputElements = [ + iDisplayname, + iSubtitle, + iBio, + iBannerColor, + iBorderColor, + iBackgroundColor, + // These elements have async calls + // they'll update the preview when complete + // pBanner, + // pAvatar, + ] + const _RecognizedBadges = [ + [1 << 0, 'crown', 'The Boss'], + [1 << 1, 'star', 'V.I.P'], + [1 << 2, 'picross', 'Picross Master'], + ] + + SendRequest('GET', '/api/v1/users/@me', null, 1) + .then(([_, body]) => { + let removingBanner = false + let removingAvatar = false + let unsavedChanges = false + const hexToDec = e => parseInt(e.value.slice(1), 16) + const trimDataURL = (s, x = undefined) => s.slice(s.indexOf(',') + 1, x) + const decToHex = (e, x) => '#' + (e?.toString(16).padStart('6', 0) || x) + + function FormReset() { + const colorBanner = decToHex(body.accent_banner, '1d1e2e') + const colorBorder = decToHex(body.accent_border, '282a3f') + const colorBackground = decToHex(body.accent_background, '333550') + + // Reset Options + iDisplayname.value = body.displayname + iSubtitle.value = body.subtitle + iBio.value = body.biography + iBannerColor.value = colorBanner + iBorderColor.value = colorBorder + iBackgroundColor.value = colorBackground + iAvatarFile.value = null + iBannerFile.value = null + + // Reset Preview + pDisplayname.textContent = body.displayname + pUsername.textContent = '@' + body.username + pSubtitle.textContent = body.subtitle + pBio.textContent = body.biography + body.biography + ? pDivider.removeAttribute('hidden') + : pDivider.setAttribute('hidden', true) + pAvatar.src = ImageURL('avatars', body.id, body.avatar, true, 128) + pBanner.style.backgroundImage = body.banner + ? `url('${ImageURL('banners', body.id, body.banner, true, 512)}')` + : '' + _Profile.style.setProperty('--accent-banner', colorBanner) + _Profile.style.setProperty('--accent-border', colorBorder) + _Profile.style.setProperty('--accent-background', colorBackground) + iBackgroundColor.oninput() + + // Disable Buttons + removingAvatar = false + removingBanner = false + FormUpdated() + } + function FormUpdated() { + unsavedChanges = (Object.entries(CollectBody()).length > 0) + if (unsavedChanges) { + iCancel.removeAttribute('disabled') + iSubmit.removeAttribute('disabled') + } else { + iCancel.setAttribute('disabled', true) + iSubmit.setAttribute('disabled', true) + } + !removingBanner && (body.banner !== null || iBannerFile.files.length > 0) + ? bBanner.removeAttribute('disabled') + : bBanner.setAttribute('disabled', true) + + !removingAvatar && (body.avatar !== null || iAvatarFile.files.length > 0) + ? bAvatar.removeAttribute('disabled') + : bAvatar.setAttribute('disabled', true) + } + function CollectBody() { + const data = {} + // Collect Textboxes + if ((iDisplayname.value.trim() || body.displayname) !== body.displayname) + data['displayname'] = iDisplayname.value + if (iBio.value.trim() !== (body.biography || '')) + data['biography'] = iBio.value + if (iSubtitle.value.trim() !== (body.subtitle || '')) + data['subtitle'] = iSubtitle.value + + // Collect Colors + const decBannerColor = hexToDec(iBannerColor) + if (decBannerColor !== (body.accent_banner || 1908270)) + data['accent_banner'] = decBannerColor + const decBorderColor = hexToDec(iBorderColor) + if (decBorderColor !== (body.accent_border || 2632255)) + data['accent_border'] = decBorderColor + const decBackgroundColor = hexToDec(iBackgroundColor) + if (decBackgroundColor !== (body.accent_background || 3355984)) + data['accent_background'] = decBackgroundColor + + // Collect Images + if (removingAvatar && body.avatar) data['avatar'] = '' + if (removingBanner && body.banner) data['banner'] = '' + if (iAvatarFile.files.length) + data['avatar'] = trimDataURL(pAvatar.src) + if (iBannerFile.files.length) + data['banner'] = trimDataURL(pBanner.style.backgroundImage, -2) + + console.log(data) + return data + } + + // Templating + _InputElements.forEach(e => e.addEventListener('change', FormUpdated)) + _RecognizedBadges.forEach(b => { + if ((body.public_flags & b[0]) !== 0) { + const img = document.createElement('img') + img.src = `/assets/images/icons/badge-${b[1]}.svg` + img.title = b[2] + img.alt = b[2] + pBadges.appendChild(img) + } + }) + + // Section: About Me + iDisplayname.oninput = () => pDisplayname.textContent = (iDisplayname.value || body.displayname).trim() + iSubtitle.oninput = () => pSubtitle.textContent = iSubtitle.value.trim() + iBio.oninput = () => { + const content = (iBio.value || '') + pBio.textContent = content.trim() + content.length > 0 + ? pDivider.removeAttribute('hidden') + : pDivider.setAttribute('hidden', true) + } + + // Section: Images + $('#button-avatar').onclick = () => iAvatarFile.click() + $('#button-banner').onclick = () => iBannerFile.click() + bAvatar.onclick = () => { + pAvatar.src = ImageURL('avatars', body.id, null) + iAvatarFile.value = null + removingAvatar = true + FormUpdated() + } + bBanner.onclick = () => { + pBanner.style.backgroundImage = null + iBannerFile.value = null + removingBanner = true + FormUpdated() + } + + iAvatarFile.onchange = (ev) => { + const file = ev.target.files[0] + if (!file) return + const reader = new FileReader() + reader.onload = e => { + pAvatar.src = e.target.result + removingAvatar = false + FormUpdated() + } + reader.readAsDataURL(file) + } + iBannerFile.onchange = (ev) => { + const file = ev.target.files[0] + if (!file) return + const reader = new FileReader() + reader.onload = e => { + pBanner.style.backgroundImage = `url(${e.target.result})` + removingBanner = false + FormUpdated() + } + reader.readAsDataURL(file) + } + + // Section: Accents + iBannerColor.oninput = () => _Profile.style.setProperty('--accent-banner', iBannerColor.value) + iBorderColor.oninput = () => _Profile.style.setProperty('--accent-border', iBorderColor.value) + iBackgroundColor.oninput = () => { + let hexColor = iBackgroundColor.value + _Profile.style.setProperty('--accent-background', hexColor) + + // Check Luminance of Background Color + hexColor = hexColor.replace('#', '') + var r = parseInt(hexColor.substr(0, 2), 16) + var g = parseInt(hexColor.substr(2, 2), 16) + var b = parseInt(hexColor.substr(4, 2), 16) + if (((0.299 * r) + (0.587 * g) + (0.114 * b)) / 255 > 0.85) { + _Preview.classList.add('profile-dark') + } else { + _Preview.classList.remove('profile-dark') + } + } + + // Section: Actions + iCancel.onclick = () => FormReset() + iSubmit.onclick = () => { + if (!unsavedChanges) return + const unlock = LockInteraction() + _Status.textContent = 'Saving Changes...' + SendRequest('PATCH', '/api/v1/users/@me', CollectBody(), 1) + .then(([_, patchedBody]) => { + _Status.textContent = 'Profile Saved!' + _Status.classList.remove('text-error') + localStorage.removeItem('cache_home_me') // Used by page-home.js + body = patchedBody + unlock() + FormReset() + }) + .catch(e => { + _Status.textContent = e + _Status.classList.add('text-error') + unlock() + }) + // we dont use finally block here because race condition :p + } + + // Display Profile + _Profile.removeAttribute('hidden') + FormReset() + }) + .catch(e => { + // Display Error Message + $('#alert-error').removeAttribute('hidden') + $('#error-message').textContent = e + console.log(e) + }) + .finally(() => { + // Hide Loading Symbol + $('#alert-load').setAttribute('hidden', true) + }) +})() \ No newline at end of file diff --git a/suzzy-v0-standalone/_todo/_scripts/account-security.js b/suzzy-v0-standalone/_todo/_scripts/account-security.js new file mode 100644 index 0000000..aafe61d --- /dev/null +++ b/suzzy-v0-standalone/_todo/_scripts/account-security.js @@ -0,0 +1,281 @@ +(() => { + const + elemPreviewEmail = $('#preview-email'), + actionEmailReveal = $('#email-reveal'), + actionEmailEdit = $('#email-edit'), + actionEmailResend = $('#email-resend'), + actionPassChange = $('#password-change'), + actionMFASetup = $('#mfa-setup-start'), + actionMFASetupRemove = $('#mfa-setup-remove'), + actionMFARecoveryView = $('#mfa-recovery-view'), + actionMFARecoveryRegen = $('#mfa-recovery-regen'), + actionDangerDeleteAccount = $('#danger-deleteaccount'), + errorMFA = $('#error-mfa'), + errorEmail = $('#error-email'), + errorDanger = $('#error-danger') + + SendRequest('GET', '/api/v1/users/@me', null, 1) + .then(([_, body]) => { + // Email + let emailHidden = false + let emailTemplate = elemPreviewEmail.textContent + actionEmailReveal.onclick = () => { + let newPreview + if (emailHidden) { + newPreview = body.email + actionEmailReveal.textContent = 'Hide Email Address' + } else { + const usernameLength = body.email.indexOf('@') + const censoredSegment = ''.padStart(usernameLength > 12 ? 12 : usernameLength, '*') + const hostSegment = body.email.slice(usernameLength) + newPreview = censoredSegment + hostSegment + actionEmailReveal.textContent = 'Reveal Email Address' + } + elemPreviewEmail.textContent = (emailTemplate + newPreview) + emailHidden = !emailHidden + } + actionEmailReveal.onclick() + + if (!body.verified) { + actionEmailResend.removeAttribute('hidden') + actionEmailResend.onclick = () => { + const unlock = LockInteraction() + SendRequest('POST', '/api/v1/users/@me/security/email', null, 1) + .then(() => CreateModal({ + behaviour: { + cancelText: '', + submitText: 'Close', + title: 'Verification Email Sent', + tooltip: 'An email to verify this email address has been sent. Please check your spam or inbox.', + } + })) + .catch(e => errorEmail.textContent = e) + .finally(unlock) + } + } + + actionEmailEdit.onclick = () => { + CreateModal({ + behaviour: { + title: 'Update Email', + tooltip: 'Changing your email address will mark it as unverified.', + onSubmit: ({ email }) => new Promise(ok => { + SendRequest('PATCH', '/api/v1/users/@me/security/email', { email }, 2) + .then(() => window.location.reload()) + .catch(ok) + }) + }, + '_divider': { type: 'divider-line' }, + 'email': { + type: 'text', + required: true, + title: 'Email Address', + validator: TestEmailSuite, + } + }) + } + + // Password + actionPassChange.onclick = () => { + CreateModal({ + behaviour: { + title: 'Change Password', + tooltip: 'Enter your current password and new password.', + onSubmit: ({ old_password, new_password }) => new Promise(ok => { + SendRequest( + 'PATCH', '/api/v1/users/@me/security/password', + { old_password, new_password }, 1 + ) + .then(() => ok()) + .catch(ok) + }) + }, + '_link': { + type: 'link', + title: 'Forgot Password?', + href: '/password-reset', + }, + '_divider': { type: 'divider-line' }, + 'old_password': { + type: 'password', + required: true, + title: 'Old Password', + }, + 'new_password': { + type: 'password', + required: true, + title: 'New Password', + validator: (v, { new_password_again }) => { + if (v !== new_password_again) return 'Passwords do not Match' + return TestPasswordSuite(v) + }, + }, + 'new_password_again': { + type: 'password', + required: true, + title: 'New Password Again', + validator: (v, { new_password }) => { + if (v !== new_password) return 'Passwords do not Match' + return TestPasswordSuite(v) + }, + }, + }) + } + + // Multi-Factor + function RecoveryModal(codes) { + return CreateModal({ + behaviour: { + title: 'Recovery Codes', + tooltip: + 'Keep these recovery codes safe! ' + + 'You\'ll lose access to your account if both recovery codes and your authenticator app are lost.', + submitText: 'Download', + cancelText: 'Close', + onSubmit: () => { + // Generate Text + const filename = `Recovery Codes for ${body.username}.txt` + const filedata = + `Recovery Codes for ${body.username} (${body.email})\n` + + `Generated on ${new Date().toLocaleString()}\n` + + `------------\n` + + codes.map(c => `> ${c}\n`).join(''); + // Fake Download Element + const a = document.createElement('a') + a.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(filedata)) + a.setAttribute('download', filename) + a.style.display = 'none' + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + return ' ' + } + }, + '_codes': { + type: 'recovery-codes', + codes: codes + } + }) + } + if (body.mfa_enabled) { + actionMFASetupRemove.removeAttribute('hidden') + actionMFASetupRemove.onclick = async () => { + await CreateModal({ + behaviour: { + title: 'Remove Authenticator App?', + tooltip: 'This will disable MFA on your account.', + } + }) + const unlock = LockInteraction() + SendRequest('DELETE', '/api/v1/users/@me/security/mfa/setup', null, 2) + .then(() => window.location.reload()) + .catch(e => errorMFA.textContent = e) + .finally(unlock) + } + actionMFARecoveryView.removeAttribute('hidden') + actionMFARecoveryView.onclick = () => { + const unlock = LockInteraction() + SendRequest('GET', '/api/v1/users/@me/security/mfa/codes', null, 2) + .then(([_, codes]) => RecoveryModal(codes)) + .catch(e => errorMFA.textContent = e) + .finally(unlock) + } + actionMFARecoveryRegen.removeAttribute('hidden') + actionMFARecoveryRegen.onclick = async () => { + await CreateModal({ + behaviour: { + title: 'Reset Recovery Codes?', + tooltip: 'This will void the previously generated codes.', + } + }) + const unlock = LockInteraction() + SendRequest('DELETE', '/api/v1/users/@me/security/mfa/codes', null, 2) + .then(([_, codes]) => RecoveryModal(codes)) + .catch(e => errorMFA.textContent = e) + .finally(unlock) + } + } else { + actionMFASetup.removeAttribute('hidden') + actionMFASetup.onclick = () => { + const unlock = LockInteraction() + SendRequest('GET', '/api/v1/users/@me/security/mfa/setup', null, 1) + .then(([_, mfa]) => { + CreateModal({ + behaviour: { + title: 'Setup Authenticator App', + tooltip: 'Scan the QR code using your Authenticator App and enter the generated passcode.', + onSubmit: ({ passcode }) => new Promise(ok => { + const unlock = LockInteraction() + SendRequest('POST', '/api/v1/users/@me/security/mfa/setup', { passcode }, 1) + .then((/* 204 No Content */) => { + RecoveryModal(mfa.recovery_codes) + .finally(() => window.location.reload()) + ok() + }) + .catch(ok) + .finally(unlock) + }) + }, + '_spacer1': { type: 'divider-line' }, + '_qrcode': { + type: 'qr-code', + src: mfa.image, + }, + '_spacer2': { type: 'divider-line' }, + '_secret': { + type: 'text', + title: 'Can\'t Scan the QR Code? Use this secret key:', + value: mfa.secret, + readonly: true, + }, + passcode: { + required: true, + type: 'password', + title: 'Passcode', + autocomplete: 'one-time-code', + placeholder: '123456', + validator: v => { + if (!/[0-9]{6}/.test(v)) return 'Invalid Passcode' + return true + } + }, + }) + }) + .catch(e => errorMFA.textContent = e) + .finally(unlock) + } + } + + // Danger Zone + actionDangerDeleteAccount.onclick = async () => { + // Prompt Modal + await CreateModal({ + behaviour: { + title: 'Delete Account?', + tooltip: + 'This will start a countdown where afterwards your account and all of its data will be deleted. ' + + 'More details will be sent to your email address.', + } + }) + const unlock = LockInteraction() + SendRequest('DELETE', '/api/v1/users/@me', null, 2) + .then(() => SignOut()) + .catch(e => errorDanger.textContent = e) + .finally(unlock) + } + + // Display Menu + $('#security-wrapper').removeAttribute('hidden') + }) + .catch(e => { + // Display Error Message + $('#alert-error').removeAttribute('hidden') + $('#error-message').textContent = e + console.log(e) + }) + .finally(() => { + // Hide Loading Symbol + $('#alert-load').setAttribute('hidden', true) + }) + +})() \ No newline at end of file diff --git a/suzzy-v0-standalone/_todo/_scripts/login-index.js b/suzzy-v0-standalone/_todo/_scripts/login-index.js new file mode 100644 index 0000000..b690207 --- /dev/null +++ b/suzzy-v0-standalone/_todo/_scripts/login-index.js @@ -0,0 +1,67 @@ +let x = { + static: true, + submitText: 'Log In', + cancelText: '', + onSubmit: ({ email, password }) => new Promise(submitResult => { + const unlock = LockInteraction() + SendRequest('POST', '/api/v1/auth/login', { email, password }, 0, false) + .then(([resp, body]) => { + if (!resp.ok) { + // Display Custom Modal + switch (body.code) { + case 2000: // ERROR_MFA_EMAIL_SENT + return CreateModal({ + behaviour: { + cancelText: '', + title: 'Verify Login from a New Device', + tooltip: 'Check your email for a verification link to authorize access from this device.' + }, + }) + .catch(() => { /** Silence Modal Closing */ }) + case 2001: // ERROR_MFA_PASSCODE_REQUIRED + return CreateModal({ + 'behaviour': { + title: 'Passcode Required', + tooltip: 'Please enter your Authenticator Passcode or Recovery Code to log in.', + onSubmit: ({ passcode }) => new Promise(mfaResult => { + SendRequest('POST', '/api/v1/auth/login', { email, password, passcode }, 0) + .then(([_, body]) => { + // Store Credentials + localStorage.setItem('auth_token', body.token) + localStorage.setItem('auth_expires', Date.now() + (body.expires_in * 1000)) + FollowRedirect('/account/profile') + }) + .catch(mfaResult) + }) + }, + '_spacer': { + type: 'divider-line' + }, + 'passcode': { + required: true, + type: 'password', + title: 'Passcode', + autocomplete: 'one-time-code', + placeholder: '123456', + validator: v => { + if (!/([0-9]{6}|[A-z0-9]{8})/.test(v)) + return 'Invalid Passcode' + return true + } + } + }) + .catch(() => { /** Silence Modal Closing */ }) + default: + return submitResult(body.error || body) + } + } else { + // Store Credentials + localStorage.setItem('auth_token', body.token) + localStorage.setItem('auth_expires', Date.now() + (body.expires_in * 1000)) + FollowRedirect('/account/profile') + } + }) + .catch(submitResult) + .finally(unlock) + }), +} \ No newline at end of file diff --git a/suzzy-v0-standalone/_todo/_scripts/login-oauth2.js b/suzzy-v0-standalone/_todo/_scripts/login-oauth2.js new file mode 100644 index 0000000..a0fea61 --- /dev/null +++ b/suzzy-v0-standalone/_todo/_scripts/login-oauth2.js @@ -0,0 +1,78 @@ +(() => { + // Fetch Application and Profile + const href = window.location.href + const query = href.slice(href.indexOf('?') + 1) + SendRequest('GET', '/api/v1/oauth2/authorize', query, 1) + .then(([_, body]) => { + const + me = body.user, + app = body.application, + ScopesParent = $('#content-scopes'), + imageUser = $('#image-user'), + imageApp = $('#image-app') + + // Set Avatars + imageUser.src = ImageURL('avatars', me.id, me.avatar, false, 128) + imageApp.src = ImageURL('icons', app.id, app.icon, false, 128) + imageUser.alt = `Avatar for '${me.username}'` + imageApp.alt = `Icon for Application '${app.name}'` + + // Fill Template + $('#content-login').textContent = `Logged in as ${me.username}, not you?` + $('#content-aname').textContent = `Connect ${app.name}?` + $('#content-redirect').textContent += body.redirect + $('#content-created').textContent += (() => { + const t = new Date(app.created) + const m = ['January', 'Feburary', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][t.getMonth()] + const d = t.getDate() + const y = t.getFullYear() + return `${m} ${d}${d === 1 ? 'st' : d === 2 ? 'nd' : d === 3 ? 'rd' : 'th'}, ${y}` + })() + _Scopes.forEach(s => { + if ((body.scopes & s[0]) !== 0) { + const div = document.createElement('div') + div.classList.add('scope') + const dot = document.createElement('div') + dot.classList.add('dot-green') + const txt = document.createElement('p') + txt.textContent = s[1] + div.append(dot, txt) + ScopesParent.appendChild(div) + } + }) + + // Cancel Request, go to application with error... + $('#action-cancel').onclick = (ev) => { + if (!ev.isTrusted) return + const url = new URL(body.redirect) + url.searchParams.set('error', 'access_denied') + url.searchParams.set('error_description', 'User Declined') + window.location.assign(url.toString()) + } + + // Complete Request, go to application with code... + const actionError = $('#action-error') + $('#action-authorize').onclick = (ev) => { + if (!ev.isTrusted) return + const unlock = LockInteraction(true) + actionError.textContent = '' + SendRequest('POST', '/api/v1/oauth2/authorize', query, 1) + .then(([_, body]) => window.location.assign(body.location)) + .catch(e => { + actionError.textContent = e.error || e + console.log(e) + }) + .finally(() => unlock()) + } + + $('#oauth2-flow').removeAttribute('hidden') + }) + .catch(e => { + $('#oauth2-fail').removeAttribute('hidden') + $('#fail-reason').textContent = e.error || e + console.log(e) + }) + .finally(() => { + $('#oauth2-load').setAttribute('hidden', true) + }) +})() \ No newline at end of file diff --git a/suzzy-v0-standalone/_todo/_styles/ui-account.css b/suzzy-v0-standalone/_todo/_styles/ui-account.css new file mode 100644 index 0000000..d68523a --- /dev/null +++ b/suzzy-v0-standalone/_todo/_styles/ui-account.css @@ -0,0 +1,389 @@ +/* Collections */ +div.collection-wrapper { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +div.collection-item { + width: 100%; + height: fit-content; + border-radius: 8px; + overflow: hidden; + background-color: var(--background-layer-4); + border: 2px solid var(--background-layer-1); + box-sizing: border-box; + padding: 16px; + gap: 8px; +} + +div.text-box { + background-color: var(--background-layer-5); + box-sizing: border-box; + border-radius: 8px; + padding: 8px; +} + +div.collection-message { + width: 100%; + padding: 32px; + text-align: center; + box-sizing: border-box; +} + +div.collection-message img, +div.collection-message i { + fill: var(--background-layer-0); + height: 48px; + width: 48px; +} + + +/* Page: Devices */ +div.item-device { + display: flex; + align-items: center; +} + +div.device-icon { + min-height: 72px; + min-width: 72px; + padding: 8px; + border-radius: 100%; + box-sizing: border-box; + background-color: var(--background-layer-4); + border: 2px solid var(--background-layer-1); + display: flex; + align-items: center; + justify-content: center; +} + +div.device-icon img { + fill: var(--background-layer-0); + height: 40px; + width: 40px; +} + +div.device-info { + flex-basis: 100%; +} + +div.info-alongside { + display: flex; + flex-wrap: wrap; + align-items: center; + overflow: hidden; + gap: 8px; +} + +button.device-action { + fill: var(--background-layer-1); + height: 24px; + width: 24px; + background-color: transparent; + cursor: pointer; + border: none; + transition: var(--transition-time) ease-in-out fill; +} + +button.device-action:hover, +button.device-action:focus-visible { + fill: var(--text-error-1); +} + +/* Page: Connections */ +div.application-header { + display: flex; + align-items: self-start; + justify-content: space-between; + gap: 8px; +} + +img.application-icon { + border-radius: 100%; + box-sizing: border-box; + border: 4px solid var(--background-layer-1); + background-color: var(--background-layer-4); + min-height: 64px; + max-height: 64px; + min-width: 64px; + max-width: 64px; + opacity: 0; + background-size: contain; + transition: var(--transition-time) ease-in-out opacity; +} + +div.application-info { + flex-basis: 100%; + overflow: hidden; +} + +div.application-extended { + display: grid; + gap: 8px; + transition: var(--transition-time) ease-in-out all; + height: -8px; +} + +div.scopes-wrapper { + display: grid; +} + +div.scope { + display: flex; + gap: 4px; + align-items: center; +} + +div.dot-green { + width: 8px; + height: 8px; + border-radius: 100%; + background-color: #42f495; +} + +/* Page: Profile */ +div.profile-wrapper { + display: flex; +} + +div.profile-options, +div.profile-container { + flex-basis: 50%; + box-sizing: border-box; + padding: 4px; +} + +div.profile-options { + display: grid; + gap: 8px; +} + +div.widget-container { + display: flex; + flex-wrap: wrap; + justify-content: space-evenly; + align-items: flex-start +} + +div.widget-color { + display: grid; + place-items: center; +} + +input[type='color']::-webkit-color-swatch-wrapper { + padding: 0; +} + +input.color-preview { + cursor: pointer; + border-radius: 8px; + background-color: white; + height: 48px; + width: 96px; + border: 2px solid var(--background-layer-2); + transition: var(--transition-time) ease-in-out all; +} + +div.profile-container { + min-width: 350px; + height: fit-content; + background-color: var(--accent-background); + border: 4px solid var(--accent-border); + box-sizing: border-box; + border-radius: 8px; + overflow: hidden; + margin: 16px; + padding: 0; +} + +#profile-banner { + width: 100%; + height: 128px; + border-bottom: 4px solid var(--accent-border); + background-color: var(--accent-banner); + background-position: center; + background-size: cover; +} + +div.profile-header { + display: flex; + justify-content: space-between; + box-sizing: border-box; + padding: 8px 8px 0 8px; +} + +#profile-avatar { + width: 96px; + height: 96px; + margin-top: -60px; + border-radius: 100%; + border: 4px solid var(--accent-border); + background-color: var(--accent-banner); + object-fit: cover; +} + +#profile-badges { + height: fit-content; + width: fit-content; + display: flex; + flex-wrap: wrap; + background-color: rgb(0, 0, 0, 0.4); + border-radius: 8px; + padding: 8px; + gap: 4px; +} + +#profile-badges:empty { + display: none; +} + +#profile-badges img { + height: 20px; + width: 20px; +} + +div.profile-about { + background-color: rgba(0, 0, 0, 0.4); + margin: 8px; + padding: 8px; + border-radius: 8px; + box-sizing: border-box; +} + +div.profile-about p, +div.profile-about pre { + transition: var(--transition-time) ease-in-out color; + max-height: 300px; + overflow: hidden; + text-wrap: balance; +} + +div.profile-dark p, +div.profile-dark pre { + color: var(--text-layer-2); +} + +#profile-displayname { + font-size: large; +} + +#profile-username { + font-size: small; + opacity: 0.80; +} + +#profile-divider { + background-color: var(--accent-border); + opacity: 0.5; +} + +/* Page: Applications */ +div.application-options { + display: grid; + gap: 8px; +} + +div.options-hidden { + height: 0; + overflow: hidden; +} + +div.input-redirect { + display: flex; + gap: 8px; +} + +div.input-redirect button { + min-width: fit-content; + height: 100%; +} + +div#container-redirects { + display: grid; + gap: 8px; +} + +/* Mobile Styling */ +@media (max-width: 1024px) { + div.main-content { + border-right: none; + } +} + +@media (max-width: 640px) { + + div.main-content { + border: none; + background-color: transparent; + } + + div.main-wrapper { + display: block; + } + + div.main-wrapper div.divider-line { + display: block !important; + } + + a.navigation-link { + font-size: large; + } + + div.main-aside { + width: 100%; + max-width: 100%; + } + + div.main-content { + width: 100%; + height: fit-content; + min-height: 100vh; + max-width: 100%; + } + + div.collection-item { + background-color: var(--background-layer-4); + border: 2px solid var(--background-layer-3); + } + + div.device-icon { + background-color: var(--background-layer-4); + border: 2px solid var(--background-layer-1); + } + + /* Page: Connections */ + div.application-header { + display: block; + } + + div.application-header button { + margin: 8px 0; + width: 100%; + } + + div.text-box { + background-color: var(--background-layer-3); + } + + /* Page: Devices */ + button.device-action { + height: 40px; + width: 40px; + } + + div.info-alongside { + text-wrap: balance; + } + + /* Page: Profile */ + div.profile-wrapper { + display: block; + } + + div.profile-container { + min-width: 0px; + margin: 32px 0 0 0; + } + +} \ No newline at end of file diff --git a/suzzy-v0-standalone/_todo/public/Users-Profile.pug b/suzzy-v0-standalone/_todo/public/Users-Profile.pug new file mode 100644 index 0000000..0470511 --- /dev/null +++ b/suzzy-v0-standalone/_todo/public/Users-Profile.pug @@ -0,0 +1,73 @@ +//- redo with connection to profiles database +extends _Template.pug +block pageStyles + link(rel='stylesheet' href=`/assets/styles/ui-public-profile.css?v=${cachev}`) +block pageScripts + script(src=`/assets/scripts/public-profile.js?v=${cachev}`) +block pageHeader + meta(property='og:image' content='https://suzzygames.com/assets/images/og-logo.png') + meta(property='og:title' content=`profile - ${username}`) + meta(property='og:description' content=`View ${username}'s profile on suzzy games`) + meta(name='description' content=`View ${username}'s profile on suzzy games`) + title profile - #{username} +block pageContent + noscript + style. + noscript { + display: grid; + box-sizing: border-box; + place-items: center; + align-content: center; + } + #alert-load { + display: none + } + div(class='collection-message') + img(aria-hidden='true' src='/assets/images/icons/icon-cross.svg' alt='Icon for Cross') + p This page requires JavaScript to be enabled. + + div(class='collection-message' id='alert-load') + img(src='/assets/images/icons/diagram-loading.svg' alt='Icon for Spinner') + p Loading Profile + div(class='collection-message' id='alert-error' hidden) + img(src='/assets/images/icons/icon-cross.svg' alt='Icon for Cross') + p(id='alert-error-message') An Error has Occurred + + div(class='main-wrapper') + //- Profile + div(class='main-aside' hidden) + div(class='profile-container') + div(id='profile-banner') + div(class='profile-header') + img(id='profile-avatar' alt='My Avatar') + div(id='profile-badges') + div(class='profile-about text-nopad') + div(class='info-alongside') + p(id='profile-displayname') + p(id='profile-username') + p(id='profile-subtitle' class='text-tip') + div(class='divider-line' id='profile-divider' hidden) + pre(id='profile-bio' class='text-tip') + //- Categories + div(class='divider-spacer') + p(class='text-header') Categories + button(class='category category-active') + img(alt='Icon for Blog Post' src='/assets/blog/category-any.png') + p(class='category-name') None + p(class='category-count text-muted') (x) + //- button(class='category' id='category-posts') + //- img(alt='Icon for Blog Post' src='/assets/blog/category-news.png') + //- p(class='category-name') Blog Posts + //- p(class='category-count text-muted') (1) + //- button(class='category' id='category-friends') + //- img(alt='Icon for Blog Post' src='/assets/blog/category-friends.png') + //- p(class='category-name') Friends + //- p(class='category-count text-muted') (0) + + div(class='divider-dashed' hidden) + + //- Content + div(class='main-content' role='main' hidden) + div(class='collection-message') + img(src='/assets/images/icons/icon-filter.svg' alt='Icon for Filter') + p(class='text-muted') Please select a category \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/audio/picross_1.mp3 b/suzzy-v0-standalone/assets/audio/picross_1.mp3 new file mode 100644 index 0000000..3c3e243 Binary files /dev/null and b/suzzy-v0-standalone/assets/audio/picross_1.mp3 differ diff --git a/suzzy-v0-standalone/assets/audio/picross_2.mp3 b/suzzy-v0-standalone/assets/audio/picross_2.mp3 new file mode 100644 index 0000000..53d3c8c Binary files /dev/null and b/suzzy-v0-standalone/assets/audio/picross_2.mp3 differ diff --git a/suzzy-v0-standalone/assets/blog/author-bakonpancakz.png b/suzzy-v0-standalone/assets/blog/author-bakonpancakz.png new file mode 100644 index 0000000..7e530dc Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/author-bakonpancakz.png differ diff --git a/suzzy-v0-standalone/assets/blog/category-any.png b/suzzy-v0-standalone/assets/blog/category-any.png new file mode 100644 index 0000000..8bd128f Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/category-any.png differ diff --git a/suzzy-v0-standalone/assets/blog/category-bts.png b/suzzy-v0-standalone/assets/blog/category-bts.png new file mode 100644 index 0000000..6a8f3e1 Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/category-bts.png differ diff --git a/suzzy-v0-standalone/assets/blog/category-pm.png b/suzzy-v0-standalone/assets/blog/category-pm.png new file mode 100644 index 0000000..e9ff70c Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/category-pm.png differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Options_InGame.mp3 b/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Options_InGame.mp3 new file mode 100644 index 0000000..d0eba0d Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Options_InGame.mp3 differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Options_Original.mp3 b/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Options_Original.mp3 new file mode 100644 index 0000000..3b05f9b Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Options_Original.mp3 differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Stage_1.mp3 b/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Stage_1.mp3 new file mode 100644 index 0000000..b726bbd Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Stage_1.mp3 differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Stage_2.mp3 b/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Stage_2.mp3 new file mode 100644 index 0000000..44d82a6 Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Stage_2.mp3 differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Stage_3.mp3 b/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Stage_3.mp3 new file mode 100644 index 0000000..03ad0af Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Stage_3.mp3 differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Stage_4.mp3 b/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Stage_4.mp3 new file mode 100644 index 0000000..0b5df4a Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/FNAK_Stage_4.mp3 differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/ProjectMonke_Settings.mp3 b/suzzy-v0-standalone/assets/blog/hello-world/ProjectMonke_Settings.mp3 new file mode 100644 index 0000000..f1b5e80 Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/ProjectMonke_Settings.mp3 differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/ProjectZ-Cover.jpeg b/suzzy-v0-standalone/assets/blog/hello-world/ProjectZ-Cover.jpeg new file mode 100644 index 0000000..b454f25 Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/ProjectZ-Cover.jpeg differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/ProjectZ.mp4 b/suzzy-v0-standalone/assets/blog/hello-world/ProjectZ.mp4 new file mode 100644 index 0000000..3ca61c5 Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/ProjectZ.mp4 differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/ProjectZ.webm b/suzzy-v0-standalone/assets/blog/hello-world/ProjectZ.webm new file mode 100644 index 0000000..06078e7 Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/ProjectZ.webm differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/banner.gif b/suzzy-v0-standalone/assets/blog/hello-world/banner.gif new file mode 100644 index 0000000..aaf2eef Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/banner.gif differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/craf-1.jpeg b/suzzy-v0-standalone/assets/blog/hello-world/craf-1.jpeg new file mode 100644 index 0000000..6eb9e13 Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/craf-1.jpeg differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/craf-2.jpeg b/suzzy-v0-standalone/assets/blog/hello-world/craf-2.jpeg new file mode 100644 index 0000000..da23b4b Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/craf-2.jpeg differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/fnak-1.jpeg b/suzzy-v0-standalone/assets/blog/hello-world/fnak-1.jpeg new file mode 100644 index 0000000..801b6ab Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/fnak-1.jpeg differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/fnak-2.jpeg b/suzzy-v0-standalone/assets/blog/hello-world/fnak-2.jpeg new file mode 100644 index 0000000..376e93f Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/fnak-2.jpeg differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/fnak-3.jpeg b/suzzy-v0-standalone/assets/blog/hello-world/fnak-3.jpeg new file mode 100644 index 0000000..02c622d Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/fnak-3.jpeg differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/peak-performance.jpeg b/suzzy-v0-standalone/assets/blog/hello-world/peak-performance.jpeg new file mode 100644 index 0000000..0adb452 Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/peak-performance.jpeg differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/settings-Cover.jpeg b/suzzy-v0-standalone/assets/blog/hello-world/settings-Cover.jpeg new file mode 100644 index 0000000..c43da2d Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/settings-Cover.jpeg differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/settings.mp4 b/suzzy-v0-standalone/assets/blog/hello-world/settings.mp4 new file mode 100644 index 0000000..0e665d9 Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/settings.mp4 differ diff --git a/suzzy-v0-standalone/assets/blog/hello-world/settings.webm b/suzzy-v0-standalone/assets/blog/hello-world/settings.webm new file mode 100644 index 0000000..f601fe9 Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/hello-world/settings.webm differ diff --git a/suzzy-v0-standalone/assets/blog/repo-of-knowledge/BECCA2D_TEASER.mp4 b/suzzy-v0-standalone/assets/blog/repo-of-knowledge/BECCA2D_TEASER.mp4 new file mode 100644 index 0000000..668d92a Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/repo-of-knowledge/BECCA2D_TEASER.mp4 differ diff --git a/suzzy-v0-standalone/assets/blog/repo-of-knowledge/BECCA2D_TEASER_COVER.png b/suzzy-v0-standalone/assets/blog/repo-of-knowledge/BECCA2D_TEASER_COVER.png new file mode 100644 index 0000000..6e2df0f Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/repo-of-knowledge/BECCA2D_TEASER_COVER.png differ diff --git a/suzzy-v0-standalone/assets/blog/repo-of-knowledge/LIBRARY_SHOWCASE.png b/suzzy-v0-standalone/assets/blog/repo-of-knowledge/LIBRARY_SHOWCASE.png new file mode 100644 index 0000000..43b6270 Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/repo-of-knowledge/LIBRARY_SHOWCASE.png differ diff --git a/suzzy-v0-standalone/assets/blog/repo-of-knowledge/OAUTH2_SHOWCASE.png b/suzzy-v0-standalone/assets/blog/repo-of-knowledge/OAUTH2_SHOWCASE.png new file mode 100644 index 0000000..7677cde Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/repo-of-knowledge/OAUTH2_SHOWCASE.png differ diff --git a/suzzy-v0-standalone/assets/blog/repo-of-knowledge/SITE_SHOWCASE.jpg b/suzzy-v0-standalone/assets/blog/repo-of-knowledge/SITE_SHOWCASE.jpg new file mode 100644 index 0000000..d8bad4e Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/repo-of-knowledge/SITE_SHOWCASE.jpg differ diff --git a/suzzy-v0-standalone/assets/blog/repo-of-knowledge/banner.png b/suzzy-v0-standalone/assets/blog/repo-of-knowledge/banner.png new file mode 100644 index 0000000..6b2347d Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/repo-of-knowledge/banner.png differ diff --git a/suzzy-v0-standalone/assets/blog/repo-of-knowledge/conclusion.png b/suzzy-v0-standalone/assets/blog/repo-of-knowledge/conclusion.png new file mode 100644 index 0000000..47bb061 Binary files /dev/null and b/suzzy-v0-standalone/assets/blog/repo-of-knowledge/conclusion.png differ diff --git a/suzzy-v0-standalone/assets/email/background.png b/suzzy-v0-standalone/assets/email/background.png new file mode 100644 index 0000000..d2daf0c Binary files /dev/null and b/suzzy-v0-standalone/assets/email/background.png differ diff --git a/suzzy-v0-standalone/assets/email/bimi.svg b/suzzy-v0-standalone/assets/email/bimi.svg new file mode 100644 index 0000000..de00be7 --- /dev/null +++ b/suzzy-v0-standalone/assets/email/bimi.svg @@ -0,0 +1,24 @@ + + + + + + diff --git a/suzzy-v0-standalone/assets/email/logo.gif b/suzzy-v0-standalone/assets/email/logo.gif new file mode 100644 index 0000000..ef2ca8e Binary files /dev/null and b/suzzy-v0-standalone/assets/email/logo.gif differ diff --git a/suzzy-v0-standalone/assets/fonts/font-poppins.css b/suzzy-v0-standalone/assets/fonts/font-poppins.css new file mode 100644 index 0000000..fd7454d --- /dev/null +++ b/suzzy-v0-standalone/assets/fonts/font-poppins.css @@ -0,0 +1,59 @@ +/* devanagari */ +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('file://fonts/pxiEyp8kv8JHgFVrJJbecmNE.woff2') format('woff2'); + unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09; +} + +/* latin-ext */ +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('file://fonts/pxiEyp8kv8JHgFVrJJnecmNE.woff2') format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* latin */ +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('file://fonts/pxiEyp8kv8JHgFVrJJfecg.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* devanagari */ +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('file://fonts/pxiByp8kv8JHgFVrLEj6Z11lFc-K.woff2') format('woff2'); + unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09; +} + +/* latin-ext */ +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('file://fonts/pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2') format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* latin */ +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('file://fonts/pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/fonts/pxiByp8kv8JHgFVrLEj6Z11lFc-K.woff2 b/suzzy-v0-standalone/assets/fonts/pxiByp8kv8JHgFVrLEj6Z11lFc-K.woff2 new file mode 100644 index 0000000..354d0a4 Binary files /dev/null and b/suzzy-v0-standalone/assets/fonts/pxiByp8kv8JHgFVrLEj6Z11lFc-K.woff2 differ diff --git a/suzzy-v0-standalone/assets/fonts/pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2 b/suzzy-v0-standalone/assets/fonts/pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2 new file mode 100644 index 0000000..644e515 Binary files /dev/null and b/suzzy-v0-standalone/assets/fonts/pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2 differ diff --git a/suzzy-v0-standalone/assets/fonts/pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2 b/suzzy-v0-standalone/assets/fonts/pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2 new file mode 100644 index 0000000..921e962 Binary files /dev/null and b/suzzy-v0-standalone/assets/fonts/pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2 differ diff --git a/suzzy-v0-standalone/assets/fonts/pxiEyp8kv8JHgFVrJJbecmNE.woff2 b/suzzy-v0-standalone/assets/fonts/pxiEyp8kv8JHgFVrJJbecmNE.woff2 new file mode 100644 index 0000000..3295a4c Binary files /dev/null and b/suzzy-v0-standalone/assets/fonts/pxiEyp8kv8JHgFVrJJbecmNE.woff2 differ diff --git a/suzzy-v0-standalone/assets/fonts/pxiEyp8kv8JHgFVrJJfecg.woff2 b/suzzy-v0-standalone/assets/fonts/pxiEyp8kv8JHgFVrJJfecg.woff2 new file mode 100644 index 0000000..b69e009 Binary files /dev/null and b/suzzy-v0-standalone/assets/fonts/pxiEyp8kv8JHgFVrJJfecg.woff2 differ diff --git a/suzzy-v0-standalone/assets/fonts/pxiEyp8kv8JHgFVrJJnecmNE.woff2 b/suzzy-v0-standalone/assets/fonts/pxiEyp8kv8JHgFVrJJnecmNE.woff2 new file mode 100644 index 0000000..c871a3e Binary files /dev/null and b/suzzy-v0-standalone/assets/fonts/pxiEyp8kv8JHgFVrJJnecmNE.woff2 differ diff --git a/suzzy-v0-standalone/assets/icons/badge-crown.svg b/suzzy-v0-standalone/assets/icons/badge-crown.svg new file mode 100644 index 0000000..caf9cde --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/badge-crown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/badge-picross.svg b/suzzy-v0-standalone/assets/icons/badge-picross.svg new file mode 100644 index 0000000..364b385 --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/badge-picross.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/badge-star.svg b/suzzy-v0-standalone/assets/icons/badge-star.svg new file mode 100644 index 0000000..5109135 --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/badge-star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/diagram-connection.svg b/suzzy-v0-standalone/assets/icons/diagram-connection.svg new file mode 100644 index 0000000..176ca2e --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/diagram-connection.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/diagram-loading.svg b/suzzy-v0-standalone/assets/icons/diagram-loading.svg new file mode 100644 index 0000000..47097d9 --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/diagram-loading.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-app-none.svg b/suzzy-v0-standalone/assets/icons/icon-app-none.svg new file mode 100644 index 0000000..4cf13c8 --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-app-none.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-app.svg b/suzzy-v0-standalone/assets/icons/icon-app.svg new file mode 100644 index 0000000..ab4223d --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-app.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-computer.svg b/suzzy-v0-standalone/assets/icons/icon-computer.svg new file mode 100644 index 0000000..b3a9e11 --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-computer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-cross.svg b/suzzy-v0-standalone/assets/icons/icon-cross.svg new file mode 100644 index 0000000..ebad51f --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-cross.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-delete.svg b/suzzy-v0-standalone/assets/icons/icon-delete.svg new file mode 100644 index 0000000..dcb078a --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-desktop.svg b/suzzy-v0-standalone/assets/icons/icon-desktop.svg new file mode 100644 index 0000000..a30d4b0 --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-desktop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-download.svg b/suzzy-v0-standalone/assets/icons/icon-download.svg new file mode 100644 index 0000000..6fbd681 --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-exit.svg b/suzzy-v0-standalone/assets/icons/icon-exit.svg new file mode 100644 index 0000000..86b35b1 --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-exit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-filter.svg b/suzzy-v0-standalone/assets/icons/icon-filter.svg new file mode 100644 index 0000000..3082234 --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-filter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-lock.svg b/suzzy-v0-standalone/assets/icons/icon-lock.svg new file mode 100644 index 0000000..e8662a9 --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-mobile.svg b/suzzy-v0-standalone/assets/icons/icon-mobile.svg new file mode 100644 index 0000000..59b0030 --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-mobile.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-pause.svg b/suzzy-v0-standalone/assets/icons/icon-pause.svg new file mode 100644 index 0000000..4a4edf9 --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-play.svg b/suzzy-v0-standalone/assets/icons/icon-play.svg new file mode 100644 index 0000000..900ec42 --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-plug-socket.svg b/suzzy-v0-standalone/assets/icons/icon-plug-socket.svg new file mode 100644 index 0000000..d93f718 --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-plug-socket.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-plug.svg b/suzzy-v0-standalone/assets/icons/icon-plug.svg new file mode 100644 index 0000000..c85c668 --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-plug.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-star.svg b/suzzy-v0-standalone/assets/icons/icon-star.svg new file mode 100644 index 0000000..85b9280 --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-user.svg b/suzzy-v0-standalone/assets/icons/icon-user.svg new file mode 100644 index 0000000..ca2573a --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/icons/icon-volume.svg b/suzzy-v0-standalone/assets/icons/icon-volume.svg new file mode 100644 index 0000000..174e9fb --- /dev/null +++ b/suzzy-v0-standalone/assets/icons/icon-volume.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/images/background-blog.png b/suzzy-v0-standalone/assets/images/background-blog.png new file mode 100644 index 0000000..5a087a8 Binary files /dev/null and b/suzzy-v0-standalone/assets/images/background-blog.png differ diff --git a/suzzy-v0-standalone/assets/images/background-cat.png b/suzzy-v0-standalone/assets/images/background-cat.png new file mode 100644 index 0000000..d6fe27f Binary files /dev/null and b/suzzy-v0-standalone/assets/images/background-cat.png differ diff --git a/suzzy-v0-standalone/assets/images/background-home.png b/suzzy-v0-standalone/assets/images/background-home.png new file mode 100644 index 0000000..a7e6b2b Binary files /dev/null and b/suzzy-v0-standalone/assets/images/background-home.png differ diff --git a/suzzy-v0-standalone/assets/images/brands/logo-discord.png b/suzzy-v0-standalone/assets/images/brands/logo-discord.png new file mode 100644 index 0000000..dbb3553 Binary files /dev/null and b/suzzy-v0-standalone/assets/images/brands/logo-discord.png differ diff --git a/suzzy-v0-standalone/assets/images/brands/logo-roblox.png b/suzzy-v0-standalone/assets/images/brands/logo-roblox.png new file mode 100644 index 0000000..e0ebd4d Binary files /dev/null and b/suzzy-v0-standalone/assets/images/brands/logo-roblox.png differ diff --git a/suzzy-v0-standalone/assets/images/brands/logo-x.png b/suzzy-v0-standalone/assets/images/brands/logo-x.png new file mode 100644 index 0000000..3b797ba Binary files /dev/null and b/suzzy-v0-standalone/assets/images/brands/logo-x.png differ diff --git a/suzzy-v0-standalone/assets/images/default-anon.png b/suzzy-v0-standalone/assets/images/default-anon.png new file mode 100644 index 0000000..ef95d9b Binary files /dev/null and b/suzzy-v0-standalone/assets/images/default-anon.png differ diff --git a/suzzy-v0-standalone/assets/images/default-icon.png b/suzzy-v0-standalone/assets/images/default-icon.png new file mode 100644 index 0000000..6b6b59d Binary files /dev/null and b/suzzy-v0-standalone/assets/images/default-icon.png differ diff --git a/suzzy-v0-standalone/assets/images/default-user0.png b/suzzy-v0-standalone/assets/images/default-user0.png new file mode 100644 index 0000000..50a2b5e Binary files /dev/null and b/suzzy-v0-standalone/assets/images/default-user0.png differ diff --git a/suzzy-v0-standalone/assets/images/default-user1.png b/suzzy-v0-standalone/assets/images/default-user1.png new file mode 100644 index 0000000..a20ac29 Binary files /dev/null and b/suzzy-v0-standalone/assets/images/default-user1.png differ diff --git a/suzzy-v0-standalone/assets/images/default-user2.png b/suzzy-v0-standalone/assets/images/default-user2.png new file mode 100644 index 0000000..d00b340 Binary files /dev/null and b/suzzy-v0-standalone/assets/images/default-user2.png differ diff --git a/suzzy-v0-standalone/assets/images/default-user3.png b/suzzy-v0-standalone/assets/images/default-user3.png new file mode 100644 index 0000000..0ef4689 Binary files /dev/null and b/suzzy-v0-standalone/assets/images/default-user3.png differ diff --git a/suzzy-v0-standalone/assets/images/default-user4.png b/suzzy-v0-standalone/assets/images/default-user4.png new file mode 100644 index 0000000..8cce22c Binary files /dev/null and b/suzzy-v0-standalone/assets/images/default-user4.png differ diff --git a/suzzy-v0-standalone/assets/images/default-user5.png b/suzzy-v0-standalone/assets/images/default-user5.png new file mode 100644 index 0000000..e7b4184 Binary files /dev/null and b/suzzy-v0-standalone/assets/images/default-user5.png differ diff --git a/suzzy-v0-standalone/assets/images/favicon.png b/suzzy-v0-standalone/assets/images/favicon.png new file mode 100644 index 0000000..2f2c348 Binary files /dev/null and b/suzzy-v0-standalone/assets/images/favicon.png differ diff --git a/suzzy-v0-standalone/assets/images/login-globe.png b/suzzy-v0-standalone/assets/images/login-globe.png new file mode 100644 index 0000000..bc306e9 Binary files /dev/null and b/suzzy-v0-standalone/assets/images/login-globe.png differ diff --git a/suzzy-v0-standalone/assets/images/public-footer.gif b/suzzy-v0-standalone/assets/images/public-footer.gif new file mode 100644 index 0000000..7319fb8 Binary files /dev/null and b/suzzy-v0-standalone/assets/images/public-footer.gif differ diff --git a/suzzy-v0-standalone/assets/images/public-logo-og.png b/suzzy-v0-standalone/assets/images/public-logo-og.png new file mode 100644 index 0000000..d469a45 Binary files /dev/null and b/suzzy-v0-standalone/assets/images/public-logo-og.png differ diff --git a/suzzy-v0-standalone/assets/images/public-logo.png b/suzzy-v0-standalone/assets/images/public-logo.png new file mode 100644 index 0000000..a18d15e Binary files /dev/null and b/suzzy-v0-standalone/assets/images/public-logo.png differ diff --git a/suzzy-v0-standalone/assets/images/public-nopage.png b/suzzy-v0-standalone/assets/images/public-nopage.png new file mode 100644 index 0000000..287161a Binary files /dev/null and b/suzzy-v0-standalone/assets/images/public-nopage.png differ diff --git a/suzzy-v0-standalone/assets/images/public-noposts.png b/suzzy-v0-standalone/assets/images/public-noposts.png new file mode 100644 index 0000000..f9b07f6 Binary files /dev/null and b/suzzy-v0-standalone/assets/images/public-noposts.png differ diff --git a/suzzy-v0-standalone/assets/library/background-navigation.png b/suzzy-v0-standalone/assets/library/background-navigation.png new file mode 100644 index 0000000..ac37151 Binary files /dev/null and b/suzzy-v0-standalone/assets/library/background-navigation.png differ diff --git a/suzzy-v0-standalone/assets/library/branding/becca-logo.svg b/suzzy-v0-standalone/assets/library/branding/becca-logo.svg new file mode 100644 index 0000000..d3ab8ef --- /dev/null +++ b/suzzy-v0-standalone/assets/library/branding/becca-logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/library/branding/craf-gameicon-1.png b/suzzy-v0-standalone/assets/library/branding/craf-gameicon-1.png new file mode 100644 index 0000000..2f26fc7 Binary files /dev/null and b/suzzy-v0-standalone/assets/library/branding/craf-gameicon-1.png differ diff --git a/suzzy-v0-standalone/assets/library/branding/craf-gameicon-2.png b/suzzy-v0-standalone/assets/library/branding/craf-gameicon-2.png new file mode 100644 index 0000000..c65be09 Binary files /dev/null and b/suzzy-v0-standalone/assets/library/branding/craf-gameicon-2.png differ diff --git a/suzzy-v0-standalone/assets/library/branding/craf-promo-1.png b/suzzy-v0-standalone/assets/library/branding/craf-promo-1.png new file mode 100644 index 0000000..29eeea7 Binary files /dev/null and b/suzzy-v0-standalone/assets/library/branding/craf-promo-1.png differ diff --git a/suzzy-v0-standalone/assets/library/branding/craf-promo-2.png b/suzzy-v0-standalone/assets/library/branding/craf-promo-2.png new file mode 100644 index 0000000..c212706 Binary files /dev/null and b/suzzy-v0-standalone/assets/library/branding/craf-promo-2.png differ diff --git a/suzzy-v0-standalone/assets/library/branding/monke-gameicon-initials.png b/suzzy-v0-standalone/assets/library/branding/monke-gameicon-initials.png new file mode 100644 index 0000000..8c0321f Binary files /dev/null and b/suzzy-v0-standalone/assets/library/branding/monke-gameicon-initials.png differ diff --git a/suzzy-v0-standalone/assets/library/branding/monke-gameicon.png b/suzzy-v0-standalone/assets/library/branding/monke-gameicon.png new file mode 100644 index 0000000..a134e6d Binary files /dev/null and b/suzzy-v0-standalone/assets/library/branding/monke-gameicon.png differ diff --git a/suzzy-v0-standalone/assets/library/branding/monke-logo-initials.png b/suzzy-v0-standalone/assets/library/branding/monke-logo-initials.png new file mode 100644 index 0000000..686ba18 Binary files /dev/null and b/suzzy-v0-standalone/assets/library/branding/monke-logo-initials.png differ diff --git a/suzzy-v0-standalone/assets/library/branding/monke-logo-text.png b/suzzy-v0-standalone/assets/library/branding/monke-logo-text.png new file mode 100644 index 0000000..70e9375 Binary files /dev/null and b/suzzy-v0-standalone/assets/library/branding/monke-logo-text.png differ diff --git a/suzzy-v0-standalone/assets/library/branding/monke-promo-1.png b/suzzy-v0-standalone/assets/library/branding/monke-promo-1.png new file mode 100644 index 0000000..0ee4d51 Binary files /dev/null and b/suzzy-v0-standalone/assets/library/branding/monke-promo-1.png differ diff --git a/suzzy-v0-standalone/assets/library/branding/suzzy-icon.png b/suzzy-v0-standalone/assets/library/branding/suzzy-icon.png new file mode 100644 index 0000000..0b7e465 Binary files /dev/null and b/suzzy-v0-standalone/assets/library/branding/suzzy-icon.png differ diff --git a/suzzy-v0-standalone/assets/library/branding/suzzy-logo.png b/suzzy-v0-standalone/assets/library/branding/suzzy-logo.png new file mode 100644 index 0000000..222a154 Binary files /dev/null and b/suzzy-v0-standalone/assets/library/branding/suzzy-logo.png differ diff --git a/suzzy-v0-standalone/assets/library/favicon.png b/suzzy-v0-standalone/assets/library/favicon.png new file mode 100644 index 0000000..a47d05f Binary files /dev/null and b/suzzy-v0-standalone/assets/library/favicon.png differ diff --git a/suzzy-v0-standalone/assets/library/introduction/banner-welcome.png b/suzzy-v0-standalone/assets/library/introduction/banner-welcome.png new file mode 100644 index 0000000..d3b94ba Binary files /dev/null and b/suzzy-v0-standalone/assets/library/introduction/banner-welcome.png differ diff --git a/suzzy-v0-standalone/assets/library/logo-navigation.png b/suzzy-v0-standalone/assets/library/logo-navigation.png new file mode 100644 index 0000000..206dc37 Binary files /dev/null and b/suzzy-v0-standalone/assets/library/logo-navigation.png differ diff --git a/suzzy-v0-standalone/assets/library/logo-og.png b/suzzy-v0-standalone/assets/library/logo-og.png new file mode 100644 index 0000000..93f4f16 Binary files /dev/null and b/suzzy-v0-standalone/assets/library/logo-og.png differ diff --git a/suzzy-v0-standalone/assets/library/search/search-empty.png b/suzzy-v0-standalone/assets/library/search/search-empty.png new file mode 100644 index 0000000..91c0695 Binary files /dev/null and b/suzzy-v0-standalone/assets/library/search/search-empty.png differ diff --git a/suzzy-v0-standalone/assets/library/search/search-error.png b/suzzy-v0-standalone/assets/library/search/search-error.png new file mode 100644 index 0000000..91c0695 Binary files /dev/null and b/suzzy-v0-standalone/assets/library/search/search-error.png differ diff --git a/suzzy-v0-standalone/assets/library/search/search-initial.png b/suzzy-v0-standalone/assets/library/search/search-initial.png new file mode 100644 index 0000000..91c0695 Binary files /dev/null and b/suzzy-v0-standalone/assets/library/search/search-initial.png differ diff --git a/suzzy-v0-standalone/assets/library/tutorials/applications-step-1.png b/suzzy-v0-standalone/assets/library/tutorials/applications-step-1.png new file mode 100644 index 0000000..d97e520 Binary files /dev/null and b/suzzy-v0-standalone/assets/library/tutorials/applications-step-1.png differ diff --git a/suzzy-v0-standalone/assets/library/tutorials/applications-step-2.png b/suzzy-v0-standalone/assets/library/tutorials/applications-step-2.png new file mode 100644 index 0000000..099111a Binary files /dev/null and b/suzzy-v0-standalone/assets/library/tutorials/applications-step-2.png differ diff --git a/suzzy-v0-standalone/assets/projects/craf/background.jpeg b/suzzy-v0-standalone/assets/projects/craf/background.jpeg new file mode 100644 index 0000000..971bfe0 Binary files /dev/null and b/suzzy-v0-standalone/assets/projects/craf/background.jpeg differ diff --git a/suzzy-v0-standalone/assets/projects/craf/image-1.jpeg b/suzzy-v0-standalone/assets/projects/craf/image-1.jpeg new file mode 100644 index 0000000..0d95d20 Binary files /dev/null and b/suzzy-v0-standalone/assets/projects/craf/image-1.jpeg differ diff --git a/suzzy-v0-standalone/assets/projects/craf/image-2.jpeg b/suzzy-v0-standalone/assets/projects/craf/image-2.jpeg new file mode 100644 index 0000000..94c5e3d Binary files /dev/null and b/suzzy-v0-standalone/assets/projects/craf/image-2.jpeg differ diff --git a/suzzy-v0-standalone/assets/projects/monke/background.jpeg b/suzzy-v0-standalone/assets/projects/monke/background.jpeg new file mode 100644 index 0000000..3a133a5 Binary files /dev/null and b/suzzy-v0-standalone/assets/projects/monke/background.jpeg differ diff --git a/suzzy-v0-standalone/assets/projects/monke/image-1.jpeg b/suzzy-v0-standalone/assets/projects/monke/image-1.jpeg new file mode 100644 index 0000000..ec8b520 Binary files /dev/null and b/suzzy-v0-standalone/assets/projects/monke/image-1.jpeg differ diff --git a/suzzy-v0-standalone/assets/scripts/browser-blog.ts b/suzzy-v0-standalone/assets/scripts/browser-blog.ts new file mode 100644 index 0000000..4e41bc9 --- /dev/null +++ b/suzzy-v0-standalone/assets/scripts/browser-blog.ts @@ -0,0 +1,47 @@ +// Widget: Latest Articles +(() => { + let articleIndex = 0 + let autoScrollEnabled = true + const elemControls = document.querySelector('div.widget-latest-controls')! + const elemArticles = document.querySelector('div.widget-latest-articles')! + const listArticles = elemArticles.querySelectorAll('div.latest-item') + const listDots = elemControls.querySelectorAll('div.dot-circle') + const gapOffset = parseInt(window + .getComputedStyle(elemArticles) + .getPropertyValue('gap') + ) + + // Autoscrolling Behaviour + function transitionNextArticle(shouldAdd) { + // Over/Underflow Handling + shouldAdd ? articleIndex++ : articleIndex-- + if (articleIndex < 0) articleIndex = listArticles.length - 1 + const desiredIndex = (articleIndex % listArticles.length) + + // Set Active Dot + listDots.forEach((e, i) => { + e.classList.remove('dot-active') + if (desiredIndex === i) e.classList.add('dot-active') + }) + + // Scroll over to post + let scrollOffset = 0 + for (let i = 0; i < desiredIndex; i++) { + scrollOffset += listArticles[i].offsetWidth + gapOffset + } + elemArticles.scrollLeft = scrollOffset + } + setInterval(() => autoScrollEnabled && transitionNextArticle(true), 10_000) + elemArticles.onmouseenter = () => autoScrollEnabled = false + elemArticles.onmouseleave = () => autoScrollEnabled = true + elemControls.onmouseenter = () => autoScrollEnabled = false + elemControls.onmouseleave = () => autoScrollEnabled = true + + // Back/Next Buttons + document + .querySelector('button#latest-next')! + .addEventListener('click', () => transitionNextArticle(true)) + document + .querySelector('button#latest-last')! + .addEventListener('click', () => transitionNextArticle(false)) +})() \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/scripts/browser-library.ts b/suzzy-v0-standalone/assets/scripts/browser-library.ts new file mode 100644 index 0000000..7761633 --- /dev/null +++ b/suzzy-v0-standalone/assets/scripts/browser-library.ts @@ -0,0 +1,201 @@ +(() => { + const enum STATE { + HELP = 0, // Help Message + CLOSED, // Menu Hidden + LOADING, // Loading Results + ERRORED, // Search Error + EMPTY, // No Results Returned + LIST, // List all Results + OPEN // Open Modal + } + const paneLoading = document.querySelector('div.pane#loading')! + const paneError = document.querySelector('div.pane#error')! + const paneHelp = document.querySelector('div.pane#help')! + const paneEmpty = document.querySelector('div.pane#empty')! + const paneResults = document.querySelector('div.pane#results')! + + const elementWrapper = document.querySelector('div.wrapper-search')! + const elementInput = document.querySelector('input.search-input')! + const elementClose = document.querySelector('button.search-close')! + const elementOpen = document.querySelector('button#search-open')! + const elementFooter = document.querySelector('div.search-footer > p')! + const elementError = paneError.querySelector('p')! + + let searchTimings = 0 + let searchElements = new Array() + let searchTimeout: ReturnType + let searchState = STATE.CLOSED // Modal State + let searchFocus = -1 // Selected Element Index (-1 for Input, -2 for Close) + let searchQuery = '' // Previous Search Query + let searchError = '' // API Error + let searchResults = new Array<{ // API Data + categoryName: string; + occurrences: Array<{ + location: string; + snippet: string; + link: string; + }>; + }>() + + function updateView(state: STATE) { + paneLoading.style.opacity = '0' + paneError.style.opacity = '0' + paneHelp.style.opacity = '0' + paneEmpty.style.opacity = '0' + paneResults.style.opacity = '0' + if (state === STATE.OPEN) { + state = searchResults.length > 0 + ? STATE.LIST + : STATE.HELP + } + if (state !== STATE.CLOSED && searchState === STATE.CLOSED) { + elementWrapper.style.pointerEvents = 'all' + elementWrapper.style.opacity = '1' + updateFocus(-1, true) + } + searchState = state + switch (state) { + case STATE.HELP: + elementFooter.textContent = '' + paneHelp.style.opacity = '1' + return + + case STATE.EMPTY: + elementFooter.textContent = '' + paneEmpty.style.opacity = '1' + return + + case STATE.LOADING: + elementFooter.textContent = '' + paneLoading.style.opacity = '1' + return + + case STATE.ERRORED: + paneError.style.opacity = '1' + elementError.textContent = searchError + return + + case STATE.CLOSED: + elementWrapper.style.pointerEvents = 'none' + elementWrapper.style.opacity = '0' + return + + case STATE.LIST: + const t = Date.now() - searchTimings + const c = searchResults.map(r => r.occurrences.length).reduce((a, b) => a + b, 0) + elementFooter.textContent = `Returned ${c} result${c === 1 ? '' : 's'} in ${t}ms` + + // Clear All Children + for (const c of paneResults.children) c.remove() + searchElements = [] + + // Display Empty Results + if (c === 0) { + updateView(STATE.EMPTY) + return + } + + // Render Children + for (const cat of searchResults) { + const div = document.createElement('div') + div.classList.add('search-result') + + const p = document.createElement('p') + p.classList.add('text-header', 'text-tip') + p.textContent = cat.categoryName + div.appendChild(p) + + for (const o of cat.occurrences) { + const a = document.createElement('a') + a.classList.add('search-item') + a.href = o.link + + const p1 = document.createElement('p') + p1.innerHTML = o.snippet + a.appendChild(p1) + + const p2 = document.createElement('p') + p2.classList.add('text-muted', 'text-tip') + p2.textContent = o.location + a.appendChild(p2) + + div.appendChild(a) + searchElements.push(a) + } + + paneResults.appendChild(div) + } + return + } + } + function updateFocus(adjust: number, set = false) { + set ? searchFocus = adjust : searchFocus += adjust + if (isNaN(searchFocus)) searchFocus = -1 + if (searchFocus > searchElements.length - 1) searchFocus = searchElements.length - 1 + if (searchFocus < -2) searchFocus = -2 + if (searchFocus === -2) return elementClose.focus() + if (searchFocus === -1) return elementInput.focus() + + console.log("FOCUS", searchElements[searchError]) + } + + // Keybind Handling + const pressedKeys = new Set() + document.addEventListener('keyup', ev => pressedKeys.delete(ev.key)) + document.addEventListener('keydown', ev => { + pressedKeys.add(ev.key) + if (pressedKeys.has('Control') && ev.key === 'k') { + ev.preventDefault() + updateView(searchState === STATE.CLOSED ? STATE.OPEN : STATE.CLOSED) + } + if (searchState !== STATE.CLOSED) { + if (ev.key === 'Escape') updateView(STATE.CLOSED) + if (ev.key === 'ArrowUp') updateFocus(-1) + if (ev.key === 'ArrowDown') updateFocus(1) + if (ev.key === 'ArrowLeft' && searchFocus === -2) updateFocus(-1, true) + if (ev.key === 'ArrowRight' && searchFocus === -1) updateFocus(-2, true) + } + }) + elementClose.addEventListener('click', () => updateView(STATE.CLOSED)) + elementOpen.addEventListener('click', () => updateView(STATE.OPEN)) + elementWrapper.addEventListener('click', ev => + ev.target === elementWrapper && updateView(STATE.CLOSED) + ) + elementInput.addEventListener('input', () => { + clearTimeout(searchTimeout) + + const query = encodeURIComponent(elementInput.value.trim()) + if (query.length === 0) return updateView(STATE.HELP) + if (query === searchQuery) return + searchQuery = query + updateView(STATE.LOADING) + + // Queue Search Request + searchTimeout = setTimeout(() => { + searchTimings = Date.now() + fetch(`/library/snippet?query=${query}`) + .then(resp => resp.json()) + .then(body => { + searchResults = body + updateView(STATE.LIST) + }) + .catch(e => { + console.error('SEARCH ERROR', e) + searchError = String(e) + updateView(STATE.ERRORED) + }) + }, 200) + }) + + // // Restore Session (if any) + // const cacheFocus = localStorage.getItem('search_focus') + // const cacheQuery = localStorage.getItem('search_query') + // const cacheResult = localStorage.getItem('search_data') + // if (cacheResult && cacheQuery && cacheFocus) { + // elementInput.value = cacheQuery + // searchResults = JSON.parse(cacheResult) + // updateFocus(parseInt(cacheFocus), true) + // } else { + // updateView(STATE.CLOSED) + // } +})() \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/scripts/challenge-picross.ts b/suzzy-v0-standalone/assets/scripts/challenge-picross.ts new file mode 100644 index 0000000..c0abc56 --- /dev/null +++ b/suzzy-v0-standalone/assets/scripts/challenge-picross.ts @@ -0,0 +1,120 @@ +(async () => { + { + // Setup Grid Tiles + const audioSet = document.querySelector('audio#set')! + const audioRelease = document.querySelector('audio#release')! + document.querySelectorAll('button.picross-button').forEach(e => { + e.addEventListener('click', () => { + e.classList.remove('picross-ignore') + e.classList.toggle('picross-active') + ? audioSet.play() + : audioRelease.play() + }) + e.addEventListener('contextmenu', ev => { + ev.preventDefault() + e.classList.remove('picross-active') + e.classList.toggle('picross-ignore') + audioRelease.play() + }) + }) + + // Setup Page Timer + let secondsElapsed = 0 + const timerElement = document.querySelector('p#time-spent')! + setInterval(() => { + secondsElapsed++ + timerElement.textContent = (() => { + const hours = Math.floor(secondsElapsed / 3600) + const minutes = Math.floor((secondsElapsed % 3600) / 60) + const seconds = Math.floor(secondsElapsed % 60); + return `Time Spent: ${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; + })() + }, 1000) + + + // Prevent annoying context menu popup when you miss a grid tile by 1 pixel :L + document. + querySelector('div.special-picross')! + .addEventListener('contextmenu', e => e.preventDefault()) + } + + // Fetch Badges for User Profile + const resp = await CallAPI<{ public_flags: number; }>('GET', '/v1/users/@me', 'basic') + if (resp.code === 401) { + console.info('User is not logged in, redirecting...') + CreateMessage( + 'Account Required', 'You must be logged in to attempt this challenge!', + 'Login', () => window.location.assign('/login?redirect=/challenge-picross') + ) + return + } + if (!resp.success) { + console.error(`Server Responded with Code ${resp.code}`, resp.body) + return CreateMessage( + 'Server Unavailable', + 'Encountered an error while fetching your profile', + 'Refresh', + () => window.location.reload(), + ) + } + + // Setup Webpage + const correctAnswer = 2560660350 + const completed = (resp.json.public_flags & 4) !== 0 + if (completed) CreateMessage( + 'Already Collected!', + `You\'ve already collected this badge, but you can still replay this challenge.`, + 'Got it!', + () => { } + ) + + // Setup Submit Button + document.querySelector('a#done')!.addEventListener('click', async () => { + SetDynamic(false) + + // Filter: Correct Answer? + let givenAnswer = 0 + document.querySelectorAll('div.picross-row').forEach((childRow, rowIndex) => { + let childIndex = 0 + for (const child of childRow.children) { + if (child.nodeName === 'BUTTON') { + if (child.classList.contains('picross-active')) { + givenAnswer += 1 << ((rowIndex - 3) * 9) + childIndex + } + childIndex++ + } + } + }) + if (correctAnswer !== givenAnswer) { + CreateMessage('Nope!', 'Not quite! Check your work and try again!', 'Fine', () => { }) + SetDynamic(true) + return + } + + // Filter: Already Completed? + if (completed) return CreateMessage( + 'Congrats!', + 'But you\'ve already collected this badge. Thank you for playing!', + 'Visit Profile', + () => window.location.assign('/profile') + ) + + // Redeem Reward for User + const resp = await CallAPI( + 'POST', '/v1/users/@me/inventory/redeem', 'basic', + { code: 'challenge-' + givenAnswer } + ) + if (resp.ok) { + CreateMessage( + 'Congrats!', + 'You\'ve earned the picross badge! You it is now visible on your profile.', + 'Let me see!', + () => window.location.assign('/profile') + ) + } else { + CreateMessage('Redeem Error', resp.body, 'Close', () => { }) + console.error('Redeem Error', resp) + } + SetDynamic(true) + }) +})() \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/scripts/login-index.ts b/suzzy-v0-standalone/assets/scripts/login-index.ts new file mode 100644 index 0000000..0607583 --- /dev/null +++ b/suzzy-v0-standalone/assets/scripts/login-index.ts @@ -0,0 +1,36 @@ +let debounce = false +CreateModal([ + //- Login View + { + elements: { + '1': { type: 'headline', label: 'Log In', }, + '2': { type: 'text', label: 'to your suzzy games account' }, + '3': { type: 'divider' }, + 'email': { type: 'input', inputType: 'text', label: 'Email Address', }, + 'password': { type: 'input', inputType: 'password', label: 'Password', }, + '4': { type: 'spacer', }, + '5': { type: 'link', label: 'Forgot Password?', href: '/password-reset', }, + '6': { type: 'link', label: 'Don\'t have an account? Sign up.', href: '/signup', }, + }, + closeLabel: 'Cancel', + closeLogic: () => FollowRedirect('/'), + submitLabel: 'Log In', + submitLogic: async (modal, values) => { + return { success: false, messages: 'Not Yet Implemented' } + }, + }, + + // Verification Email Sent Prompt + { + submitLabel: 'Done', + submitLogic: async m => { + m.SetActiveView(0) + return { success: false, messages: '' } + }, + elements: { + }, + }, + +]) + .then(r => console.log('done', r)) + .catch(e => console.error('fail', e)) \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/scripts/login-passwordreset.ts b/suzzy-v0-standalone/assets/scripts/login-passwordreset.ts new file mode 100644 index 0000000..b973ea2 --- /dev/null +++ b/suzzy-v0-standalone/assets/scripts/login-passwordreset.ts @@ -0,0 +1,36 @@ +CreateModal([ + { + submitLabel: 'Submit', + submitLogic: (modal, { email }) => new Promise(ok => { + const result = TestEmailSuite(email) + if (result) return ok({ success: false, messages: { 'email': result } }) + CallAPI('POST', '/v1/auth/password-reset', 'public', { email }).then(resp => { + // This endpoint always returns a '204 No Content' + // so anything else other than that is a server error + if (resp.error) return ok({ + success: false, + messages: String(resp.error) + }) + modal.SetActiveView(1) + return ok({ success: false, messages: '' }) + }) + }), + elements: { + '1': { type: 'headline', label: 'Reset Password', }, + '2': { type: 'text', label: 'for your suzzy games account' }, + '3': { type: 'divider' }, + 'email': { type: 'input', inputType: 'text', label: 'Email Address' }, + '4': { type: 'spacer' }, + '5': { type: 'link', label: 'Remember your password? Log in.', href: '/login', }, + }, + }, + { + closeLabel: 'Done', + closeLogic: () => window.location.assign('/login'), + elements: { + '1': { type: 'header', label: 'Email Sent' }, + '2': { type: 'text', label: "An email with instructions on how to reset your password has been sent to your inbox." }, + '3': { type: 'text', label: "Can't find it? Try checking your spam filter." }, + } + } +]) \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/scripts/login-passwordupdate.ts b/suzzy-v0-standalone/assets/scripts/login-passwordupdate.ts new file mode 100644 index 0000000..0619b1c --- /dev/null +++ b/suzzy-v0-standalone/assets/scripts/login-passwordupdate.ts @@ -0,0 +1,47 @@ +CreateModal([ + { + submitLabel: 'Submit', + submitLogic: (modal, values) => new Promise(ok => { + const messages: Record = {} + const key_password = 'password' + const key_password_again = 'password_again' + messages[key_password] = TestPasswordSuite(values[key_password]) + messages[key_password_again] = TestPasswordSuite(values[key_password_again]) + if (values[key_password] !== values[key_password_again]) { + messages[key_password_again] = 'Passwords Do Not Match' + } + if (Object.values(messages).filter(e => e).length !== 0) { + ok({ success: false, messages }) + return + } + + // Fetch Token from Parameters and Send Request + const href = document.location.href + const token = new URLSearchParams(href.slice(href.indexOf('?') + 1)).get('token') + CallAPI( + 'PATCH', '/v1/auth/password-reset', 'basic', + { [key_password]: values[key_password], token } + ).then(resp => { + if (!resp.ok) return ok({ success: false, messages: resp.json.error }) + modal.SetActiveView(1) + ok({ success: false, messages: '' }) + }) + }), + elements: { + '1': { type: 'headline', label: 'Change Password', }, + '2': { type: 'text', label: 'Enter the new password you wish to use.' }, + '3': { type: 'divider' }, + 'password': { type: 'input', inputType: 'password', label: 'New Password' }, + 'password_again': { type: 'input', inputType: 'password', label: 'New Password Again' }, + '4': { type: 'spacer' }, + }, + }, + { + closeLabel: 'Log In', + closeLogic: () => window.location.assign('/login'), + elements: { + '1': { type: 'header', label: 'Password Changed' }, + '2': { type: 'text', label: 'You may now log in using your new password. You have not been logged out on any devices.' }, + } + } +]) \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/scripts/login-signup.ts b/suzzy-v0-standalone/assets/scripts/login-signup.ts new file mode 100644 index 0000000..ee4a19e --- /dev/null +++ b/suzzy-v0-standalone/assets/scripts/login-signup.ts @@ -0,0 +1,47 @@ +CreateModal([{ + elements: { + '1': { type: 'headline', label: 'Sign Up' }, + '2': { type: 'text', label: 'for a suzzy games account' }, + '3': { type: 'divider' }, + 'username': { type: 'input', inputType: 'text', label: 'Username' }, + 'email': { type: 'input', inputType: 'text', label: 'Email Address' }, + 'password': { type: 'input', inputType: 'password', label: 'Password' }, + '4': { type: 'spacer' }, + '5': { type: 'link', label: 'Already have an account? Log in', href: '/login' }, + '6': { type: 'unique-links', text: 'By signing up you agree to our;Terms Of Service:/library/legal/terms-of-service;and;Privacy Policy:/library/legal/privacy-policy' } + }, + closeLabel: 'Cancel', + closeLogic: () => FollowRedirect('/'), + submitLabel: 'Sign Up', + submitLogic: (modal, { username, email, password }) => { + return new Promise(async ok => { + const errorMessages: Record = {} + errorMessages.username = TestUsernameSuite(username) + errorMessages.password = TestPasswordSuite(password) + errorMessages.email = TestEmailSuite(email) + if (Object.values(errorMessages).filter(e => e).length !== 0) { + ok({ success: false, messages: errorMessages }) + return + } + + // Create Account + let resp = await CallAPI('POST', '/v1/auth/signup', 'public', { username, email, password }) + if (!resp.ok) return ok({ + success: false, + messages: `Signup Failed (${resp.code}): ${resp.body}` + }) + // Login Account + // Use basic auth to allow collection of cookies + resp = await CallAPI('POST', '/v1/auth/login', 'basic', { email, password }) + if (!resp.ok) return ok({ + success: false, + messages: `Login Failed (${resp.code}): ${resp.body}` + }) + FollowRedirect('/profile') + }) + }, +}]) + +document.querySelector('#username')!.value = Date.now() + '' +document.querySelector('#email')!.value = Date.now() + '@gmail.com' +document.querySelector('#password')!.value = 'Googleman00!' \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/scripts/login-verifyemail.ts b/suzzy-v0-standalone/assets/scripts/login-verifyemail.ts new file mode 100644 index 0000000..ee20d96 --- /dev/null +++ b/suzzy-v0-standalone/assets/scripts/login-verifyemail.ts @@ -0,0 +1,10 @@ +(() => { + const href = document.location.href + const token = new URLSearchParams(href.slice(href.indexOf('?') + 1)).get('token') + if (token) CallAPI('POST', '/v1/auth/verify-email/' + token, 'public') + CreateMessage( + 'Email Verified', + 'Thank you for verifying your email! Redirecting you to your profile...', + 'Done', () => window.location.assign('/profile') + ) +})() \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/scripts/login-verifylogin.ts b/suzzy-v0-standalone/assets/scripts/login-verifylogin.ts new file mode 100644 index 0000000..640c3fe --- /dev/null +++ b/suzzy-v0-standalone/assets/scripts/login-verifylogin.ts @@ -0,0 +1,10 @@ +(() => { + const href = document.location.href + const token = new URLSearchParams(href.slice(href.indexOf('?') + 1)).get('token') + if (token) CallAPI('POST', '/v1/auth/verify-login/' + token, 'public') + CreateMessage( + 'Login Verified', + 'You can log in from the your new device. Redirecting you to the login page...', + 'Done', () => window.location.assign('/login') + ) +})() \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/scripts/public-homepage.ts b/suzzy-v0-standalone/assets/scripts/public-homepage.ts new file mode 100644 index 0000000..3d78bfd --- /dev/null +++ b/suzzy-v0-standalone/assets/scripts/public-homepage.ts @@ -0,0 +1,8 @@ +// Displays a birthday pancake beside the 'Active since May 13th, 2021' +// on the Homepage when it's suzzy games birthday :3 +(() => { + const t = new Date() + if (t.getMonth() === 4 && t.getDate() === 13) { + document.querySelector('#birthday')!.textContent += '🥞' + } +})() \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/scripts/public-projects.ts b/suzzy-v0-standalone/assets/scripts/public-projects.ts new file mode 100644 index 0000000..4cf27e0 --- /dev/null +++ b/suzzy-v0-standalone/assets/scripts/public-projects.ts @@ -0,0 +1,41 @@ +document + .querySelectorAll('div.widget-project') + .forEach(project => { + let currentImage = 0 + const galleryDots = project.querySelectorAll('div.gallery-dot')! + const galleryItems = project.querySelectorAll('img.gallery-image')! + const galleryScroll = project.querySelector('div.project-gallery-scroll')! + const gapOffset = parseInt(window + .getComputedStyle(galleryScroll) + .getPropertyValue('gap') + ) + + function ScrollImage(shouldAdd: boolean) { + // Over/Underflow Handling + shouldAdd ? currentImage++ : currentImage-- + if (currentImage < 0) currentImage = galleryItems.length - 1 + const desiredIndex = (currentImage % galleryItems.length) + + // Set Active Dot + galleryDots.forEach((e, i) => { + i === desiredIndex ? + e.classList.add('gallery-dot-active') + : e.classList.remove('gallery-dot-active') + }) + + // Scroll over to image + let scrollOffset = 0 + for (let i = 0; i < desiredIndex; i++) { + scrollOffset += galleryItems[i].offsetWidth + gapOffset + } + galleryScroll.scrollLeft = scrollOffset + } + + // Back/Next Buttons + project + .querySelector('button#last')! + .addEventListener('click', () => ScrollImage(false)) + project + .querySelector('button#next')! + .addEventListener('click', () => ScrollImage(true)) + }) \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/scripts/shared-article.ts b/suzzy-v0-standalone/assets/scripts/shared-article.ts new file mode 100644 index 0000000..5fdf298 --- /dev/null +++ b/suzzy-v0-standalone/assets/scripts/shared-article.ts @@ -0,0 +1,123 @@ +(() => { + // Audio Elements + /** Converts seconds into a timestamp (ex. 0:00) */ + function formatTime(someTime) { + const minutes = (someTime / 60) | 0 + someTime -= minutes * 60 + return `${minutes}:${(someTime | 0).toString().padStart(2, '0')}` + } + + document.querySelectorAll('div.element-audio').forEach(audioElement => { + const elemAudio = audioElement.querySelector('audio')! + const elemProgress = audioElement.querySelector('input')! + const elemButton = audioElement.querySelector('button')! + const elemButtonIcon = audioElement.querySelector('button img')! + const elemTime = audioElement.querySelector('p.audio-time')! + const elemDownload = audioElement.querySelector('a.audio-download')! + + let isLoaded = false + let isDragging = false + let ignoreInput = false + elemButton.addEventListener('click', () => { + if (ignoreInput) return + if (!isLoaded) { + // Load Audio Source + ignoreInput = true + elemAudio.src = elemDownload.href + elemAudio.oncanplay = () => { + elemProgress.removeAttribute('disabled') + + // Audio Seeking + elemProgress.addEventListener('mousedown', () => isDragging = true) + elemProgress.addEventListener('mouseup', () => isDragging = false) + elemProgress.addEventListener('change', () => { + elemAudio.currentTime = elemAudio.duration * (parseInt(elemProgress.value) / 100) + }) + + // Audio Controls + elemAudio.addEventListener('play', () => elemButtonIcon.src = '/assets/icons/icon-pause.svg') + elemAudio.addEventListener('pause', () => elemButtonIcon.src = '/assets/icons/icon-play.svg') + elemAudio.addEventListener('timeupdate', () => { + elemTime.textContent = (formatTime(elemAudio.currentTime) + '/' + formatTime(elemAudio.duration)) + + // Prevent progress bar from moving while user is still dragging it: + // elemProgress 'change' event will update the bar each second causing the + // progress bar to snap back and forth between actual playtime and desired playtime (mouse position) + if (!isDragging) { + elemProgress.value = (((100 / elemAudio.duration) * elemAudio.currentTime) | 0).toString() + } + }) + + // Play Audio and Finish Setup + elemAudio.play() + elemAudio.volume = 0.5 + ignoreInput = false + isLoaded = true + } + } else { + elemAudio.paused ? elemAudio.play() : elemAudio.pause() + } + }) + }) + + // Code Block Elements + document.querySelectorAll('div.element-code').forEach(parentElement => { + const action = parentElement.querySelector('button')! + const content = parentElement.querySelector('pre')! + action.addEventListener('click', () => { + navigator.clipboard.writeText(content.textContent!) + alert('Content Copied to Clipboard!') + }) + }) + + // Chapter Hightlighting + const chapters = new Array<[HTMLAnchorElement, HTMLParagraphElement]>() + let scroll_debounce = false + let scroll_offset = 0 + let scroll_search = '' + let scroll_tracks = new Array<{ source: any; key: string; }>() + + if (window.location.pathname.startsWith('/library')) { + scroll_search = 'div.layout-navigation a.navigation-chapter' + scroll_tracks = [ + { 'source': document.querySelector('div.layout-content'), 'key': 'scrollTop' }, + { 'source': window, key: 'scrollY' } + ] + } + if (window.location.pathname.startsWith('/blog')) { + scroll_search = 'div.article-chapters a' + scroll_tracks = [{ 'source': window, key: 'scrollY' }] + scroll_offset = 64 + } + + // Find and locate all Chapter Links + document.querySelectorAll(scroll_search).forEach(anchor => { + const href = anchor.href.split('#').at(-1) + if (!href) return + const head = document.querySelector(`p#${href}`) + if (head) chapters.push([anchor, head]) + }) + + // Listen to page scroll events + function onPageScroll(x: number) { + chapters.forEach(([link, header], i, a) => { + ( + x >= (!i ? 0 : header.offsetTop) && + x <= (a[i + 1]?.[1]?.offsetTop || Infinity) + ) + ? link.setAttribute('active', 'true') + : link.removeAttribute('active') + }) + } + for (const { source, key } of scroll_tracks) { + source.addEventListener('scroll', () => { + if (scroll_debounce) return + scroll_debounce = true + window.requestAnimationFrame(() => { + onPageScroll(source[key] + scroll_offset) + scroll_debounce = false + }) + }) + } + onPageScroll(0) +})() \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/scripts/shared-public.ts b/suzzy-v0-standalone/assets/scripts/shared-public.ts new file mode 100644 index 0000000..3a705dc --- /dev/null +++ b/suzzy-v0-standalone/assets/scripts/shared-public.ts @@ -0,0 +1,98 @@ +(() => { + // Have Footer always be just slightly out of view + { + const footerWrapper = document.querySelector('div.wrapper-footer') + if (footerWrapper) { + footerWrapper.style.marginTop = Math.max( + document.documentElement.clientHeight - + (document.body.clientHeight - footerWrapper.clientHeight), 0 + ) + 'px' + } + } + // Navigation Bar - Profile + // Cache for this is invalidated and updated by another script + { + interface MinimalProfileResponse { + id: number; + avatar: string | null; + } + let loggedIn = true + const profileButton = document.querySelector('a#navigation-profile')! + const profileCache = sessionStorage.getItem('cache_profile') + profileCache + ? UpdateProfile(JSON.parse(profileCache)) + : FetchProfile() + + /** Redirect to profile if logged in, otherwise prompt login */ + profileButton.addEventListener('click', () => loggedIn + ? window.location.assign('/profile') + : window.location.assign('/login') + ) + /** Update Profile Elements */ + function UpdateProfile(data: undefined | MinimalProfileResponse) { + if (data === undefined) return loggedIn = false + profileButton.style.backgroundImage = `url('${ImageURL('avatars', data.id, data.avatar, true, "small")}')` + } + /** Fetch Profile from API */ + function FetchProfile() { + CallAPI('GET', '/v1/users/@me', 'basic').then(resp => { + if (resp.ok) { + sessionStorage.setItem('cache_profile', resp.body) + UpdateProfile(resp.json) + } else { + UpdateProfile(undefined) + console.error(`[PROFILE] Fetch Failed`, resp.error) + } + }) + } + } + // Navigation Bar - Status + // Fetches player count and server status from service every 5 minutes + // TODO: Add random offset so traffic spike doesn't disintegrate the server lol + { + const CLASS_RED = 'status-red' + const CLASS_YEL = 'status-yellow' + const CLASS_GRN = 'status-green' + const ELEMENT_DOT = document.querySelector('div#status-dot')! + const ELEMENT_TXT = document.querySelector('p#status-text')! + FetchStatus() + + /** Update Status Elements */ + function UpdateStatus(status: undefined | { status: "online" | "degraded" | "down" | "none"; counter_players_online?: number; }) { + for (const c of [CLASS_RED, CLASS_YEL, CLASS_GRN]) ELEMENT_DOT.classList.remove(c) + if (status !== undefined) switch (status.status) { + case "online": + ELEMENT_DOT.classList.add(CLASS_GRN) + ELEMENT_TXT.textContent = `Online • ${status.counter_players_online} players` + break + case "degraded": + ELEMENT_DOT.classList.add(CLASS_YEL) + ELEMENT_TXT.textContent = `Service Degraded • ${status.counter_players_online} players` + break + case "down": + ELEMENT_DOT.classList.add(CLASS_RED) + ELEMENT_TXT.textContent = 'Service Down' + break + case "none": + ELEMENT_TXT.textContent = "Unknown" + break + } + } + /** Fetch Status from API */ + function FetchStatus() { + fetch(ENV_URLS.status + '/api/widget.json').then(async resp => { + const REFRESH_IN = parseFloat(resp.headers.get('X-Refresh')!) * 1000 | 0 + if (resp.status === 503) { + setTimeout(FetchStatus, REFRESH_IN) + return + } else if (resp.status === 200) { + UpdateStatus(await resp.json()) + return + } else { + console.error(`[STATUS] Request Failed with Status Code ${resp.status}:`, await resp.text()) + UpdateStatus({ status: "none" }) + } + }) + } + } +})() \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/scripts/shared-utils.ts b/suzzy-v0-standalone/assets/scripts/shared-utils.ts new file mode 100644 index 0000000..5b72cb2 --- /dev/null +++ b/suzzy-v0-standalone/assets/scripts/shared-utils.ts @@ -0,0 +1,440 @@ +const FLAGS_OAUTH2_SCOPES = [ + [1 << 0, 'View your Profile'], + [1 << 1, 'View your Email'], + [1 << 2, 'View your Connections'], +] +const FLAGS_PROFILE_BADGES = [ + [1 << 0, 'The Boss', '/icons/badge-crown'], + [1 << 1, 'V.I.P', '/icons/badge-star'], + [1 << 2, 'Picross Master', '/icons/badge-picross'], +] + +/** Variables that change if in localhost */ +const ENV_URLS = (() => { + const IN_DEBUG = window.location.hostname.includes('localhost') + return { + 'status': IN_DEBUG ? 'http://localhost:8200' : 'https://status.suzzygames.com', + 'api': IN_DEBUG ? 'http://localhost:8000' : 'https://apis.suzzygames.com', + 'cdn': IN_DEBUG ? 'http://localhost:8100' : 'https://content.suzzygames.com', + } +})() + +function TestEmailSuite(v: string): string | undefined { + if (v.length === 0) return 'Required Field' + if (!/^[^@]+@[^@]+\.[^@]+$/.test(v)) return 'Invalid or Malformed Email Address' +} +function TestPasswordSuite(v: string): string | undefined { + if (v.length === 0) return 'Required Field' + if (!/[^A-Za-z0-9]/.test(v)) return 'Password must contain a Special Character' + if (!/[A-Z]/.test(v)) return 'Password must contain an Uppercase Character' + if (!/[a-z]/.test(v)) return 'Password must contain a Lowercase Character' + if (!/[0-9]/.test(v)) return 'Password must contain a Number' + if (v.length < 8) return 'Password cannot be shorter than 8 characters' + if (v.length > 256) return 'Password cannot be longer than 256 characters' +} +function TestUsernameSuite(v: string): string | undefined { + if (v.length === 0) return 'Required Field' + if (!/[\w]/.test(v)) return 'Username must be Alphanumeric [a-zA-z0-9_]' + if (v.length < 3) return 'Username cannot be shorter than 3 characters' + if (v.length > 32) return 'Password cannot be longer than 32 characters' +} +function TestPasscodeSuite(v: string): string | undefined { + if (v.length === 0) return 'Required Field' + if (!/^([0-9]{6}|[0-9ABCDEF]{8})$/.test(v)) return 'Invalid or Malformed Passcode' +} + +/** Set **disabled** attribute to all DOM elements with the **dynamic** attribute */ +function SetDynamic(enabled: boolean) { + document.querySelectorAll('*[dynamic]').forEach(e => { + enabled + ? e.removeAttribute('disabled') + : e.setAttribute('disabled', 'true') + }) +} + +/** Follow Redirect using the **redirect** param in url */ +function FollowRedirect(defaultPath: string) { + const search = new URLSearchParams(document.location.href.split('?').at(1) || '') + const redirect = search.get('redirect') || defaultPath + try { + // Follow Given URL only if whitelisted + const url = new URL(redirect) + window.location.assign( + url.host.endsWith('suzzygames.com') + ? redirect + : defaultPath + ) + } catch (err) { + // Expect a Redirect that begins with a forward slash + window.location.assign( + redirect.startsWith('/') + ? redirect + : defaultPath + ) + } +} + +/** Generate an URL to a User Avatar/banner or Application Icon */ +function ImageURL( + folder: 'avatars' | 'banners' | 'icons', + id: number, + hash: string | null, + animated: boolean, + size: 'small' | 'medium' | 'large' +) { + // Use Default avatars/icons otherwise fetch from CDN + return hash === null + ? '/assets/images/' + (folder === 'avatars' ? `default-user${(id % 6)}.png` : 'default-icon.png') + : ENV_URLS.cdn + `/${folder}/${id}/${hash}/${animated && hash.startsWith('a_') ? 'gif' : 'webp'}/${size}` +} + +interface ApiResponse { + success: boolean // Request Sucessful? + error?: Error // Request Error (if any) + ok: boolean // Status Code ranges between 200-299 + code: number // HTTP Response Code + body: string // HTTP Response Body + json: BodyType // HTTP Response Body parsed as JSON +} +function CallAPI( + method: string, + path: string, + authorization: 'mfa' | 'basic' | 'public', + withBody?: Record +): Promise> { + return new Promise(ok => { + + // Send HTTP Request + let body: string | undefined + let result: ApiResponse + let headers = new Headers() + let credentials: RequestCredentials = (authorization === 'public' ? 'omit' : 'include') + if (authorization === 'mfa') { + + // Check for MFA Token + + throw 'mfa not implemented' + } + if (withBody !== undefined) { + headers.set('content-type', 'application/json') + body = JSON.stringify(withBody) + } + + fetch(ENV_URLS.api + path, { method, body, headers, credentials }) + .then(async resp => { + let json: any = undefined + const text = await resp.text() + try { json = JSON.parse(text) } catch (e) { } + result = { + 'success': true, + 'error': undefined, + 'ok': resp.ok, + 'code': resp.status, + 'body': text, + 'json': json, + } + }) + .catch(error => { + result = { + success: false, + error, + ok: false, + code: -1, + body: '', + json: undefined as any + } + }) + .finally(() => { + console.log(`[HTTP] ${method} ${path} (Auth: ${authorization}) (Body: ${body?.length || 0}B)`, result) + ok(result) + }) + }) +} + +/** Creates a modal window */ +function CreateModal(views: ModalView[]) { + return new Promise((modalClose, modalCancel) => { + let currentView = 0 + + /** Helper: Create Element with Classes and Parent */ + function _Create(tagName: string, classes: string, parent: HTMLElement) { + const elem = document.createElement(tagName) + elem.classList.add(...classes.split(',').filter(c => c.length)) + parent.appendChild(elem) + return elem as T + } + function _Close() { + SetDynamic(true) + modalWrapper.style.opacity = '0' + setTimeout(() => modalWrapper.remove(), 200) + } + + /** Scroll to desired view index */ + function SetActiveView(viewIndex: number) { + const targetView = modalViews[viewIndex] + if (targetView) { + modalViews.forEach(({ wrapper }, i) => + wrapper.style.height = (viewIndex === i) + ? `${targetView.wrapper.scrollHeight}px` + : '0' + ) + modalContainer.scrollLeft = (viewIndex * 600) + currentView = viewIndex + SetCanInteract(true) + } + } + + /** Enable/Disable Modal Interaction */ + function SetCanInteract(canInteract: boolean) { + function Enabled(elem: Element, value: boolean) { + value + ? elem.removeAttribute('disabled') + : elem.setAttribute('disabled', 'true') + } + modalViews.forEach((v, i) => { + const newValue = (i === currentView) ? canInteract : false + for (const e of v.elements.children) { + if (e.tagName === 'A') e.setAttribute('tabindex', '0') + if (e.tagName === 'INPUT') Enabled(e, newValue) + } + if (v.submit) Enabled(v.submit, newValue) + if (v.cancel) Enabled(v.cancel, newValue) + }) + } + + const modalWrapper = _Create('div', 'modal-wrapper', document.body) + const modalContainer = _Create('div', 'modal-content,special-shadow', modalWrapper) + const modalViews = views.map(o => { + const wrapper = _Create('div', 'modal-view', modalContainer) + const elements = _Create('div', 'modal-elements', wrapper) + _Create('div', 'modal-divider', wrapper) + const footer = _Create('div', 'modal-footer', wrapper) + + // Create Elements + for (const [key, value] of Object.entries(o.elements || {})) { + switch (value.type) { + // Static Elements + case 'headline': + _Create('p', 'modal-headline', elements).textContent = value.label + break + case 'header': + _Create('p', 'modal-header', elements).textContent = value.label + break + case 'divider': + _Create('div', 'modal-divider', elements) + break + case 'spacer': + _Create('div', 'modal-spacer', elements) + break + case 'link': + const a = _Create('a', 'modal-link', elements) + a.textContent = value.label + a.href = value.href + a.tabIndex = -1 + break + case 'text': + _Create('p', 'modal-text', elements).textContent = value.label + break + + // Interactive Elements + case 'input': + const label = _Create('label', 'modal-label', elements) + const input = _Create('input', 'modal-input', elements) + label.textContent = value.label + label.title = value.label + label.htmlFor = key + input.type = value.inputType + input.id = key + input.disabled = true + break + + // Unique Elements + case 'unique-links': + const linkWrapper = _Create('div', 'modal-flex', elements) + for (const e of value.text.split(';')) { + const [text, url] = e.split(':', 2) + if (url) { + const a = _Create('a', 'modal-link', linkWrapper) + a.textContent = text + a.target = '_blank' + a.href = url + } else { + _Create('p', 'modal-link', linkWrapper).textContent = text + } + } + break + } + } + + // Create Footer Elements + let submit: HTMLButtonElement | undefined + let cancel: HTMLButtonElement | undefined + let alert = _Create('p', 'modal-alert', footer) + if (o.closeLabel) { + cancel = _Create('button', 'default', footer) + cancel.textContent = o.closeLabel + cancel.disabled = true + cancel.onclick = async () => { + if (o.closeLogic) await o.closeLogic() + modalCancel('User Closed Modal' as any) + _Close() + } + } + if (o.submitLabel) { + submit = _Create('button', 'default', footer) + submit.textContent = o.submitLabel + submit.disabled = true + submit.onclick = async () => { + if (o.submitLogic) { + // Collect Inputs + const view = modalViews[currentView] + const values: Record = {} + for (const e of view.elements.children) { + if (e instanceof HTMLInputElement) { + values[e.id] = e.value + continue + } + } + // Run Logic + SetCanInteract(false) + o.submitLogic({ SetCanInteract, SetActiveView }, values) + .then(results => { + + // Reset Errors and Create Index for Label Elements + view.alert.textContent = '' + const labels: Record = {} + for (const e of view.elements.children) { + if (e instanceof HTMLLabelElement) { + labels[e.getAttribute('for')!] = e + e.textContent = e.title + e.removeAttribute('error') + } + } + + // Complete Promise and Return Results + if (results.success) { + modalClose(results.values as T) + _Close() + return + } + if (results.messages) { + if (typeof results.messages === 'string') { + // Show Basic Alert + view.alert.textContent = results.messages + } else { + // Show Rich Error(s) + for (const [id, label] of Object.entries(labels)) { + const message = results.messages[id] + if (message) { + label.textContent = `${label.title} - ${message || 'No Error Message Provided'}` + label.setAttribute('error', 'true') + } + } + } + } + }) + .catch(error => { + // Set Error + view.alert.textContent = `Script Error: ${error}` + console.error('[MODAL] Script error', error) + }) + .finally(() => SetCanInteract(true)) + } + } + } + + return { wrapper, elements, footer, submit, cancel, alert } + }) + + // A little silly but heights and offsets break until everything (AND I MEAN EVERYTHING) + // has loaded in completely and browser has rendered the first frame + requestIdleCallback(() => { + requestAnimationFrame(() => { + modalWrapper.style.opacity = '1' // Fade in Modal + SetDynamic(false) // Disable all Interactable Elements + SetActiveView(currentView) // Initialize View + }) + }) + + }) +} + +/** Simpler method of creating modals for quick messages */ +function CreateMessage( + title: ElementHeader['label'], + message: ElementText['label'], + buttonLabel: ModalView['closeLabel'], + buttonLogic: ModalView['closeLogic'] +) { + return CreateModal([{ + 'closeLabel': buttonLabel, + 'closeLogic': buttonLogic, + 'submitLogic': async () => { return { success: true } }, + 'elements': { + 'title': { type: 'header', label: title }, + 'message': { type: 'text', label: message } + } + }]) +} + +interface ModalState { + SetActiveView: (viewIndex: number) => void + SetCanInteract: (canInteract: boolean) => void +} +interface ModalResults { + success: boolean // Submit Success + values?: Record // Submit Results + messages?: Record | string // Submit Error +} +interface ModalView { + submitLabel?: string + submitLogic?: (modal: ModalState, values: Record) => Promise + closeLabel?: string + closeLogic?: () => void + elements?: { [key: string]: ModalElement } +} +type ModalElement = + ElementHeader | + ElementHeadline | + ElementDivider | + ElementSpacer | + ElementText | + ElementInput | + ElementLink | + ElementUniqueLinks + +interface ElementBase { + type: string +} +interface ElementHeader extends ElementBase { + type: 'header' + label: string +} +interface ElementHeadline extends ElementBase { + type: 'headline' + label: string +} +interface ElementDivider extends ElementBase { + type: 'divider' +} +interface ElementSpacer extends ElementBase { + type: 'spacer' +} +interface ElementText extends ElementBase { + type: 'text' + label: string +} +interface ElementInput extends ElementBase { + type: 'input' + inputType: string + label: string +} +interface ElementLink { + type: 'link'; + label: string; + href: string; +} +interface ElementUniqueLinks { + type: 'unique-links' + text: string; +} \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/styles/browser-blog.css b/suzzy-v0-standalone/assets/styles/browser-blog.css new file mode 100644 index 0000000..1c8d5b3 --- /dev/null +++ b/suzzy-v0-standalone/assets/styles/browser-blog.css @@ -0,0 +1,385 @@ +html { + scroll-behavior: smooth; +} + +/* Widget: Latest Articles */ +div.widget { + max-width: 1024px; + box-sizing: border-box; + margin: auto; + padding: 16px; +} + +div.widget-wrapper { + width: 100%; + padding: 32px; + margin-top: -56px; + padding-top: 56px; + border: 2px solid var(--background-layer-2); + border-left: none; + border-right: none; + box-sizing: border-box; + background: var(--background-layer-4) url('file://images/background-blog.png'); +} + +div.widget-latest-articles, +div.widget-latest-controls { + width: 100%; + display: flex; + gap: 16px; + overflow: hidden; + scroll-behavior: smooth; + scroll-snap-align: center; +} + +div.widget-latest-controls { + padding: 0 16px; +} + +div.latest-item { + display: flex; + min-width: 100%; + gap: 16px; +} + +img.latest-banner { + background-color: var(--background-layer-4); + border: 2px solid var(--background-layer-1); + height: 256px; + width: calc(256px * 1.78); + aspect-ratio: 16 / 9; + box-sizing: border-box; + object-fit: cover; + border-radius: 8px; +} + +div.latest-content { + flex-basis: 100%; +} + +div.latest-info { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +/* Widget: Latest Articles Controls */ +div.latest-spacer { + width: 100%; + aspect-ratio: 16/9; + height: 0px; +} + +div.latest-controls { + display: flex; + flex-basis: 100%; + justify-content: center; + place-items: center; + gap: 8px; +} + +.dot-circle { + width: 4px; + height: 4px; + padding: 0; + margin: 0 8px; + border-radius: 100%; + background-color: var(--text-layer-1); + transition: var(--transition-time) ease-in-out all; +} + +.dot-active { + width: 8px; + height: 8px; + margin: 0 4px; + background-color: var(--background-layer-0); +} + +/* Browser: Layout */ +div.main-wrapper { + margin: auto; + display: flex; +} + +div.main-aside { + width: 100%; + flex-basis: 25%; + max-width: 25%; + height: fit-content; + border-radius: 16px; +} + +div.main-aside, +div.main-content { + padding: 16px; + box-sizing: border-box; +} + +div.main-content { + max-width: 75%; + flex-basis: 75%; + overflow-x: hidden; + overflow-y: scroll; + box-sizing: border-box; + padding: 16px; +} + +div.main-content::-webkit-scrollbar { + display: none; +} + +/* Browser: Filters */ +a.filter-link { + display: flex; + align-items: center; + gap: 8px; + background: none; + border: none; + border-radius: 8px; + width: 100%; + margin-top: 8px; + padding: 4px 8px; + box-sizing: border-box; + transition: var(--transition-time) ease-in-out background-color; + text-decoration: none; +} + +a.filter-active, +a.filter-link:hover, +a.filter-link:focus-visible { + background-color: var(--background-layer-2); + cursor: pointer; +} + +img.filter-icon { + border-radius: 8px; + border: 2px solid var(--background-layer-1); + box-sizing: border-box; + height: 32px; + width: 32px; +} + +img.icon-small { + height: 24px; + width: 24px; +} + +p.filter-name { + width: 100%; + text-align: left; + overflow: hidden; + word-wrap: break-word; +} + +/* Browser: Articles */ +a.collection-item { + width: 100%; + text-decoration: none; + display: flex; + gap: 16px; + padding: 8px; + box-sizing: border-box; + border-radius: 8px; + transition: var(--transition-time) ease-in-out background-color; +} + +a.collection-item:hover, +a.collection-item:focus-visible { + background-color: var(--background-layer-3); +} + +img.item-banner { + height: 80px; + width: calc(80px * 1.78); + aspect-ratio: 16 / 9; + border-radius: 8px; + border: 2px solid var(--background-layer-1); + box-sizing: border-box; + object-fit: cover; +} + +div.item-content { + flex-basis: 100%; + align-content: center; +} + +div.item-content p.text-header { + padding: 0; +} + +div.item-info { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; +} + +/* Browser: Special */ +div.collection-empty { + width: 100%; + padding: 32px; + text-align: center; + box-sizing: border-box; +} + +div.collection-empty img { + height: 128px; + width: 128px; + image-rendering: pixelated; + object-fit: cover; +} + +div.collection-pageselect { + display: flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + gap: 12px; +} + +/* Article: Chapters */ +div.article-chapters { + position: sticky; + display: grid; + top: 56px; + gap: 8px; + min-width: fit-content; +} + +a.mobile-home { + display: none; + position: fixed; + bottom: 16px; + right: 16px; + width: fit-content; + padding: 4px 12px; + border-color: var(--background-layer-0); + background-color: var(--background-layer-1); + z-index: 999; +} + +div.article-chapters a { + text-decoration: none; + transition: var(--transition-time) ease-in-out color; +} + +div.article-chapters a:hover, +div.article-chapters a[active] { + color: var(--background-layer-0); +} + +div.article-chapters a:hover { + text-decoration: underline; +} + +/* Article: Header */ +img.article-banner { + width: calc(256px* 3); + max-width: 100%; + object-fit: cover; + aspect-ratio: 3 / 1; + border-radius: 8px; + box-sizing: border-box; + border: 2px solid var(--background-layer-1); + margin-bottom: 16px; +} + +div.article-header { + display: flex; + justify-content: space-evenly; + align-items: center; + margin-bottom: 16px; +} + +div.article-header>div.item-info { + justify-content: center; +} + +p.article-header, +div.article-header>p { + text-align: center; +} + +/* Animations */ +div.background-scroll { + animation: background-scroll 160s linear infinite; +} + +@keyframes background-scroll { + 0% { + background-position: 0 0; + } + + 100% { + background-position: 0 160px; + } +} + +/* Mobile Styling */ +@media (max-width: 640px) { + div.main-wrapper { + display: block; + } + + div.main-aside { + flex-basis: 100%; + min-width: 100%; + } + + div.main-content { + flex-basis: 100%; + min-width: 100%; + } + + div.latest-item { + display: grid; + } + + div.item-info { + gap: 8px; + justify-content: left; + } + + div.item-info p.item-bull { + display: none; + } + + img.latest-banner { + width: 100%; + height: fit-content; + } + + div.latest-spacer { + display: none; + } + + a.mobile-home { + display: block; + } + + div.article-chapters { + display: none; + position: relative; + top: 0; + } + + div.article-header { + display: grid; + place-items: center; + justify-content: center; + gap: 8px; + } + + div.widget-wrapper { + padding: 0; + margin: 0; + padding-bottom: 16px; + height: fit-content; + } + + /* Disable Special Effects */ + div.background-scroll { + animation: none; + } + +} \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/styles/browser-library.css b/suzzy-v0-standalone/assets/styles/browser-library.css new file mode 100644 index 0000000..0c9bc99 --- /dev/null +++ b/suzzy-v0-standalone/assets/styles/browser-library.css @@ -0,0 +1,431 @@ +img.element-banner { + /* Specifically for the Banner on the introduction article */ + image-rendering: pixelated; +} + +p { + /* Expand the wall of text to make it a little easier to digest */ + line-height: 1.8; +} + +html, +div.layout-content { + scroll-behavior: smooth; +} + +/* Layout */ +div.wrapper-layout { + position: relative; + max-width: 1024px; + width: 100%; + margin: auto; + display: flex; +} + +div.layout-navigation { + background: var(--background-layer-4) url('file://library/background-navigation.png'); + border: 2px solid var(--background-layer-2); + border-bottom: none; + border-top: none; + width: 300px; + max-width: 300px; + max-height: 100vh; + overflow: scroll; +} + +div.layout-navigation::-webkit-scrollbar, +div.layout-content::-webkit-scrollbar { + display: none; +} + +div.layout-navigation, +div.layout-content { + padding: 16px; + box-sizing: border-box; +} + +div.layout-content { + overflow-y: scroll; + height: 100vh; + flex-basis: 70%; + max-width: 70%; +} + +/* Layout: Navigation */ +img.navigation-logo { + width: 100%; + height: 64px; + object-fit: contain; + box-sizing: border-box; + padding-bottom: 8px; +} + +p.navigation-header { + color: var(--background-layer-0); + font-weight: bold; + font-size: large; + padding-top: 12px; +} + +a.navigation-link { + width: 100%; + display: flex; + text-decoration: none; + padding: 4px 8px; + border-radius: 4px; + box-sizing: border-box; + transition: var(--transition-time) ease-in-out all; +} + +a.navigation-link:hover, +a.navigation-link:focus-visible, +a.navigation-link[active] { + background-color: var(--background-layer-1); +} + +a.navigation-chapter { + display: block; + width: 100%; + padding-left: 16px; + text-decoration: none; + box-sizing: border-box; + transition: var(--transition-time) ease-in-out color; + font-size: small; + /* overflow stuff */ + width: 264px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +a.navigation-chapter:hover, +a.navigation-chapter:focus-visible, +a.navigation-chapter[active] { + color: var(--background-layer-0); +} + +a#skipper:focus, +button.navigation-button { + display: block; + text-align: center; + text-decoration: none; + border-radius: 8px; + box-sizing: border-box; + border: 2px solid var(--background-layer-1); + background-color: var(--background-layer-5); + padding: 4px 8px; + width: 100%; + color: var(--text-layer-3); + transition: var(--transition-time) ease-in-out border-color; + height: fit-content; +} + +a#skipper:focus, +button.navigation-button:hover, +button.navigation-button:focus-visible { + outline: none; + cursor: pointer; + border-color: var(--background-layer-0); +} + +a#skipper { + overflow: hidden; + display: block; + height: 0; +} + +a#skipper:focus { + margin-bottom: 8px; +} + +/* Layout: Search */ +div.wrapper-search { + pointer-events: none; + opacity: 0; + position: fixed; + width: 100%; + height: 100%; + z-index: 999; + left: 0; + top: 0; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(29, 30, 46, .5); +} + +div.search-actions { + display: flex +} + +button.search-close { + min-width: 48px; + height: 48px; + box-sizing: border-box; + border-radius: 0 8px 0 0; + background-color: var(--background-layer-4); + border: 2px solid var(--background-layer-1); + box-sizing: border-box; + font-size: xx-large; + line-height: 100%; + transition: var(--transition-time) ease-in-out all; + color: var(--text-layer-3); + outline: none; +} + +button.search-close:hover, +button.search-close:focus-visible { + border-color: var(--background-layer-0); + /* color: var(--background-layer-0); */ + cursor: pointer; +} + +input.search-input { + width: 552px; + height: 48px; + padding: 12px; + font-size: large; + border-radius: 8px 0 0 0; + box-sizing: border-box; + border: 2px solid var(--background-layer-1); + background-color: var(--background-layer-4); + border-right: none; + outline: none; +} + +input.search-input::placeholder { + color: var(--text-layer-3); +} + +div.search-body { + width: 600px; + max-width: 100%; + height: 320px; + padding: 4px 12px; + box-sizing: border-box; + border: 2px solid var(--background-layer-1); + background-color: var(--background-layer-4); + border-top: none; + display: grid; +} + +div.search-body div.pane { + grid-row: 1; + grid-column: 1; + text-align: center; + width: 100%; + height: 100%; + display: grid; + justify-items: center; + align-content: center; + overflow-y: auto; + opacity: 1; +} + +div.search-body div.pane::-webkit-scrollbar { + width: 4px; +} + +div.search-body div.pane::-webkit-scrollbar-thumb { + background-color: var(--background-layer-1); + border-radius: 4px; +} + +div.search-body div.pane img { + image-rendering: pixelated; + height: 128px; + width: 128px; + padding: 8px; +} + +div.search-body div.pane>span { + color: var(--background-layer-0); + font-size: large; + font-weight: bold; +} + +div.search-body div.pane#results { + /* Pushes results to the top and fixes scroll */ + display: flex; +} + +div.search-result { + height: fit-content; + width: calc(100% - 4px); + border-radius: 8px; + box-sizing: border-box; +} + +a.search-item { + text-decoration: none; + display: block; + padding: 8px 12px; + border-radius: 8px; + box-sizing: border-box; + border: 2px solid var(--background-layer-1); + background-color: var(--background-layer-5); + transition: var(--transition-time) ease-in-out border-color; + margin-bottom: 8px; +} + +a.search-item:hover, +a.search-item:focus-visible { + border-color: var(--background-layer-0); + cursor: pointer; + outline: none; +} + +a.search-item p span { + color: var(--background-layer-0); + text-decoration: underline; + font-weight: bold; +} + +a.search-item p { + padding: 0; + overflow: hidden; + white-space: nowrap; + position: relative; +} + +a.search-item p::after { + content: ''; + position: absolute; + right: 0; + top: 0; + height: 100%; + background: var(--background-layer-5); + background: linear-gradient(90deg, transparent 0%, var(--background-layer-5) 100%); + width: 16px; +} + +div.search-footer { + display: flex; + background: var(--background-layer-4); + border: 2px solid var(--background-layer-1); + border-top: none; + box-sizing: border-box; + border-radius: 0 0 8px 8px; + white-space: nowrap; + padding: 2px 12px; + align-items: center; + gap: 6px; +} + +div.search-footer span { + color: var(--background-layer-0); + font-family: monospace; + font-size: small; + padding: 0; +} + +div.search-footer p { + font-size: small; + padding: 0; +} + +/* Layout: Content */ +a.layout-return { + display: none; + position: fixed; + bottom: 16px; + right: 16px; + width: fit-content; + padding: 4px 12px; + border-color: var(--background-layer-0); + background-color: var(--background-layer-1); + z-index: 999; +} + +/* Animations */ +div.background-scroll { + animation: background-scroll 320s linear infinite; +} + +@keyframes background-scroll { + 0% { + background-position: 0 0; + } + + 100% { + background-position: 0 320px; + } +} + +/* Tablet Styling */ +@media screen and (max-width: 1024px) { + div.layout-navigation { + border-left: none; + } +} + +/* Mobile Styling */ +@media (max-width: 640px) { + div.wrapper-layout { + display: block; + position: relative; + } + + div.layout-navigation { + max-height: fit-content; + } + + div.layout-navigation { + border: none; + min-width: 100%; + height: fit-content; + border-bottom: 1px solid var(--background-layer-1); + /* Remove Shadow */ + filter: none; + } + + div.layout-content { + width: 100%; + max-width: 100%; + height: fit-content; + min-height: 100vh; + } + + img.navigation-logo { + width: 100%; + } + + a.layout-return { + display: block; + } + + div.layout-spacer { + /* Just enough space for a.action-home not to get in the way */ + height: 40px; + } + + /* Disable Special Effects */ + div.background-scroll { + animation: none; + } + + a.navigation-chapter { + width: 100%; + color: var(--text-layer-1) !important; + } + + input.navigation-search { + display: none; + } + + div.navigation-results { + display: none; + } + + div.container-search { + width: calc(100% - 16px); + } + + div.search-body { + border-radius: 0 0 8px 8px; + } + + div.search-footer { + display: none; + } +} \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/styles/public-homepage.css b/suzzy-v0-standalone/assets/styles/public-homepage.css new file mode 100644 index 0000000..b595693 --- /dev/null +++ b/suzzy-v0-standalone/assets/styles/public-homepage.css @@ -0,0 +1,43 @@ +div.widget { + padding: 16px; + box-sizing: border-box; +} + +/* Widget: Hero */ +div.widget-hero-wrapper { + width: 100%; + height: 100vh; + margin-top: -56px; + padding-top: 56px; + display: flex; + box-sizing: border-box; +} + +div.widget-hero { + max-width: 1024px; + width: 100%; + margin: auto; + background: url('file://images/background-home.png') no-repeat center right; +} + +div.widget-hero a { + color: var(--background-layer-0); +} + +/* Mobile Styling */ +@media (max-width: 640px) { + + div.widget-hero { + padding: 16px; + } + + a.game-item { + flex-basis: 100%; + } + + div.widget-hero-wrapper { + padding: 0; + margin: 0; + height: fit-content; + } +} \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/styles/public-projects.css b/suzzy-v0-standalone/assets/styles/public-projects.css new file mode 100644 index 0000000..febe252 --- /dev/null +++ b/suzzy-v0-standalone/assets/styles/public-projects.css @@ -0,0 +1,220 @@ +div.wrapper-footer { + margin-top: 0 !important; +} + +/* Widget: Header */ +div.wrapper-widget-hero { + width: 100%; + display: flex; + box-sizing: border-box; + margin-top: -56px; + padding-top: 56px; + border-bottom: 2px solid var(--background-layer-0); +} + +div.widget-hero { + margin: auto; + width: 100%; + max-width: 1024px; + padding: 64px 0; + text-align: center; + background: url('file://images/background-cat.png') no-repeat left; +} + +/* Widget: Project Layout */ +div.widget-project { + width: 100%; + box-sizing: border-box; + display: grid; + padding: 0; + border-bottom: 2px solid var(--accent); +} + +div.project-background, +div.project-foreground { + grid-column: 1; + grid-row: 1; +} + +div.project-background { + width: 100%; + height: 100%; + opacity: var(--brightness); + background-image: var(--background); + background-repeat: no-repeat; + background-size: cover; + z-index: -1; +} + +div.project-foreground { + max-width: 1024px; + padding: 16px; + margin: auto; +} + +div.project-content { + display: flex; + justify-content: space-between; + gap: 8px; +} + +/* Project: Metadata */ +div.project-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 8px; + gap: 16px; +} + +div.project-header p.text-header { + color: var(--accent); + font-size: x-large; +} + +div.project-platforms { + height: fit-content; + width: fit-content; + display: flex; + white-space: nowrap; + align-items: center; + gap: 8px; +} + +img.platform-icon { + height: 16px; +} + +div.project-info, +div.project-gallery, +div.project-actions, +div.project-gallery-actions { + flex-basis: 50%; + max-width: 50%; +} + +div.project-info { + background-color: rgba(0, 0, 0, 0.5); + box-sizing: border-box; + border-radius: 8px; + margin-bottom: 8px; + padding: 8px; +} + +div.project-description { + padding: 0 8px; +} + +/* Project: Screenshots */ +div.project-gallery { + margin: auto; +} + +div.project-gallery-upper { + display: flex; + flex-wrap: nowrap; +} + +div.project-gallery-scroll { + scroll-behavior: smooth; + display: flex; + overflow-y: hidden; + overflow-x: scroll; + flex-basis: 100%; + margin-bottom: 8px; + gap: 8px; +} + +div.project-gallery-scroll::-webkit-scrollbar { + display: none; +} + +img.gallery-image { + border: 2px solid var(--accent); + box-sizing: border-box; + border-radius: 8px; + min-width: 100%; + aspect-ratio: 16 / 9; +} + +button.project-gallery-button { + min-height: 100%; + flex-basis: 100%; + max-width: 7.5%; + font-size: xx-large; + cursor: pointer; + background-color: transparent; + border: none; + transition: var(--transition-time) ease-in-out color; +} + +button.project-gallery-button:hover, +button.project-gallery-button:focus-visible { + color: var(--accent); +} + +/* Project: Actions */ +div.project-actions { + display: flex; + gap: 8px; +} + +/* Project: Screenshot Controls */ +div.project-gallery-actions { + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + gap: 12px; +} + +div.gallery-dot { + width: 4px; + height: 4px; + margin: 0 8px; + border-radius: 100%; + background-color: var(--text-layer-1); + transition: var(--transition-time) ease-in-out all; +} + +div.gallery-dot-active { + width: 8px; + height: 8px; + margin: 0 6px; + background-color: var(--accent); +} + +/* Mobile Styling */ +@media (max-width: 640px) { + + /* Not redoing all divs so yeah */ + div.project-gallery-actions { + display: none; + } + + div.project-info, + div.project-gallery, + div.project-actions, + div.project-gallery-actions { + flex-basis: 100%; + max-width: 100%; + } + + div.project-content, + div.project-foreground { + display: grid; + gap: 8px; + } + + div.project-lower { + display: flex; + } + + div.project-header { + display: block + } + + img.gallery-image { + width: 100%; + } +} \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/styles/template-article.css b/suzzy-v0-standalone/assets/styles/template-article.css new file mode 100644 index 0000000..051a24e --- /dev/null +++ b/suzzy-v0-standalone/assets/styles/template-article.css @@ -0,0 +1,314 @@ +/* Element: Paragraph Hint */ +span.text-hint, +p.text-quote { + opacity: 0.75; + font-size: small; + font-style: italic; +} + +/* Element: Paragraph Code */ +span.text-code { + font-size: large; + background-color: var(--background-layer-3); + color: var(--background-layer-0); + font-family: monospace; + border-radius: 4px; + padding: 2px 4px; +} + +/* Element: Paragraph Highlight */ +a.link-highlight { + color: var(--background-layer-0); +} + +/* Element: Beanie */ +img.element-beanie { + background: var(--background-layer-4); + width: fit-content; + max-width: 100%; + height: 128px; + margin: 8px; + border-radius: 8px; +} + +/* Element: Header */ +p.element-header { + color: var(--background-layer-0); + font-size: x-large; + font-weight: 600; + border-bottom: 1px dashed var(--background-layer-0); + margin-bottom: 8px; + /* Fixes navbar being on top of element when using anchors */ + padding-top: 64px; + margin-top: -32px; +} + +div.layout-content p.element-header:first-child { + /* Remove Padding for first header in Library */ + padding-top: 0; + margin-top: 0; +} + +p.element-subheader { + color: var(--background-layer-0); + font-weight: 600; + padding-top: 16px; +} + +div.layout-content p.element-subheader:first-child { + /* Remove Padding for first header in Library */ + padding-top: 0; +} + +/* Element: Code Block */ + +div.element-code { + position: relative; + background-color: var(--background-layer-4); + border: 2px solid var(--background-layer-1); + box-sizing: border-box; + padding: 8px 12px; + border-radius: 8px; + margin: 6px 0 12px 0; +} + +div.element-code pre, +div.element-code pre span { + font-family: monospace; + font-size: 14px; + overflow-x: scroll; + width: 100%; + height: 100%; +} + +span.hljs-attr, +span.hljs-keyword, +span.hljs-string { + color: #a1a8ff; +} + +span.hljs-comment { + color: #bababe; +} + +div.element-code:hover { + /* Remove padding so it doesn't shift page content */ + padding-bottom: 4px; +} + +div.element-code:hover pre { + /* Padding for scrollbar so it doesn't touch text directly */ + padding-bottom: 4px; +} + +div.element-code pre::-webkit-scrollbar { + height: 0; +} + +div.element-code:hover pre::-webkit-scrollbar { + height: 4px; +} + +div.element-code pre::-webkit-scrollbar-thumb { + border-radius: 8px; + box-sizing: border-box; + transition: var(--transition-time) ease-in-out all; + background-color: transparent; +} + +div.element-code:hover pre::-webkit-scrollbar-thumb { + background-color: var(--background-layer-0); +} + +div.element-code button { + /* Copy Code Button */ + width: 80px; + position: absolute; + opacity: 0; + top: 4px; + right: 4px; + font-size: small; + transition: none; + transition: var(--transition-time) ease-in-out all; + border-color: var(--background-layer-1); +} + +div.element-code:hover button { + opacity: 1; +} + +/* Element: Banner */ +img.element-banner { + width: 100%; +} + +/* Element: Quote */ +p.text-quote { + padding: 4px 0 8px 0; +} + +/* Element: Audio */ +div.element-audio { + width: 100%; + border: 2px solid var(--background-layer-1); + background-color: var(--background-layer-4); + border-radius: 8px; + margin: 8px 0; + padding: 8px 16px; + box-sizing: border-box; +} + +div.audio-container { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +a.audio-download { + width: 16px; + height: 16px; + opacity: 0.5; + transition: var(--transition-time) ease-in-out opacity; +} + +a.audio-download:hover { + opacity: 1; +} + +button.audio-button { + border: 2px solid var(--background-layer-1); + background-color: var(--background-layer-4); + box-sizing: border-box; + min-width: 32px; + max-width: 32px; + min-height: 32px; + max-height: 32px; + border-radius: 8px; + transition: var(--transition-time) ease-in-out border-color; + text-align: center; +} + +button.audio-button:hover, +button.audio-button:focus-visible { + cursor: pointer; + border-color: var(--background-layer-0); +} + +button.audio-button img { + height: 16px; + width: 16px; +} + +input.audio-slider { + overflow: hidden; + appearance: none; + flex-basis: 100%; + background-color: var(--background-layer-1); + border-radius: 8px; + height: 8px; +} + +input.audio-slider::-webkit-slider-thumb { + appearance: none; + background-color: var(--background-layer-0); + box-shadow: -1008px 0 0 1000px var(--background-layer-0); + border-radius: 100%; + height: 16px; + width: 16px; + transition: var(--transition-time) ease-in-out opacity; +} + +input.audio-slider[disabled]::-webkit-slider-thumb { + opacity: 0.25; +} + +p.audio-time { + font-family: 'Courier New'; + background-color: var(--background-layer-5); + border-radius: 4px; + padding: 4px; + box-sizing: border-box; + min-width: fit-content; + text-align: center; +} + +/* Element: Images & Videos */ +div.element-gallery { + margin: 8px 0; +} + +div.gallery-item { + width: 100%; + display: grid; + height: fit-content; + place-items: center; + box-sizing: border-box; + text-align: center; + gap: 8px; +} + +div.gallery-item video:not(video:fullscreen) { + aspect-ratio: 16 / 9; +} + +div.gallery-item img, +div.gallery-item video:not(video:fullscreen) { + width: 100%; + border-radius: 8px; + background: var(--background-layer-4); + border: 2px solid var(--background-layer-1); +} + +/* Content: List */ +ul.element-list { + margin: 8px 0; +} + +ul.element-list li { + list-style-type: none; + font-size: small; + padding: 2px 0; + opacity: 0.9; +} + +/* Content: Table */ +table.element-table { + width: 100%; + text-align: left; + background-color: var(--background-layer-5); + border: 2px solid var(--background-layer-2); + box-sizing: border-box; + border-spacing: 0; + border-radius: 8px; + overflow: hidden; + margin: 8px 0; + word-break: break-word; +} + +table.element-table th, +table.element-table td { + font-size: small; + font-family: monospace; + padding: 8px; +} + +table.element-table thead { + background-color: var(--background-layer-5); +} + +table.element-table thead th { + font-weight: bold; +} + +table.element-table tbody tr:nth-child(odd) { + background-color: var(--background-layer-3); +} + +/* Mobile Styling */ +@media (max-width: 640px) { + p.element-header { + padding-top: 32px; + margin-top: 0; + } +} \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/styles/template-global.css b/suzzy-v0-standalone/assets/styles/template-global.css new file mode 100644 index 0000000..a7cd861 --- /dev/null +++ b/suzzy-v0-standalone/assets/styles/template-global.css @@ -0,0 +1,250 @@ +/* General Styling and Colors for all pages */ +:root { + --transition-time: 200ms; + --background-layer-5: #1d1e2e; + --background-layer-4: #212335; + --background-layer-3: #282a3f; + --background-layer-2: #2d2f47; + --background-layer-1: #333550; + --background-layer-0: #a1a8ff; + --text-layer-1: #f0f0f0; + --text-layer-2: #181818; + --text-layer-3: #b4b4b4; + --text-error-1: #f63232; +} + +body { + background-color: var(--background-layer-5); + padding: 0; + margin: 0; +} + +p, +a, +pre, +tr, +td, +ul, +li, +noscript, +label, +input, +textarea, +button, +span { + margin: 0; + padding: 0; + color: var(--text-layer-1); + font-family: 'Poppins', Arial, sans-serif; + font-weight: 400; + font-size: 16px; + padding: 4px 0; +} + + +/* Utility: Text */ +.text-hero { + font-weight: 600; + font-size: xxx-large; + color: var(--background-layer-0); +} + +.text-headline { + font-weight: 600; + font-size: xx-large; + color: var(--background-layer-0); + line-height: 1; + padding-top: 0; +} + +.text-header { + color: var(--background-layer-0); + font-size: large; + font-weight: 600; +} + +.text-error { + font-weight: bold; + font-style: italic; + color: var(--text-error-1); +} + +.text-tip { + font-size: small; +} + +.text-muted, +s { + color: var(--text-layer-3); +} + +/* Utility: Buttons */ +button.small { + font-size: small; + border: none; + background-color: transparent; + cursor: pointer; + text-align: left; + width: fit-content; + color: var(--text-layer-1); + transition: var(--transition-time) ease-in-out all; +} + +button.small:focus-visible, +button.small:hover { + color: var(--background-layer-0); +} + +button.small:disabled { + color: var(--text-layer-1) !important; + opacity: 0.5; + cursor: default; +} + +button.default, +a.default { + min-width: fit-content; + width: 100px; + text-align: center; + text-decoration: none; + height: fit-content; + text-wrap: nowrap; + border-radius: 8px; + padding: 4px 8px; + outline: none; + border: none; + cursor: pointer; + color: var(--text-layer-1); + background-color: var(--background-layer-3); + border: 2px solid var(--background-layer-2); + transition: var(--transition-time) ease-in-out all; +} + +a.default:hover, +a.default:focus-visible, +button.default:hover, +button.default:focus-visible { + color: var(--text-layer-1); + border-color: var(--background-layer-0); + background-color: var(--background-layer-1); +} + +a.destructive:hover, +a.destructive:focus-visible, +button.destructive:hover, +button.destructive:focus-visible { + border-color: var(--text-error-1); +} + +*[disabled], +*:disabled { + opacity: 0.75 !important; + background-color: var(--background-layer-3) !important; + border-color: var(--background-layer-2) !important; + pointer-events: none !important; +} + +a.wide, +button.wide { + width: 100%; +} + +a.tall, +button.tall { + height: 100%; +} + +/* Utility: Inputs */ +input.text, +textarea.text { + width: 100%; + height: fit-content; + text-wrap: nowrap; + border-radius: 8px; + box-sizing: border-box; + padding: 8px 16px; + color: var(--text-layer-1); + background-color: var(--background-layer-3); + border: 2px solid var(--background-layer-2); + transition: var(--transition-time) ease-in-out border; + outline: none; +} + +input.text:focus-visible, +textarea.text:focus-visible { + color: var(--text-layer-1); + border-color: var(--background-layer-0); + background-color: var(--background-layer-1); +} + +input.text:required:focus-visible:invalid, +textarea.text:required:focus-visible:invalid { + border: 2px solid var(--text-error-1); +} + +textarea.text { + resize: vertical; + min-height: 45px; + text-wrap: wrap; +} + + +/* Utility: Spacers */ +div.divider-line { + height: 2px; + width: 100%; + margin: 8px 0; + background-color: var(--background-layer-1); +} + +div.divider-dashed { + display: none; + width: 100%; + margin: 8px 0; + border: 1px dashed var(--background-layer-0); + box-sizing: border-box; +} + +div.divider-header { + width: 100%; + border-bottom: 2px solid var(--background-layer-1); + padding-bottom: 8px; + margin-bottom: 8px; + box-sizing: border-box; +} + +div.divider-spacer { + margin: 16px 0; +} + +/* Utility: Shadows & Animations */ +.special-shadow { + filter: drop-shadow(0 20px 13px rgb(0 0 0 / 0.03)) drop-shadow(0 8px 5px rgb(0 0 0 / 0.08)); +} + +.special-dropshadow { + filter: drop-shadow(0 25px 25px rgb(0 0 0 / 0.15)); +} + +.special-pulse { + animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +@keyframes pulse { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: .5; + } +} + +/* Mobile Styling */ +@media (max-width: 640px) { + div.divider-dashed { + display: block; + } +} \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/styles/template-modals.css b/suzzy-v0-standalone/assets/styles/template-modals.css new file mode 100644 index 0000000..d271419 --- /dev/null +++ b/suzzy-v0-standalone/assets/styles/template-modals.css @@ -0,0 +1,139 @@ +/* Modal Layout */ +div.modal-wrapper { + position: fixed; + width: 100%; + height: 100%; + background-color: rgba(29, 30, 46, .5); + display: flex; + justify-content: center; + align-items: center; + opacity: 0; + transition: var(--transition-time) ease-in opacity; + backdrop-filter: blur(4px); +} + +div.modal-content { + min-width: 600px; + max-width: 600px; + border-radius: 16px; + padding: 24px 16px; + box-sizing: border-box; + border: 2px solid var(--background-layer-1); + background-color: var(--background-layer-5); + overflow: hidden; + display: flex; + gap: 24px; +} + +div.modal-view { + height: fit-content; + min-width: 100%; +} + +div.modal-elements { + display: grid; +} + +div.modal-footer { + width: 100%; + display: flex; + align-items: center; + gap: 8px; +} + +div.modal-divider { + margin: 8px 0; + height: 2px; + width: 100%; + background-color: var(--background-layer-1); +} + +div.modal-spacer { + height: 16px; +} + +div.modal-flex { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +/* Modal Elements */ +p.modal-alert { + color: var(--text-error-1); + text-overflow: ellipsis; + font-style: italic; + font-weight: bolder; + white-space: nowrap; + overflow: hidden; + flex-basis: 100%; +} + +p.modal-headline { + font-weight: 600; + font-size: xx-large; + color: var(--background-layer-0); + line-height: 1; + padding-top: 0; +} + +p.modal-header { + color: var(--background-layer-0); + font-size: large; + font-weight: 600; +} + +p.modal-link-text { + font-size: small; +} + +a.modal-link, +p.modal-link { + font-size: small; + font-style: italic; + width: fit-content; +} + +a.modal-link:hover, +a.modal-link:focus-visible { + color: var(--background-layer-0); +} + + +label.modal-label { + font-size: small; +} + +label.modal-label[error] { + font-style: italic; + color: var(--text-error-1); +} + +input.modal-input[type=text], +input.modal-input[type=password] { + border-radius: 4px; + padding: 4px 8px; + outline: none; + border: none; + cursor: pointer; + color: var(--text-layer-1); + background-color: var(--background-layer-3); + border: 2px solid var(--background-layer-1); + transition: var(--transition-time) ease-in-out all; +} + +input.modal-input[type=text]:focus-visible, +input.modal-input[type=password]:focus-visible { + border-color: var(--background-layer-0); +} + + + +/* Mobile Styling */ +@media (max-width: 640px) { + div.modal-content { + min-width: 0; + max-width: 100%; + margin: 0 8px; + } +} \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/styles/template-profile.css b/suzzy-v0-standalone/assets/styles/template-profile.css new file mode 100644 index 0000000..75e0c1f --- /dev/null +++ b/suzzy-v0-standalone/assets/styles/template-profile.css @@ -0,0 +1,103 @@ +/* Layout */ +div.wrapper-layout { + max-width: 1024px; + width: 100%; + margin: auto; + display: flex; +} + +div.wrapper-navigation { + width: 100%; + flex-basis: 25%; + max-width: 25%; + box-sizing: border-box; + padding: 16px; +} + +div.wrapper-content { + height: 100vh; + max-width: 75%; + flex-basis: 75%; + overflow-x: hidden; + overflow-y: scroll; + box-sizing: border-box; + padding: 16px; + border: 2px solid var(--background-layer-2); + border-top: none; + border-bottom: none; +} + +div.wrapper-content::-webkit-scrollbar { + display: none; +} + +/* Layout: Navigation */ +div.wrapper-navigation { + display: flex; + flex-wrap: wrap; + flex-direction: column; + gap: 2px; +} + +img.navigation-logo { + width: 100%; + height: 80px; + object-fit: contain; +} + +a.navigation-link { + display: flex; + align-items: center; + gap: 4px; + height: fit-content; + width: 100%; + border-radius: 8px; + padding: 4px 8px; + cursor: pointer; + text-decoration: none; + text-align: left; + box-sizing: border-box; + transition: var(--transition-time) ease-in-out all; +} + +a.navigation-link[active], +a.navigation-link:hover, +a.navigation-link:focus-visible { + outline: 0; + background-color: var(--background-layer-1); + color: var(--text-layer-1); + fill: var(--text-layer-1); +} + +a.navigation-link img { + fill: var(--text-layer-1); + box-sizing: border-box; + padding: 2px; + height: 24px; + width: 24px; +} + +/* Layout: Content */ +div.collection-divider { + margin: 16px 0; + border-bottom: 2px dashed var(--background-layer-0); +} + +div.collection-alert { + width: 100%; + height: fit-content; + padding: 24px; + box-sizing: border-box; + display: grid; + justify-items: center; + gap: 8px; +} + +div.collection-alert[hidden] { + display: none; +} + +div.collection-alert img { + height: 48px; + width: 48px; +} \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/styles/template-public.css b/suzzy-v0-standalone/assets/styles/template-public.css new file mode 100644 index 0000000..5e620a8 --- /dev/null +++ b/suzzy-v0-standalone/assets/styles/template-public.css @@ -0,0 +1,206 @@ +/* Styling for all views in the public */ +/* Navigation: Layout */ +div.wrapper-navigation { + width: 100%; + position: fixed; + top: 0; + z-index: 999; + background: var(--background-layer-5); + background: linear-gradient(0deg, transparent 0%, var(--background-layer-5) 100%); +} + +div.container-navigation { + display: flex; + padding: 8px 24px; + max-width: 1024px; + width: 100%; + margin: auto; + box-sizing: border-box; + justify-content: space-between; + align-items: center; +} + +div.navigation-side { + height: 100%; + display: flex; + align-items: center; + gap: 16px; +} + +div.divider-navigation { + height: 56px; +} + +/* Navigation: Left-Hand */ +img.navigation-logo { + object-fit: contain; + width: 100%; + height: 32px; + transition: var(--transition-time) ease-in-out transform; +} + +img.navigation-logo:hover { + transform: scale(1.1) rotate(-3deg); +} + +a.text-link { + text-align: center; + transition: var(--transition-time) ease-in-out all; + text-decoration: none; +} + +a.text-link:focus-visible, +a.text-link:hover { + color: var(--background-layer-0); + text-decoration: underline; +} + +/* Navigation: Right-Hand */ +a#navigation-status, +a#navigation-profile { + padding: 0; + text-decoration: none; + border: 2px solid var(--background-layer-1); + background-color: var(--background-layer-4); + transition: var(--transition-time) ease-in-out border-color; +} + +a#navigation-status:focus-visible, +a#navigation-status:hover, +a#navigation-profile:focus-visible, +a#navigation-profile:hover { + border-color: var(--background-layer-0); + cursor: pointer; +} + +a#navigation-status { + border-radius: 8px; + height: 32px; + padding: 0 8px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +a#navigation-profile { + height: 40px; + width: 40px; + border-radius: 100%; + background-size: contain; + background-image: url('file://images/default-anon.png'); +} + +div.status-dot { + width: 10px; + height: 10px; + border-radius: 100%; + background-color: #fff; + border: 2px solid #e0e0e0; +} + +p#status-text { + min-width: 80px; + text-align: center; +} + +div.status-green { + background-color: #2ecc71; + border-color: #229954; +} + +div.status-yellow { + background-color: #f1c40f; + border-color: #be9a0b; +} + +div.status-red { + background-color: #ff005b; + border-color: #cc0048; +} + +/* Page: Content */ +div.wrapper-content { + max-width: 1024px; + width: 100%; + margin: auto; +} + +/* Page: Footer */ +div.wrapper-footer { + border-top: 2px solid var(--background-layer-2); + background: var(--background-layer-4) url('file://images/public-footer.gif') no-repeat center right 8px; +} + +div.container-footer { + padding: 24px 0; + text-align: center; + place-content: center; + display: grid; + gap: 4px; +} + +div.footer-links { + display: flex; + justify-content: center; + gap: 16px; +} + +a.footer-link img { + object-fit: contain; + height: 24px; + width: 32px; +} + +a.footer-link { + opacity: 0.5; + transition: var(--transition-time) ease-in-out opacity; +} + +a.footer-link:hover, +a.footer-link:focus-visible { + opacity: 1; +} + +/* Mobile Styling */ +div.divider-mobile { + display: none; +} + +@media (max-width: 640px) { + a.text-link { + width: 100%; + display: block; + text-align: center; + } + + div#navigation-left p.text-muted { + display: none; + } + + div.divider-mobile { + display: block !important; + } + + div.container-navigation { + display: block; + padding: 16px 24px; + } + + div.navigation-side { + justify-content: center; + } + + div#navigation-left { + display: block; + } + + div.wrapper-navigation { + position: relative; + } + + div.divider-navigation { + display: none; + } + +} \ No newline at end of file diff --git a/suzzy-v0-standalone/assets/styles/template-special.css b/suzzy-v0-standalone/assets/styles/template-special.css new file mode 100644 index 0000000..b7c2e69 --- /dev/null +++ b/suzzy-v0-standalone/assets/styles/template-special.css @@ -0,0 +1,134 @@ +div.wrapper-special { + position: absolute; + width: 100%; + height: 100%; + margin: auto; + display: grid; + place-items: center; + gap: 8px; +} + +div.container-special { + max-width: 1024px; + width: 100%; + display: grid; + gap: 8px; + text-align: center; + justify-content: center; + justify-items: center; +} + +div.special-alongside { + display: flex; + flex-wrap: wrap; + align-items: center; + overflow: hidden; + gap: 8px; +} + +img.special-banner { + width: 300px; + margin: 8px; + object-fit: contain; + image-rendering: pixelated; +} + +img.special-corner { + position: absolute; + height: 100%; + bottom: 0; + right: 0; + z-index: -999; +} + +/* Page: oAuth2 */ +div.oauth2-centered { + display: grid; + place-items: center; + text-align: center; + gap: 16px; +} + +div.oauth2-header { + text-align: center; +} + +div.oauth2-icons { + display: flex; + justify-content: space-evenly; + padding: 0 32px; + box-sizing: border-box; + align-items: center; +} + +img.oauth2-diagram { + width: 64px; + flex-basis: 100%; +} + +img.application-icon { + background-color: var(--background-layer-1); + min-height: 80px; + max-height: 80px; + min-width: 80px; + max-width: 80px; +} + +img.oauth2-icon { + background-color: var(--background-layer-3); + width: 64px; + height: 64px; + margin: auto; + border-radius: 100%; + padding: 16px; + border: 2px solid var(--background-layer-1); + object-fit: contain; + image-rendering: pixelated; +} + +div.oauth2-container { + width: fit-content; + min-width: 400px; + min-height: 400px; + max-width: 400px; +} + +/* Page: Picross */ +div.special-picross { + margin-left: -48px; + display: grid; +} + +div.picross-row { + display: flex; + gap: 4px; +} + +p.picross-hint { + color: var(--background-layer-0); + width: 24px; + height: 24px; +} + +button.picross-button { + transition: var(--transition-time) ease-in-out background-color; + background-color: var(--background-layer-1); + width: 24px; + height: 24px; + border: none; +} + +button.picross-active { + background-color: var(--background-layer-0) !important; +} + +button.picross-ignore { + background-color: var(--background-layer-4); +} + +/* Mobile Styling */ +@media (max-width: 640px) { + div.special-picross { + margin-left: 0px; + } +} \ No newline at end of file diff --git a/suzzy-v0-standalone/package-lock.json b/suzzy-v0-standalone/package-lock.json new file mode 100644 index 0000000..df96e7d --- /dev/null +++ b/suzzy-v0-standalone/package-lock.json @@ -0,0 +1,2187 @@ +{ + "name": "@suzzy/site", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@suzzy/site", + "version": "1.0.0", + "dependencies": { + "clean-css": "^5.3.3", + "express": "^4.21.2", + "highlight.js": "^11.11.1", + "node-html-parser": "^6.1.13", + "pug": "^3.0.3", + "sharp": "^0.33.5", + "uglify-js": "^3.19.3" + }, + "devDependencies": { + "@types/clean-css": "^4.2.11", + "@types/express": "^5.0.0", + "@types/node": "^22.10.1", + "@types/pug": "^2.0.10", + "@types/uglify-js": "^3.17.5", + "dotenv": "^16.4.7", + "typescript": "^5.9.3" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/clean-css": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.11.tgz", + "integrity": "sha512-Y8n81lQVTAfP2TOdtJJEsCoYl1AnOkqDqMvXb9/7pfgZZ7r8YrEyurrAvAoAjHOGXKRybay+5CsExqIH6liccw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "source-map": "^0.6.0" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/pug": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", + "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/uglify-js": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.5.tgz", + "integrity": "sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/assert-never": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.3.0.tgz", + "integrity": "sha512-9Z3vxQ+berkL/JJo0dK+EY3Lp0s3NtSnP3VCLsh5HDcZPrh0M+KQRK5sWhUeyPPH+/RCxZqOxLMR+YC6vlviEQ==", + "license": "MIT" + }, + "node_modules/babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.9.6" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "license": "MIT", + "dependencies": { + "is-regex": "^1.0.3" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/constantinople": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-expression": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "object-assign": "^4.1.1" + } + }, + "node_modules/is-expression/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "license": "MIT" + }, + "node_modules/jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", + "license": "MIT", + "dependencies": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "license": "MIT", + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pug": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz", + "integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==", + "license": "MIT", + "dependencies": { + "pug-code-gen": "^3.0.3", + "pug-filters": "^4.0.0", + "pug-lexer": "^5.0.1", + "pug-linker": "^4.0.0", + "pug-load": "^3.0.0", + "pug-parser": "^6.0.0", + "pug-runtime": "^3.0.1", + "pug-strip-comments": "^2.0.0" + } + }, + "node_modules/pug-attrs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "js-stringify": "^1.0.2", + "pug-runtime": "^3.0.0" + } + }, + "node_modules/pug-code-gen": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz", + "integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==", + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.2", + "pug-attrs": "^3.0.0", + "pug-error": "^2.1.0", + "pug-runtime": "^3.0.1", + "void-elements": "^3.1.0", + "with": "^7.0.0" + } + }, + "node_modules/pug-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", + "license": "MIT" + }, + "node_modules/pug-filters": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "jstransformer": "1.0.0", + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0", + "resolve": "^1.15.1" + } + }, + "node_modules/pug-lexer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", + "license": "MIT", + "dependencies": { + "character-parser": "^2.2.0", + "is-expression": "^4.0.0", + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-linker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-load": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0", + "token-stream": "1.0.0" + } + }, + "node_modules/pug-runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", + "license": "MIT" + }, + "node_modules/pug-strip-comments": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/with": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "assert-never": "^1.2.1", + "babel-walk": "3.0.0-canary-5" + }, + "engines": { + "node": ">= 10.0.0" + } + } + } +} diff --git a/suzzy-v0-standalone/package.json b/suzzy-v0-standalone/package.json new file mode 100644 index 0000000..f319886 --- /dev/null +++ b/suzzy-v0-standalone/package.json @@ -0,0 +1,30 @@ +{ + "private": true, + "version": "1.0.0", + "name": "@suzzy/site", + "main": "source/service.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "npx tsc && node ./dist/service.js", + "preview": "npx tsc && set NODE_ENV=production && node ./dist/service.js", + "dev": "npx tsc && node ./dist/service.js" + }, + "dependencies": { + "clean-css": "^5.3.3", + "express": "^4.21.2", + "highlight.js": "^11.11.1", + "node-html-parser": "^6.1.13", + "pug": "^3.0.3", + "sharp": "^0.33.5", + "uglify-js": "^3.19.3" + }, + "devDependencies": { + "@types/clean-css": "^4.2.11", + "@types/express": "^5.0.0", + "@types/node": "^22.10.1", + "@types/pug": "^2.0.10", + "@types/uglify-js": "^3.17.5", + "dotenv": "^16.4.7", + "typescript": "^5.9.3" + } +} diff --git a/suzzy-v0-standalone/source/class/articles.ts b/suzzy-v0-standalone/source/class/articles.ts new file mode 100644 index 0000000..828e312 --- /dev/null +++ b/suzzy-v0-standalone/source/class/articles.ts @@ -0,0 +1,414 @@ +import { Locals, Manifest, ManifestArticle, ManifestEntry } from '../types/articles' +import { PRODUCTION } from '../constants' +import parseArticle from '../functions/parseArticle' +import processView from '../functions/processView' +import Log from '../functions/logger' + +import { existsSync, readFileSync, watch } from 'fs' +import { Request, Response } from 'express' +import { createHash } from 'crypto' +import { join } from 'path' + +type ArticleID = number +type ElementID = number +type WordID = number +const stripSpaces = (t: string) => t.replaceAll(/[\s]{2,}/gm, ' ').trim() +const stripSpecial = (t: string) => t.replaceAll(/[^a-z0-9_-\s]/gm, ' ') +const difference = (a: string, b: string): number => { + let match = b.length + if (Math.abs(a.length - b.length) < 2) { + for (const i in b.split('')) { + if (a[i] === b[i]) match-- + } + } + return match +} + +export class Articles { + private path: string + private directory: string + private articles = new Array() + private categories = new Array() + private authors = new Array() + private indexWord: Record> = {} + private indexData = new Array>>( + // Article > Elements > Words + ) + private views: Record = {} + + constructor(folder: string, path: string) { + this.directory = join('views', folder) + this.path = path + if (!PRODUCTION) { + Log('DEBUG', 'Articles', `${this.directory}: Watching Directory`) + let timeout: ReturnType + let callback = () => { + Log('INFO', 'Articles', `${this.directory}: Changes Detected, Reloading...`) + this.reload().catch(e => { + Log('ERROR', 'Articles', `${this.directory}: Reload Error Caught`, e) + }) + } + watch(this.directory, { recursive: true }, () => { + clearTimeout(timeout) + timeout = setTimeout(() => callback(), 10) + }) + } + this.reload() + } + + private async reload() { + this.authors = [] + this.articles = [] + this.categories = [] + this.indexData = [] + this.indexWord = {} + + // Validate Folder Structure + if (!existsSync(join(this.directory, 'browser.pug'))) + throw `${this.directory}: File 'browser.pug' is required` + if (!existsSync(join(this.directory, 'post.pug'))) + throw `${this.directory}: File 'post.pug' is required` + if (!existsSync(join(this.directory, 'manifest.json'))) + throw `${this.directory}: File 'manifest.json' is required` + if (!existsSync(join(this.directory, 'articles'))) + throw `${this.directory}: Folder 'articles' is required` + + // Read and Parse Manifest + let manifest!: Manifest + try { + const data = readFileSync(join(this.directory, 'manifest.json'), 'utf8') + manifest = JSON.parse(data) + + if (!Array.isArray(manifest.categories)) + throw `Field 'categories' must be an array` + if (!Array.isArray(manifest.authors)) + throw `Field 'authors' must be an array` + if (!Array.isArray(manifest.articles)) + throw `Field 'articles' must be an array` + if (manifest.itemsPerPage !== undefined && typeof manifest.itemsPerPage !== 'number') + throw `Field 'itemsPerPage' must be a number` + if (manifest.noBrowser !== undefined && typeof manifest.noBrowser !== 'boolean') + throw `Field 'noBrowser' must be a boolean` + + let i = 0 + for (const item of manifest['articles']) { + if (typeof item !== 'string') + throw `${i}: Must of 'string' type` + i++ + } + i = 0 + + for (const field of [manifest['categories'], manifest['authors']]) { + let m: Record = {} + for (const item of field) { + if (item.id === undefined) + throw `${i}: Missing field 'id'` + if (item.icon === undefined) + throw `${i}: Missing field 'icon'` + if (item.name === undefined) + throw `${i}: Missing field 'name'` + if (m[item.id]) + throw `${i}: Duplicate ID with index ${m[item.id]}` + i++ + } + } + this.categories = manifest['categories'] + this.authors = manifest['authors'] + } catch (e) { + throw `${this.directory}: Unable to Parse Manifest (${e})` + } + + // Read and Parse Articles + for (const filepath of manifest.articles) { + try { + const path = join(this.directory, 'articles', filepath) + const file = readFileSync(path, 'utf8') + const [attributes, elements] = parseArticle(file) + for (const field of [ + 'id', 'created', 'categoryId', 'authorId', + 'title', 'snippet', 'banner', 'noindex' + ]) { + if (attributes[field] === undefined) + throw `Attribute '${field}' must be initialized` + } + const author = this.authors.find(c => c.id === attributes.authorId) + if (!author) { + throw `Author ID is invalid` + } + const category = this.categories.find(c => c.id === attributes.categoryId) + if (!category) { + throw `Category ID is invalid` + } + this.articles.push({ + 'info': { + 'id': attributes.id, + 'created': attributes.created, + 'categoryId': attributes.categoryId, + 'authorId': attributes.authorId, + 'title': attributes.title, + 'snippet': attributes.snippet, + 'banner': attributes.banner, + 'noindex': attributes.noindex, + }, + 'author': author, + 'category': category, + 'elements': elements, + 'path': path, + }) + } catch (e) { + throw `${this.directory}: Unable to Parse Article '${filepath}' (${e})` + } + } + this.articles.sort((a, b) => + new Date(b.info.created).getTime() - + new Date(a.info.created).getTime() + ) + + // Generate Article Indexes + this.articles.forEach((article, articleIndex) => { + if (article.info.noindex === 'true') return + + const textElements = article.elements.map(e => { + switch (e.type) { + case 'header': + case 'subheader': return e.value + case 'text': return e.items.map(e => e.content).join(' ') + case 'list': return e.items.join(' , ') + case 'quote': return e.value + case 'audio': return `[AUDIO]` + case 'image': return `[IMAGE]` + case 'banner': return `[BANNER]` + case 'beanie': return `[BEANIE]` + case 'video': return `[VIDEO]` + case 'code': return `[CODE]` + case 'table': return `[TABLE]` + } + }) + + this.indexData.push( + textElements.map((element, elementIndex) => { + const words = stripSpaces(element).split(' ') + words.forEach((word, wordIndex) => { + word = word.toLowerCase() + + // Add Entire Word to Index + if (!this.indexWord[word]) this.indexWord[word] = [] + this.indexWord[word].push([ + articleIndex, + elementIndex, + wordIndex, + ]) + console.log(word) + + // Remove Special Characters and Split Word + const subwords = stripSpaces(stripSpecial(word)).split(' ') + console.log(subwords) + subwords.forEach(subword => { + subword = subword.toLowerCase() + if (!this.indexWord[subword]) this.indexWord[subword] = [] + this.indexWord[subword].push([ + articleIndex, + elementIndex, + wordIndex, + ]) + }) + + }) + return words + }) + ) + }) + + { + const T = performance.now() + const searchQuery = 'Get Widgets' + const searchWords = stripSpaces(searchQuery.trim().toLowerCase()).split(' ', 16) + + // Find Starting Words + const sapling = new Array() + for (const word of Object.keys(this.indexWord)) { + if (difference(word, searchWords[0]) < 2) { + sapling.push(word) + } + } + // Traverse Words + console.log(sapling) + for (const index of sapling) { + for (const branch of this.indexWord[index]) { + + console.log(index, branch) + } + } + + console.log('processing time:', performance.now() - T) + } + + // Pre-Render all Browser Pages + if (!manifest.itemsPerPage) manifest.itemsPerPage = 8 + if (!manifest.noBrowser) { + + // Add undefined so we can generate an 'all' category + for (const index in [undefined, ...this.categories]) { + + const category = this.categories[index] + const relevantArticles = this.articles.filter(a => !category || a.category.id === category.id) + const pageTotal = Math.ceil(relevantArticles.length / manifest.itemsPerPage) || 1 + + for (let pageIndex = 1; pageIndex < pageTotal + 1; pageIndex++) { + try { + const l: Locals = { + 'articles': this.articles, + 'relevantArticle': undefined, + 'relevantArticles': relevantArticles, + 'relevantCategory': category, + 'authors': this.authors, + 'categories': this.categories, + 'pageCurrent': pageIndex, + 'pageOffset': (pageIndex - 1) * manifest.itemsPerPage, + 'pageIndex': pageIndex, + 'pageTotal': pageTotal, + } + l.relevantArticles = l.relevantArticles.slice( + l.pageOffset, + l.pageOffset + manifest.itemsPerPage + ) + + const document = await processView(join(this.directory, 'browser.pug'), l) + const view = { + 'content': document, + 'size': Buffer.byteLength(document), + 'hash': createHash('md5').update(document).digest('hex'), + } + const path = (category ? `${this.path}/${category.id}` : `${this.path}`) + if (pageIndex === 1) this.views[path] = view + this.views[`${path}/${pageIndex}`] = view + + } catch (e) { + throw `Render Error: '${category?.id || 'all'}/${pageIndex}' (${e})` + } + } + } + } + + // Pre-Render all Article Pages + for (const article of this.articles) { + try { + const l: Locals = { + 'articles': this.articles, + 'relevantArticle': article, + 'relevantArticles': [], + 'relevantCategory': article.category, + 'authors': manifest.authors, + 'categories': manifest.categories, + 'pageIndex': -1, + 'pageOffset': -1, + 'pageTotal': -1, + 'pageCurrent': -1, + } + const document = await processView(join(this.directory, 'post.pug'), l) + this.views[`${this.path}/${article.category.id}/${article.info.id}`] = { + 'content': document, + 'size': Buffer.byteLength(document), + 'hash': createHash('md5').update(document).digest('hex'), + } + } catch (e) { + throw `Render Error: '${article.category.id}/${article.info.id}' (${e})` + } + } + + } + + // Serve a Browser Request with path (Paths: '/my-articles', '/my-articles/:cat', '/my-articles/:cat/:opt') + public serveContent = this._serveContent.bind(this) + private _serveContent(req: Request, res: Response) { + let key = this.path + if (req.params['cat']) key += `/${req.params['cat']}` + if (req.params['opt']) key += `/${req.params['opt']}` + const view = this.views[key] + if (view) { + if (req.headers['if-none-match'] === view.hash) { + res.status(304).end() + } else { + res.setHeader('Content-Type', 'text/html') + res.setHeader('Content-Length', view.size) + res.setHeader('Etag', view.hash) + res.end(view.content) + } + return + } + // Continue to 404 Handler + if (req.next) req.next() + } + + // Search for Snippet of text in all article (Path: '/my-articles/snippet') + public serveSnippet = this._serveSnippet.bind(this) + private _serveSnippet(req: Request, res: Response) { + res.end('todo') + } +} + + + +// // Parse Search Query +// // const Query = (key: string): string => (typeof req.query[key] === 'string' ? req.query[key] : '') +// const search = Query('query') || Query('q') +// if (!search) { +// res.json([]) +// return +// } + +// // Find First Word +// const query = this.formatSnippet(search, 12) +// if (!query.length) { +// res.json([]) +// return +// } +// const matches = this.indexWords[query[0]] +// if (!matches) { +// res.json([]) +// return +// } + +// // Generate Results +// const results = new Array<{ +// score: number +// link: string +// snippet: string +// location: string +// }>() +// for (const { wordIndex: wi, articleIndex: ai, elementIndex: ei } of matches) { +// const a = this.articles[ai] +// const e = a.elements[ei] as HeaderElement | SubheaderElement +// let score = 0, o = 0 +// let matchWords = new Array() +// for (const queryWord of query) { +// const articleWord = a.words[wi + o] +// if (!articleWord) break +// if (queryWord === this.formatWord(articleWord)) { +// score++ +// matchWords.push(articleWord) +// } else { +// break +// } +// o++ +// } +// if (query.length === 1 || score > 1) { +// const location = [a.category?.name, a.info.title, e.type === 'header' ? '' : '...', e.value].filter(e => e).join(' > ') +// const link = `${this.path}/${a.info.categoryId}/${a.info.id}#${formatHref(e.value)}` +// const ql = matchWords.length +// const snippet = [ +// ...a.words.slice(wi - 3, wi), // Last 3 Words +// `${matchWords.join(' ')}`, // Search Query +// ...a.words.slice(ql + wi, ql + wi + 5) // Next 5 Words +// ].join(' ') +// results.push({ score, link, snippet, location }) +// } +// } + +// // Sort Results +// results.sort((a, b) => b.score - a.score) +// res.json(results) \ No newline at end of file diff --git a/suzzy-v0-standalone/source/constants.ts b/suzzy-v0-standalone/source/constants.ts new file mode 100644 index 0000000..b88b8c1 --- /dev/null +++ b/suzzy-v0-standalone/source/constants.ts @@ -0,0 +1,107 @@ +import sharp from "sharp" +try { require('dotenv').config() } catch (e) { } + +// Export Variables +const PARSED_ADDR = /^([A-z0-9.]+):([0-9]+)$/.exec(process.env['WEB_ADDR'] || 'localhost:8080') +if (PARSED_ADDR === null) { + process.stdout.write(`[ENV] Malformed TCP Address for Variable 'WEB_ADDR' (Ex. localhost:8080, 127.0.0.1:8080)\n`) + process.exit(4) +} +export const PRODUCTION = (process.env['NODE_ENV']?.trim() == 'production') +export const WEB_PORT = parseInt(PARSED_ADDR[2]!) +export const WEB_ADDR = PARSED_ADDR[1]! + +// Constants +export const PROCESSOR_PREFIX = 'file://' +export const PROCESSOR_DIRECT = 'https://suzzygames.com' // todo: reimplement direct urls for og tags and such +export const PROCESSOR_INDEX: Record = {} +export const PROCESSOR_ASSETS = new Array<{ + type: string; + size: number; + file: string | Buffer; +}>() + +// Presets Available to Image Processor +export const PROCESSOR_PRESETS: Record = { + 'icon': { + height: 64, + width: 64, + quality: 100, + format: 'png', + resize: {} + }, + 'branding': { + height: 128, + width: 128, + quality: 80, + format: 'png', + resize: { + fit: 'contain', + kernel: 'lanczos3', + background: { + r: 0, + g: 0, + b: 0, + alpha: 0 + }, + } + }, +} + +// Common extensions and their MIME types +export const CONTENT_TYPE_MAP: Record = { + // Text and Web + 'html': 'text/html', + 'htm': 'text/html', + 'js': 'text/javascript', + 'css': 'text/css', + 'json': 'application/json', + 'xml': 'application/xml', + 'csv': 'text/csv', + 'txt': 'text/plain', + 'md': 'text/markdown', + // Images + 'png': 'image/png', + 'jpeg': 'image/jpeg', + 'jpg': 'image/jpeg', + 'gif': 'image/gif', + 'bmp': 'image/bmp', + 'webp': 'image/webp', + 'svg': 'image/svg+xml', + 'ico': 'image/x-icon', + 'tiff': 'image/tiff', + 'heic': 'image/heic', + // Audio + 'mp3': 'audio/mpeg', + 'wav': 'audio/wav', + 'ogg': 'audio/ogg', + 'm4a': 'audio/mp4', + 'flac': 'audio/flac', + 'opus': 'audio/opus', + // Video + 'mp4': 'video/mp4', + 'webm': 'video/webm', + 'avi': 'video/x-msvideo', + 'mov': 'video/quicktime', + 'mkv': 'video/x-matroska', + // Fonts + 'woff': 'font/woff', + 'woff2': 'font/woff2', + 'ttf': 'font/ttf', + 'otf': 'font/otf', + 'eot': 'application/vnd.ms-fontobject', + // Application files + 'pdf': 'application/pdf', + 'zip': 'application/zip', + 'gz': 'application/gzip', + 'tar': 'application/x-tar', + 'rar': 'application/vnd.rar', + '7z': 'application/x-7z-compressed', + 'exe': 'application/vnd.microsoft.portable-executable', +} \ No newline at end of file diff --git a/suzzy-v0-standalone/source/functions/logger.ts b/suzzy-v0-standalone/source/functions/logger.ts new file mode 100644 index 0000000..3d1e8dc --- /dev/null +++ b/suzzy-v0-standalone/source/functions/logger.ts @@ -0,0 +1,17 @@ +import { inspect } from 'util' + +export default function Log( + severity: 'FATAL' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'DATA', + service: string, + message: string, + data?: any +) { + const d = new Date() + const t = `${d.toDateString().slice(4)} ${d.toTimeString().slice(0, 8)}` + process.stdout.write(`${t} [${severity}] ${service}: ${message}\x1b[0m\n`) + if (data) { + process.stdout.write(inspect(data, false, Infinity, true)) + process.stdout.write('\n---\n\n') + } + if (severity === 'FATAL') process.exit(1) +} \ No newline at end of file diff --git a/suzzy-v0-standalone/source/functions/parseArticle.ts b/suzzy-v0-standalone/source/functions/parseArticle.ts new file mode 100644 index 0000000..82096f5 --- /dev/null +++ b/suzzy-v0-standalone/source/functions/parseArticle.ts @@ -0,0 +1,132 @@ +import { RawAttributes } from 'node-html-parser/dist/nodes/html' +import { Element, TableElement } from '../types/articles' +import { parse } from 'node-html-parser' +import hljs from "highlight.js" +import { inspect } from 'util' + +/** Parses and extracts elements from an Article */ +export default function parseArticle(someDocument: string): [RawAttributes, Array] { + const clean = (s: string) => s.split('\n').map(s => s.trim()).join(' ').trim() + const root = parse(someDocument, {}).querySelector('article') + const data = new Array() + if (!root || root.tagName !== 'ARTICLE') { + throw 'Expected Article Tag' + } + for (const tag of root.childNodes) { + const tagName = tag.rawTagName.trim().toLowerCase() + // @ts-expect-error + const element = tag as HTMLElement + + switch (tagName) { + + case 'quote': + case 'header': + case 'subheader': + data.push({ + 'type': tagName, + 'value': tag.textContent + }) + break + + case 'text': + data.push({ + 'type': 'text', + 'items': ( + tag.childNodes.map(child => { + const data: Record = {} + if (child.rawTagName === 'a') { + // @ts-expect-error + const c = child as HTMLElement + data.href = c.getAttribute('href')! + data.target = c.getAttribute('target')! + } + return Object.assign(data, { + 'tag': child.rawTagName, + 'content': clean(child.textContent), + }) + }) + .filter(c => c.content.length !== 0) + ) + }) + break + + case 'code': + const code = tag.textContent.trim() + const syntax = element.getAttribute('syntax')! + data.push({ + 'type': 'code', + 'content': ( + syntax !== 'skip' + ? hljs.highlight(code, { 'language': syntax }).value + : code + ) + }) + break + + case 'list': + data.push({ + 'type': 'list', + 'items': ( + tag.childNodes + .map(c => clean(c.textContent)) + .filter(e => e.length !== 0) + ) + }) + break + + case 'table': + const table: TableElement = { + type: 'table', + items: [] + } + for (const row of tag.childNodes) { + const items = new Array() + if (row.rawTagName !== 'row') continue + for (const column of row.childNodes) { + if (column.rawTagName !== 'column') continue + items.push(clean(column.textContent)) + } + table.items.push(items) + } + data.push(table) + break + + case 'image': + case 'banner': + case 'beanie': + data.push({ + 'type': tagName, + 'resource': element.getAttribute('resource')!, + 'caption': element.getAttribute('caption')!, + 'alt': element.getAttribute('alt')!, + }) + break + + case 'video': + data.push({ + 'type': tagName, + 'resource': element.getAttribute('resource')!, + 'caption': element.getAttribute('caption')!, + 'alt': element.getAttribute('alt')!, + 'cover': element.getAttribute('cover')!, + }) + break + + case 'audio': + data.push({ + 'type': tagName, + 'resource': element.getAttribute('resource')!, + 'name': element.getAttribute('name')!, + }) + break + + case '': break + default: + throw `Unsupported Tag Name '${tagName}'` + } + } + return [ + root.rawAttributes, + data + ] +} \ No newline at end of file diff --git a/suzzy-v0-standalone/source/functions/processFile.ts b/suzzy-v0-standalone/source/functions/processFile.ts new file mode 100644 index 0000000..c741b18 --- /dev/null +++ b/suzzy-v0-standalone/source/functions/processFile.ts @@ -0,0 +1,145 @@ +import { CONTENT_TYPE_MAP, PROCESSOR_ASSETS, PROCESSOR_DIRECT, PROCESSOR_INDEX, PROCESSOR_PREFIX, PROCESSOR_PRESETS, PRODUCTION } from '../constants' +import { existsSync, readFileSync } from 'fs' +import { extname, join } from 'path' +import { createHash } from 'crypto' +import cleanCss from 'clean-css' +import * as js from 'uglify-js' +import * as ts from 'typescript' +import sharp from 'sharp' +import Log from './logger' +import { byteSize } from './pugHelpers' + +const PROCCESED = new Map() + +// Background processes a file making it available at the returned path +export function processFile(uri: string, dry = false) { + return new Promise(async (ok, cancel) => { + if (!uri.startsWith(PROCESSOR_PREFIX)) { + cancel(`URI '${uri}' must begin with ${PROCESSOR_PREFIX}`) + return + } + if (dry) { + ok(`/asset-processor/${uri.slice(PROCESSOR_PREFIX.length)}`) + return + } + if (PRODUCTION && PROCCESED.has(uri)) { + return ok(PROCCESED.get(uri)) + } + + // Read Contents From Disk + const t = Date.now() + const [pathname, search] = uri.slice(PROCESSOR_PREFIX.length).split('?', 2) + const params = new URLSearchParams(search) + const path = join(process.cwd(), 'assets', pathname) + if (!existsSync(path)) { + cancel(`Path '${path}' does not exist`) + return + } + const input = readFileSync(path) + const extension = extname(pathname).slice(1) + + function complete(ext: string, output: string | Buffer) { + const outHash = createHash('md5').update(output).digest('hex').slice(0, 8) + const uriPath = + (params.get('direct') ? PROCESSOR_DIRECT : '') + + `/assets/${pathname.slice(0, pathname.length - extension.length)}${ext}` + const uriFull = `${uriPath}?v=${outHash}` + const index = PROCESSOR_ASSETS.push({ + 'type': CONTENT_TYPE_MAP[ext] || 'application/octet-stream', + 'size': Buffer.byteLength(output), + 'file': output, + }) - 1 + PROCESSOR_INDEX[uriPath] = index + PROCESSOR_INDEX[uriFull] = index + PROCCESED.set(uri, uriFull) + Log('DEBUG', 'processFile', `'${uriFull}' (${byteSize(output.length)}) (${Date.now() - t}ms)`) + ok(uriFull) + } + switch (extension) { + + // Resize Image + case 'webp': + case 'jpeg': + case 'jpg': + case 'png': + case 'gif': { + const preset = PROCESSOR_PRESETS[params.get('preset') || ''] + const animated = params.get('animated') !== null + complete( + preset ? preset.format : extension, + preset ? await sharp(input, { animated }) + .resize(preset.width, preset.height, preset.resize) + .toFormat(preset.format, { 'quality': preset.quality }) + .toBuffer() + : input + ) + break + } + + // Minify Script + case 'js': { + const minified = js.minify(input.toString(), { mangle: true }) + if (minified.error) { + cancel(`JavaScript Minify Error: ${minified.error}`) + return + } + complete('js', minified.code) + break + } + + // Transpile and Minify Script + case 'ts': { + const script = ts.transpile(input.toString(), { + 'target': ts.ScriptTarget.ES2022, + 'module': ts.ModuleKind.CommonJS, + 'lib': ['DOM', 'ES2022'], + 'allowSyntheticDefaultImports': true, + 'skipLibCheck': true, + 'strict': true, + }) + const minified = js.minify(script, { mangle: true }) + if (minified.error) { + cancel(`JavaScript Minify Error: ${minified.error}`) + return + } + complete('js', minified.code) + break + } + + // Minify Styles + case 'css': { + const css = input.toString() + const search = /('file:\/\/[^']+')/g + const promises = new Array>() + + // Search for URIs inside CSS File + css.replaceAll(search, uri => { + promises.push(processFile(uri.slice(1, -1), !PRODUCTION)) + return uri + }) + Promise.all(promises).then(results => { + // Replace all URIs and Minify + let index = 0 + const minifed = new cleanCss().minify( + css.replaceAll(search, () => results[index++]) + ) + if (minifed.errors.length) { + cancel(`Minify CSS Error: ${minifed.errors.join(', ')}`) + return + } + complete('css', minifed.styles) + + }).catch(e => { + cancel(`CSS Processing Errors: ${e}`) + }) + break + } + + // Unsupported, Skip Processing! + default: + complete(extension, input) + break + } + }) +} + diff --git a/suzzy-v0-standalone/source/functions/processView.ts b/suzzy-v0-standalone/source/functions/processView.ts new file mode 100644 index 0000000..8072c3a --- /dev/null +++ b/suzzy-v0-standalone/source/functions/processView.ts @@ -0,0 +1,60 @@ +import { byteSize, calcReadTime, formatDate, formatHref } from "./pugHelpers" +import { PROCESSOR_PREFIX, PRODUCTION } from "../constants" +import { processFile } from "./processFile" +import parse from "node-html-parser" +import pug from 'pug' + +const replaceAttr = new Array('src', 'href', 'content', 'poster') +const replaceQuery = new Array( + 'img[src]', // Image Elements + 'source[src]', // Video Sources + 'audio[src]', // Audio Sources + 'script[src]', // Script Elements + 'link[href]', // CSS Links + 'meta[content]', // Meta OG Tags + 'a[href]', // Download Anchors + 'video[poster]', // Video Posters +).join(',') + +// Render a View and Process all Files +export default async function processView(path: string, locals: any): Promise { + const document = parse( + pug.renderFile(path, Object.assign(locals, { + 'pretty': !PRODUCTION, + 'formatBytes': byteSize, + 'formatDate': formatDate, + 'formatString': formatHref, + 'calcReadTime': calcReadTime, + })), + { comment: true } + ) + + // Process Inline Styles + for await (const elem of document.querySelectorAll('*[style]')) { + let css = elem.getAttribute('style') + if (!css) continue + const search = /('file:\/\/[^']+')/g + const promises = new Array>() + css.replaceAll(search, uri => { + promises.push(processFile(uri.slice(1, -1), !PRODUCTION)) + return uri + }) + await Promise.all(promises).then(results => { + let index = 0 + css = css!.replaceAll(search, () => results[index++]) + }) + elem.setAttribute('style', css) + } + + // Process Element Sources + for (const elem of document.querySelectorAll(replaceQuery)) { + const attrKey = replaceAttr.find(e => elem.getAttribute(e)) + if (!attrKey) continue + const attrVal = elem.getAttribute(attrKey) + if (attrVal && attrVal.startsWith(PROCESSOR_PREFIX)) { + elem.setAttribute(attrKey, await processFile(attrVal, !PRODUCTION)) + } + } + + return document.toString() +} \ No newline at end of file diff --git a/suzzy-v0-standalone/source/functions/pugHelpers.ts b/suzzy-v0-standalone/source/functions/pugHelpers.ts new file mode 100644 index 0000000..68516da --- /dev/null +++ b/suzzy-v0-standalone/source/functions/pugHelpers.ts @@ -0,0 +1,66 @@ +import { Element } from "../types/articles" + +// Format ISO Timestamp into Generic Date (ex: Jun 26th 2003) +export function formatDate(isoTimestamp: string): string { + const t = new Date(isoTimestamp) + const m = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dece'][t.getMonth()] + const d = t.getDate() + const y = t.getFullYear() + return `${m} ${d}${d === 1 ? 'st' : d === 2 ? 'nd' : d === 3 ? 'rd' : 'th'} ${y}` +} + +// Format String for Usage in HREF Tags +export function formatHref(text: string): string { + return text + .trim() + .toLowerCase() + .replace(/[^A-Za-z0-9 ]/g, '') + .replace(/\s+/g, '-') +} + +// Determine byte size by length +export function byteSize(length: number) { + if (length > 1e+9) return `${(length / 1e+9).toFixed(2)}gb` + if (length > 1e+6) return `${(length / 1e+6).toFixed(2)}mb` + if (length > 1024) return `${(length / 1024).toFixed(2)}kb` + return `${length}b` +} + +// Estimate Required Time to Read this Article +export function calcReadTime(articleElements: Array): number { + return Math.ceil( + articleElements + .map((e): number => { + const AVERAGE_READING_SPEED = 200 // wpm + switch (e.type) { + + // Calculate seconds based on average reading speed divided by amount of characters + case 'list': return e.items.map(e => e.length).reduce((a, b) => a + b, 0) / AVERAGE_READING_SPEED + case 'text': return e.items.map(e => e.content.split(' ').length).reduce((a, b) => a + b, 0) / AVERAGE_READING_SPEED + case 'code': return e.content.split(' ').length / AVERAGE_READING_SPEED + + case 'subheader': + case 'header': + case 'quote': + return e.value.split(' ').length / AVERAGE_READING_SPEED + case 'table': + return e.items + .map(r => r.map(c => c.split(' ').length).reduce((a, b) => a + b, 0)) + .reduce((a, b) => a + b, 0) + / AVERAGE_READING_SPEED + + // Static increases for each image element + case 'image': + case 'banner': + case 'beanie': + return 0.25 // 15 seconds + + // Static increases for each video element + case 'video': + case 'audio': + return 0.50 // 30 seconds + } + }) + .reduce((a, b) => a + b, 0) + ) +} \ No newline at end of file diff --git a/suzzy-v0-standalone/source/functions/serveFunctions.ts b/suzzy-v0-standalone/source/functions/serveFunctions.ts new file mode 100644 index 0000000..5c6a053 --- /dev/null +++ b/suzzy-v0-standalone/source/functions/serveFunctions.ts @@ -0,0 +1,51 @@ +import { Request, RequestHandler, Response } from 'express' +import { PRODUCTION } from '../constants' +import { createHash } from 'crypto' +import processView from './processView' +import Log from './logger' + +// Renders a View from Disk and Serves it with Caching Enabled +export async function Static(path: string): Promise { + if (PRODUCTION) { + Log('INFO', 'static', `Pre-Render: '${path}'`) + const content = await processView(path, {}) + const length = Buffer.byteLength(content) + const hash = createHash('md5').update(content).digest('hex') + + return function (req: Request, res: Response) { + if (req.headers['if-none-match'] === hash) { + res.status(304).end() + return + } + res.setHeader('Content-Type', 'text/html') + res.setHeader('Content-Length', length) + res.setHeader('ETag', hash) + res.end(content) + } + } else { + return async function (req: Request, res: Response) { + Log('INFO', 'static', `Render: '${path}'`) + processView(path, {}) + .then(html => res.send(html)) + .catch(err => res + .status(500) + .end('Caught Rendering Error! Check Console for more details.\n' + String(err)) + ) + } + } + +} + +// Redirect to Another Location +export function Redirect(location: string) { + return function (req: Request, res: Response) { + res.redirect(location) + } +} + +// Return a Response Code with an Empty Body +export function Empty(status: number) { + return function (req: Request, res: Response) { + res.status(status).end() + } +} \ No newline at end of file diff --git a/suzzy-v0-standalone/source/service.ts b/suzzy-v0-standalone/source/service.ts new file mode 100644 index 0000000..1deb1d7 --- /dev/null +++ b/suzzy-v0-standalone/source/service.ts @@ -0,0 +1,103 @@ +import { PROCESSOR_ASSETS, PROCESSOR_INDEX, PROCESSOR_PREFIX, PRODUCTION, WEB_ADDR, WEB_PORT } from './constants' +import { Empty, Redirect, Static } from './functions/serveFunctions' +import { processFile } from './functions/processFile' +import { Articles } from './class/articles' +import Log from './functions/logger' +import express from 'express' + +// todo: implement gzip compression + +(async () => { + const blog = new Articles('blog', '/blog') + const library = new Articles('library', '/library') + const app = express() + app.disable('x-powered-by') + app.disable('etag') + + // Dynamic Routes + app.get(['/blog', '/blog/:cat', '/blog/:cat/:opt'], blog.serveContent) + app.get(['/library/:cat', '/library/:cat/:opt'], library.serveContent) + app.get('/library/snippet', library.serveSnippet) + app.get('/library*', Redirect('/library/home/welcome')) + + // Static Routes + app.get('/', await Static('views/public/index.pug')) + app.get('/projects', await Static('views/public/projects.pug')) + app.get('/login', await Static('views/public/login.pug')) + app.get('/signup', await Static('views/public/signup.pug')) + app.get('/password-reset', await Static('views/public/password-reset.pug')) + app.get('/password-update', await Static('views/public/password-update.pug')) + app.get('/verify-email', await Static('views/public/verify-email.pug')) + app.get('/verify-login', await Static('views/public/verify-login.pug')) + app.get('/challenge-picross', await Static('views/public/challenge-picross.pug')) + + // app.get('/profile', await Static('views/profile/index.pug')) + // app.get('/profile/devices', await Static('views/profile/devices.pug')) + // app.get('/profile/security', await Static('views/profile/security.pug')) + // app.get('/profile/applications', await Static('views/profile/applications.pug')) + // app.get('/profile/connections', await Static('views/profile/connections.pug')) + // app.get('/profile*', Redirect('/profile')) + app.get('/assets*', async (req, res) => { + const index = PROCESSOR_INDEX[req.originalUrl] + if (index === undefined) { + res.status(404).end() + return + } + const asset = PROCESSOR_ASSETS[index] + res.setHeader('Cache-Control', 'public, max-age=31536000, immutable') + res.setHeader('Content-Type', asset.type) + res.setHeader('Content-Length', asset.size) + res.end(asset.file) + }) + + if (!PRODUCTION) { + app.get('/asset-processor*', async (req, res) => { + processFile('file://' + req.originalUrl.slice('/asset-processor/'.length)) + .then(uri => { + const asset = PROCESSOR_ASSETS[PROCESSOR_INDEX[uri]] + res.setHeader('Cache-Control', 'public, max-age=31536000, immutable') + res.setHeader('Content-Type', asset.type) + res.setHeader('Content-Length', asset.size) + res.end(asset.file) + }) + .catch(e => { + res.status(500) + res.end(`Caught Processor Error!\n${String(e)}`) + }) + }) + } + + app.get('*', await Static('views/404.pug')) + app.use(Empty(404)) + + app.listen(WEB_PORT, WEB_ADDR, () => { + Log('INFO', 'http', `Server Listening @ ${WEB_ADDR}:${WEB_PORT}`) + }) + + // Array of items not referenced but required! + const INCLUDES = new Array( + // scripts/shared-utils.ts + 'file://images/default-icon.png', + 'file://images/default-user0.png', + 'file://images/default-user1.png', + 'file://images/default-user2.png', + 'file://images/default-user3.png', + 'file://images/default-user4.png', + 'file://images/default-user5.png', + + // scripts/shared-utils.ts + 'file://icons/badge-crown.svg', + 'file://icons/badge-star.svg', + 'file://icons/badge-picross.svg', + + // scripts/template-article.ts + 'file://icons/icon-play.svg', + 'file://icons/icon-pause.svg', + + // todo: Email Directory + ) + for (const uri of INCLUDES) { + processFile(uri) + } + +})() diff --git a/suzzy-v0-standalone/source/types/articles.d.ts b/suzzy-v0-standalone/source/types/articles.d.ts new file mode 100644 index 0000000..e1a9375 --- /dev/null +++ b/suzzy-v0-standalone/source/types/articles.d.ts @@ -0,0 +1,147 @@ +type FilePath = string; + +export interface Locals { + relevantArticle?: ManifestArticle // Current Article (if any) + relevantArticles: ManifestArticle[] // List of all articles filtered by current category + relevantCategory?: ManifestEntry // Relevant Articles + articles: ManifestArticle[] // List of all articles in manifest + authors: ManifestEntry[] // List of all authors in manifest + categories: ManifestEntry[] // List of all categories in manifest + pageIndex: number // Current Index given by user + pageOffset: number // Current Offset for relevantArticles + pageTotal: number // Total amount of relevant pages + pageCurrent: number // Current Page Number +} + +export interface Manifest { + noBrowser?: boolean; + itemsPerPage?: number; + categories: Array + authors: Array + articles: Array +} + +export interface ManifestEntry { + id: string; + name: string; + icon: FilePath; +} + +export interface ManifestArticle { + info: { + id: string; + created: string; + categoryId: string; + authorId: string; + title: string; + snippet: string; + banner: FilePath; + noindex: string; + } + path: string; + category: ManifestEntry; + author: ManifestEntry; + elements: Array; +} + +export type Element = + HeaderElement | + SubheaderElement | + QuoteElement | + ImageElement | + TextElement | + BannerElement | + BeanieElement | + VideoElement | + AudioElement | + ListElement | + TableElement | + CodeElement + +// Header/Chapter Element +export interface HeaderElement { + type: 'header'; + value: string; +} + +// Subchapter Element +export interface SubheaderElement { + type: 'subheader'; + value: string; +} + + +// Quote Text Element +export interface QuoteElement { + type: 'quote'; + value: string; +} + +// Text Element +export interface TextElement { + type: 'text'; + items: { + tag: string; + content: string; + target?: string; + href?: string; + }[] +} + +// Bordered Image Element +export interface ImageElement { + type: 'image'; + resource: string; + caption: string; + alt: string; +} + +// Borderless Image Element +export interface BannerElement { + type: 'banner'; + resource: string; + caption: string; + alt: string; +} + +// Borderless Image Element with Download on Click +export interface BeanieElement { + type: 'beanie'; + resource: string; + caption: string; // Unused + alt: string; +} + +// Video Element +export interface VideoElement { + type: 'video'; + resource: string; + caption: string; + alt: string; + cover: string; +} + +// Audio Player +export interface AudioElement { + type: 'audio'; + resource: string; + name: string; +} + +// List Element +export interface ListElement { + type: 'list'; + items: string[]; +} + +// Table Element +export interface TableElement { + type: 'table'; + items: string[][]; +} + +// Code Element +export interface CodeElement { + type: 'code'; + content: string; +} \ No newline at end of file diff --git a/suzzy-v0-standalone/tsconfig.json b/suzzy-v0-standalone/tsconfig.json new file mode 100644 index 0000000..6cba43a --- /dev/null +++ b/suzzy-v0-standalone/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "outDir": "dist", + "strict": true, + "target": "ES2022", + "module": "commonjs", + "esModuleInterop": true, + "skipLibCheck": true, + "noImplicitOverride": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true + }, + "exclude": [ + "assets/**" + ] +} \ No newline at end of file diff --git a/suzzy-v0-standalone/views/404.pug b/suzzy-v0-standalone/views/404.pug new file mode 100644 index 0000000..9ebc6ce --- /dev/null +++ b/suzzy-v0-standalone/views/404.pug @@ -0,0 +1,16 @@ +extends _TemplateSpecial +block pageHeader + meta(name='robots' content='noindex') + title suzzy - Page Not Found (404) +block pageContent + //- User Message + img(class='special-banner' src='file:///images/public-nopage.png') + p(class='text-header') Page Not Found + p(class='text-tip') Should've made a left instead of a right... + + //- User Actions + div(class='divider-line') + div(class='special-alongside') + a(class='default' href='/') Homepage + a(class='default' href='javascript:window.history.back()') Go Back + \ No newline at end of file diff --git a/suzzy-v0-standalone/views/_TemplateArticle.pug b/suzzy-v0-standalone/views/_TemplateArticle.pug new file mode 100644 index 0000000..d9d318f --- /dev/null +++ b/suzzy-v0-standalone/views/_TemplateArticle.pug @@ -0,0 +1,79 @@ +each e in relevantArticle.elements + case e.type + when 'text' + p + each t in e.items + case t.tag + when '' + | #{t.content} + when 'h' + | #[span(class='text-hint') #{t.content}] + when 'c' + | #[span(class='text-code') #{t.content}] + when 'a' + | #[a(class='link-highlight' target=t.target href=t.href) #{t.content}] + when 's' + | #[s #{t.content}] + when 'b' + | #[b #{t.content}] + when 'i' + | #[i #{t.content}] + when 'header' + p(class='element-header' id=formatString(e.value)) #{e.value} + when 'subheader' + p(class='element-subheader' id=formatString(e.value)) #{e.value} + when 'quote' + p(class='text-quote') > #{e.value} + when 'code' + div(class='element-code') + button(class='default') Copy + pre !{e.content} + when 'image' + div(class='element-gallery') + div(class='gallery-item') + img(class='special-dropshadow' loading='lazy' src=e.resource alt=e.alt) + p #{e.caption} + when 'video' + div(class='element-gallery') + div(class='gallery-item') + video(class='special-dropshadow' controls preload='none' poster=e.cover) + source(src=e.resource) + p #{e.caption} + when 'audio' + div(class='element-audio special-shadow') + audio(preload='auto') + //- Audio Metadata + div(class='audio-container') + p #{e.name} + a(class='audio-download' title='Download Track' download=e.resource href=e.resource) + img(src='file://icons/icon-download.svg' alt='Icon for Download') + //- Audio Controls + div(class='audio-container') + button(class='audio-button' title='Play/Pause Audio') + img(src='file://icons/icon-play.svg' alt='Icon for Play/Pause') + input(class='audio-slider' type='range' value='0' disabled) + p(class='audio-time') -:--/-:-- + when 'table' + table(class='element-table special-shadow') + - var firstRow = e.items.at(0) + - var otherRow = e.items.slice(1) + thead + tr + each column in firstRow + th #{column} + tbody + each row in otherRow + tr + each column in row + td #{column} + when 'list' + ul(class='element-list') + each listItem in e.items + li • #{listItem} + when 'banner' + img(class='element-banner' src=e.resource alt=e.alt) + when 'beanie' + a(href=e.resource download=(e.resource.split('/').at(-1).split('?').at(0))) + img(class='element-beanie' src=e.resource alt=e.alt) + default + - throw "Unknown Type: " + e.type \ No newline at end of file diff --git a/suzzy-v0-standalone/views/_TemplateProfile.pug b/suzzy-v0-standalone/views/_TemplateProfile.pug new file mode 100644 index 0000000..cb578f7 --- /dev/null +++ b/suzzy-v0-standalone/views/_TemplateProfile.pug @@ -0,0 +1,67 @@ +doctype html +html(lang='en') + head + meta(name='viewport' content='width=device-width, initial-scale=1') + link(rel='icon' type='image/png' href='file://images/favicon.png') + link(rel='stylesheet' href='file://fonts/font-poppins.css') + link(rel='stylesheet' href='file://styles/template-global.css') + link(rel='stylesheet' href='file://styles/template-modals.css') + link(rel='stylesheet' href='file://styles/template-profile.css') + meta(property='theme-color' content='#A1A8FF') + block pageHeader + body + noscript + style. + noscript div { + position: absolute; + width: 100%; + height: 100%; + display: grid; + justify-items: center; + align-content: center; + text-align: center; + } + noscript p { + padding: 24px; + box-sizing: border-box; + } + div.wrapper-layout { + display: none; + } + div + p This webpage requires JavaScript to be enabled. + a(href='/') Go to Homepage + + div(class='wrapper-layout') + div(class='wrapper-navigation') + - var PAGE = filename.split('\\').at(-1).split('.').at(0) + a(href='/') + img(class='navigation-logo' src='file://images/public-logo.png' alt='Logo for suzzy games') + a(active=(PAGE === 'index') class='navigation-link' href='/profile') + img(src='file://icons/icon-user.svg' alt='Icon for Profile') + span Profile + a(active=(PAGE === 'security') class='navigation-link' href='/profile/security') + img(src='file://icons/icon-lock.svg' alt='Icon for Security') + span Security + a(active=(PAGE === 'devices') class='navigation-link' href='/profile/devices') + img(src='file://icons/icon-mobile.svg' alt='Icon for Devices') + span Devices + a(active=(PAGE === 'connections') class='navigation-link' href='/profile/connections') + img(src='file://icons/icon-plug.svg' alt='Icon for Connections') + span Connections + a(active=(PAGE === 'applications') class='navigation-link' href='/profile/applications') + img(src='file://icons/icon-app.svg' alt='Icon for Applications') + span Applications + + div(class='divider-line') + a(class='navigation-link' href='/profile/redeem') + img(src='file://icons/icon-star.svg' alt='Icon for Redeem Code') + span Redeem Code + a(class='navigation-link' href='/') + img(src='file://icons/icon-exit.svg' alt='Icon for Sign Out') + span Sign Out + + div(class='wrapper-content') + block pageContent + + script(src='file://scripts/shared-utils.ts') \ No newline at end of file diff --git a/suzzy-v0-standalone/views/_TemplateProfilePage.pug b/suzzy-v0-standalone/views/_TemplateProfilePage.pug new file mode 100644 index 0000000..a5885b9 --- /dev/null +++ b/suzzy-v0-standalone/views/_TemplateProfilePage.pug @@ -0,0 +1,15 @@ +//- Collection Alerts and Errors +div(class='collection-alert' id='alert-loading') + img(src='file://icons/diagram-loading.svg' alt='Icon for Loading') + span Loading #{loading_message ||'Content'} +div(class='collection-alert' id='alert-error' hidden) + img(src='file://icons/icon-cross.svg' alt='Icon for Cross') + span An Error Has Occurred + +if empty_message + div(class='collection-alert' id='alert-empty' hidden) + img(src=empty_source alt=`Icon for ${empty_alt}`) + span #{empty_message} + +//- Collection Items +div(class='wrapper-collection') \ No newline at end of file diff --git a/suzzy-v0-standalone/views/_TemplatePublic.pug b/suzzy-v0-standalone/views/_TemplatePublic.pug new file mode 100644 index 0000000..af84180 --- /dev/null +++ b/suzzy-v0-standalone/views/_TemplatePublic.pug @@ -0,0 +1,66 @@ +doctype html +html(lang='en') + head + meta(charset='UTF-8') + meta(name='viewport' content='width=device-width, initial-scale=1.0') + link(rel='icon' type='image/png' href='file://images/favicon.png') + link(rel='stylesheet' href='file://fonts/font-poppins.css') + link(rel='stylesheet' href='file://styles/template-global.css') + link(rel='stylesheet' href='file://styles/template-public.css') + link(rel='stylesheet' href='file://styles/template-modals.css') + meta(property='theme-color' content='#A1A8FF') + block pageHeader + body + //- Navigation Bar + div(class='wrapper-navigation') + div(class='container-navigation') + //- Left-Hand: Site Navigation + div(class='navigation-side' id='navigation-left') + a(href="/" style="display:grid;" tabindex="0") + img( + class='navigation-logo' + src='file://images/public-logo.png' + alt='Logo for suzzy games' + ) + p(class='text-muted') • + a(class='text-link' href='/blog') Blog + a(class='text-link' href='/library') Library + a(class='text-link' href='/projects') Projects + //- Right-Hand: User & Site Status + div(class='navigation-side' id='navigation-right') + a(class='special-shadow' id='navigation-status' href='https://status.suzzygames.com/' title='Server Status') + div(id='status-dot' class='status-dot special-pulse') + p(id='status-text' class='text-tip') Loading + a(id='navigation-profile' class='special-shadow' title='Manage Account') + + //- Content Dividers + //- div.divider-navigation: Spacer for Navigation Bar on Desktop + //- div.divider-mobile: Line Divider only present on Mobile + div(class='divider-navigation') + div(class='divider-line divider-mobile') + + //- Escape Content Wrapper for elements that required the full width of the page + block pageEscaped + + //- Wrapper for Page Content + div(class='wrapper-content') + block pageContent + + //- Wrapper for Page Footer + div(class='wrapper-footer') + div(class='container-footer') + div(class='footer-links') + a(class='footer-link' target='_blank' href='https://twitter.com/bakonpancakz' title='Twitter Account for bakonpancakz') + img(src='file://images/brands/logo-x.png' alt='Logo for X') + a(class='footer-link' target='_blank' href='https://discord.gg/invite/bvTCZavXUU' title='Discord Server for suzzy games') + img(src='file://images/brands/logo-discord.png' alt='Logo for Discord') + a(class='footer-link' target='_blank' href='https://www.roblox.com/groups/10781692' title='ROBLOX Group for suzzy games') + img(src='file://images/brands/logo-roblox.png' alt='Logo for Roblox') + p(class='text-tip') + | © 2021-2024 suzzy games. All Rights Reserved. + a(class='text-tip text-muted' href='/challenge-picross' style='text-decoration: none;') + | Developed with ♡ by bakonpancakz + + //- Page Scripts + script(src='file://scripts/shared-utils.ts') + script(src='file://scripts/shared-public.ts') \ No newline at end of file diff --git a/suzzy-v0-standalone/views/_TemplateSpecial.pug b/suzzy-v0-standalone/views/_TemplateSpecial.pug new file mode 100644 index 0000000..aa22b82 --- /dev/null +++ b/suzzy-v0-standalone/views/_TemplateSpecial.pug @@ -0,0 +1,17 @@ +doctype html +html(lang='en') + head + meta(name='viewport' content='width=device-width, initial-scale=1') + link(rel='icon' type='image/png' href='file://images/favicon.png') + link(rel='stylesheet' href='file://fonts/font-poppins.css') + link(rel='stylesheet' href='file://styles/template-global.css') + link(rel='stylesheet' href='file://styles/template-modals.css') + link(rel='stylesheet' href='file://styles/template-special.css') + meta(property='theme-color' content='#A1A8FF') + block pageHeader + body + div(class='wrapper-special') + div(class='container-special') + block pageContent + script(src='file://scripts/shared-utils.ts') + block pageEscaped \ No newline at end of file diff --git a/suzzy-v0-standalone/views/blog/articles/04.17.24.Hello World.xml b/suzzy-v0-standalone/views/blog/articles/04.17.24.Hello World.xml new file mode 100644 index 0000000..9467826 --- /dev/null +++ b/suzzy-v0-standalone/views/blog/articles/04.17.24.Hello World.xml @@ -0,0 +1,182 @@ + \ No newline at end of file diff --git a/suzzy-v0-standalone/views/blog/articles/05.03.24.Repo of Knowledge.xml b/suzzy-v0-standalone/views/blog/articles/05.03.24.Repo of Knowledge.xml new file mode 100644 index 0000000..2555059 --- /dev/null +++ b/suzzy-v0-standalone/views/blog/articles/05.03.24.Repo of Knowledge.xml @@ -0,0 +1,103 @@ +
+ +
A Fresh Coat of Paint
+ + Welcome to the new and improved suzzy games website! + + + Absolutely everything on this site has been either upgraded or replaced + entirely. The blog system went from janky to proper, pages are now full + of content and trivia instead of being half-baked, and a new library was + built. More on this later! + I dedicated an entire weekend to turn this site from potato to french + fry. I absolutely love how it looks now and I hope you do too. + + Sometimes I open up the website just to stare at it @_@ + A 4 by 4 grid showing the old site on top and the new site design on the bottom + + +
Introducing oAuth2
+ + Are you a developer? Then this chapter is for you! Applications + and oAuth2 are now available for everybody! This allows third-party + developers to safely access your profile and the game data that + you decide to share with them. + + + New features and subsequent documentation on those features will become + available for developers in the library. + Which brings us to the next topic... + + An example oAuth2 prompt asking the user to allow access to certain account permissions + + +
Building a Library
+ + The library is a new section of the suzzy website! + It contains legal stuff, branding guidelines, and most importantly + documentation for developers. If you're really bored check out the + branding article! + It contains little bits of trivia on all past suzzy projects. + + A preview for the suzzy games library with the mascot sitting on a bean bag in their room reading a book + + +
Introducing BECCA2D
+
\ No newline at end of file diff --git a/suzzy-v0-standalone/views/blog/browser.pug b/suzzy-v0-standalone/views/blog/browser.pug new file mode 100644 index 0000000..add94fb --- /dev/null +++ b/suzzy-v0-standalone/views/blog/browser.pug @@ -0,0 +1,104 @@ +extends ../_TemplatePublic +block pageHeader + link(rel='stylesheet' href='file://styles/browser-blog.css') + meta(property='og:image' content='file://images/public-logo-og.png') + meta(property='og:title' content='suzzy games - blog') + meta(property='og:description' content='News and Updates straight from the source.') + meta(name='description' content='News and Updates straight from the source.') + title suzzy games - blog + style. + div.divider-mobile { + display: none !important; + } +block pageEscaped + div(class='widget-wrapper background-scroll') + + //- Widget: Latest Articles + div(class='widget') + p(class='text-header') > Latest < + div(class='widget-latest-articles') + - var recents = articles.slice(0, 4) + each article, index in recents + div(class='latest-item') + img(class='latest-banner special-dropshadow' alt=`Banner Image for Blog Post '${article.info.title}'` src=article.info.banner) + div(class='latest-content') + p(class='text-headline') #{article.info.title} + div(class='latest-info') + p(class='text-tip text-muted') #{article.author.name} + p(class='text-tip text-muted') • #{formatDate(article.info.created)} • + p(class='text-tip text-muted') #{article.category.name} + div(class='divider-header') + p(class='text-tip') #{article.info.snippet} + a(class='text-tip text-muted' href=`/blog/${article.category.id}/${article.info.id}`) + | Read More (#{calcReadTime(article.elements)} minutes) + + //- Widget: Latest Controls + div(class='widget widget-latest-controls') + div(class='latest-spacer') + div(class='latest-controls') + button(class='default' id='latest-last' aria-label='Last Blog Post') < + each _, index in recents + div(class='dot-circle' + (index === 0 ? ' dot-active' : '')) + button(class='default' id='latest-next' aria-label='Next Blog Post') > + script(src='file://scripts/browser-blog.ts') + +block pageContent + div(class='main-wrapper') + + //- Browser Filters + div(class='main-aside') + p(class='text-header') > Browser < + a(class=`filter-link ${!relevantCategory ? 'filter-active' : ''}` href='/blog') + img(class='filter-icon' alt='Icon for Anything' src='file://blog/category-any.png') + p(class='filter-name text-tip') Anything + each category in categories + a( + href=`/blog/${category.id}` + class=`filter-link ${relevantCategory && (relevantCategory.id === category.id ? 'filter-active' : '')}` + ) + img(class='filter-icon' alt=`Icon for Category ${category.name}` src=category.icon) + p(class='filter-name text-tip') #{category.name} + div(class='divider-dashed') + + //- Browser Articles + div(class='main-content' role='main') + if relevantArticles.length === 0 + //- No Articles Found + div(class='collection-empty') + img(aria-hidden='true' alt='Icon for No Posts' src='file://images/public-noposts.png') + p(class='text-muted') no blog posts found + else + //- Article Previews + each article, index in relevantArticles + a(class='collection-item' href=`/blog/${article.category.id}/${article.info.id}`) + img(class='item-banner' alt=`Banner Image for Blog Post ${article.info.title}` src=article.info.banner) + div(class='item-content') + p(class='text-header') #{article.info.title} + div(class='item-info') + img(class='filter-icon icon-small' alt=`Icon for Category ${article.category.name}` src=article.category.icon) + p(class='text-tip') #{article.category.name} + //- img(class='filter-icon icon-small' alt=`Avatar for Author ${article.author.name}` src=article.author.icon) + //- p(class='text-tip') #{article.author.name} + p(class='text-tip text-muted item-bull') • + p(class='text-tip') #{formatDate(article.info.created)} + p(class='text-tip text-muted item-bull') • + p(class='text-tip') #{calcReadTime(article.elements)} minute read + p(class='text-tip text-muted') #{article.info.snippet} + + //- Page Select + if relevantArticles.length !== 0 + div(class='main-wrapper') + div(class='main-aside') + div(class='main-content' role='main') + div(class='collection-pageselect') + - var isLastPage = (pageCurrent === 1) + - var noNextPage = (pageCurrent === pageTotal) + - var BASE_URL = '/blog' + (relevantCategory ? `/${relevantCategory.id}` : '') + - var hrefLastPage = BASE_URL + (pageCurrent === 2 ? '' : (isLastPage ? '' : `/${pageCurrent - 1}`)) + - var hrefNextPage = BASE_URL + (noNextPage ? '' : `/${pageCurrent + 1}` ) + + a(class='default text-tip' href=(hrefLastPage) disabled=(isLastPage)) < + each _, index in Array(pageTotal) + div(class=('dot-circle ' + (index === (pageCurrent-1) ? 'dot-active': '')).trim()) + a(class='default text-tip' href=(hrefNextPage) disabled=(noNextPage)) > + div(class='divider-spacer') \ No newline at end of file diff --git a/suzzy-v0-standalone/views/blog/manifest.json b/suzzy-v0-standalone/views/blog/manifest.json new file mode 100644 index 0000000..7ab7a66 --- /dev/null +++ b/suzzy-v0-standalone/views/blog/manifest.json @@ -0,0 +1,26 @@ +{ + "itemsPerPage": 8, + "authors": [ + { + "id": "bakonpancakz", + "name": "bakonpancakz", + "icon": "file://blog/author-bakonpancakz.png" + } + ], + "categories": [ + { + "id": "behind-the-scenes", + "name": "Behind The Scenes", + "icon": "file://blog/category-bts.png" + }, + { + "id": "project-monke", + "name": "Project Monke", + "icon": "file://blog/category-pm.png" + } + ], + "articles": [ + "04.17.24.Hello World.xml", + "05.03.24.Repo of Knowledge.xml" + ] +} \ No newline at end of file diff --git a/suzzy-v0-standalone/views/blog/post.pug b/suzzy-v0-standalone/views/blog/post.pug new file mode 100644 index 0000000..744cc89 --- /dev/null +++ b/suzzy-v0-standalone/views/blog/post.pug @@ -0,0 +1,55 @@ +extends ../_TemplatePublic +block pageHeader + - const article = relevantArticle + - const author = relevantArticle.author + - const category = relevantArticle.category + link(rel='stylesheet' href='file://styles/browser-blog.css') + link(rel='stylesheet' href='file://styles/template-article.css') + meta(property='og:title' content=article.info.title) + meta(property='og:description' content=`By ${author.name} on ${formatDate(article.info.created)}\n---\n${article.info.snippet}`) + meta(name='description' content=`By ${author.name} on ${formatDate(article.info.created)}\n---\n${article.info.snippet}`) + meta(property='og:image' content=`${article.info.banner}?direct=true`) + meta(property='twitter:card' content='summary_large_image') + title blog | #{article.info.title} (#{category.name}) + +block pageContent + div(class='main-wrapper') + + //- Layout: Sidebar + div(class='main-aside article-chapters') + p(class='text-headline') Chapters + each element in article.elements + if (element.type === 'header') + a(href=`#${formatString(element.value)}` class='text-tip') • #{element.value} + a(class='text-tip text-muted' href='#') ▲ Back to Top + + //- Mobile Users get a small triangle in the bottom right instead + a(class='default mobile-home' href='#home') ▲ + + //- Layout: Article + div(class='main-content' role='main' id='home') + //- Article Headeer + img( + class='article-banner special-dropshadow' + alt=`Banner for ${article.info.title}` + src=article.info.banner + ) + p(class='text-hero article-header') #{article.info.title} + div(class='article-header') + div(class='item-info') + img(class='filter-icon' alt=`Icon for Category ${category.name}` src=category.icon) + a(class='text-tip' href=`/blog/${category.id}`) #{category.name} + p(class='text-tip') + | #{formatDate(article.info.created)} + p(class='text-tip') + | #{calcReadTime(article.elements)} minute read + div(class='item-info') + img(class='filter-icon' alt=`Avatar for Author ${author.name}` src=author.icon) + p(class='text-tip') #{author.name} + div(class='divider-line') + + //- Article Content + include ../_TemplateArticle + + div(style='height: 64px;') + script(src='file://scripts/shared-article.ts') \ No newline at end of file diff --git a/suzzy-v0-standalone/views/library/articles/api/Applications.xml b/suzzy-v0-standalone/views/library/articles/api/Applications.xml new file mode 100644 index 0000000..23638a2 --- /dev/null +++ b/suzzy-v0-standalone/views/library/articles/api/Applications.xml @@ -0,0 +1,57 @@ +
+
Introduction
+ + Applications allow you to integrate the suzzy API into your projects. + You can create and manage them while in the Applications + section of your profile. + + + Application Object + + + Field + Type + Description + + + id + integer + Unique Application ID + + + created + Date + ISO Timestamp of when application was created + + + flags + integer + the application's public flags + + + name + string + the application's name + + + description + string? + the application's description + + + icon + string? + the application's image hash + +
+ +
\ No newline at end of file diff --git a/suzzy-v0-standalone/views/library/articles/api/Changelog.xml b/suzzy-v0-standalone/views/library/articles/api/Changelog.xml new file mode 100644 index 0000000..9439f36 --- /dev/null +++ b/suzzy-v0-standalone/views/library/articles/api/Changelog.xml @@ -0,0 +1,23 @@ +
+
Notices
+ Important and Breaking changes will be listed below: + + Nothing to report! + +
History
+ Initial Release + Date: May 12th 2024 + + Applications are now generally available + suzzy Library now available + +
\ No newline at end of file diff --git a/suzzy-v0-standalone/views/library/articles/api/OAuth2.xml b/suzzy-v0-standalone/views/library/articles/api/OAuth2.xml new file mode 100644 index 0000000..20e28ba --- /dev/null +++ b/suzzy-v0-standalone/views/library/articles/api/OAuth2.xml @@ -0,0 +1,155 @@ +
+
Resources
+ + suzzy games provides support for OAuth2 allowing applications to + use the API for authorization and retrieval of user data. + + + It is recommended you + Create an Application + and store the given + CLIENT_ID and + CLIENT_SECRET for later use. + + OAuth2 URLs + + + URL + Description + + + https://suzzygames.com/oauth2 + Base Authorization URL + + + https://apis.suzzygames.com/v1/oauth2/token + Token URL + + + https://apis.suzzygames.com/v1/oauth2/token/revoke + Token Revocation URL + +
+ These endpoints will only accept the content type "application/x-www-form-urlencoded" + OAuth2 Scopes + + A list of available scopes, some endpoints will return fields with a + null value or return an error if a required scope is unavailable. + + + + Name + Description + + + identify + Allows access to /users/@me endpoint + + + email + Allows /users/@me endpoint to return the user's email + + + connections + Allows access to /users/@me/applications endpoint + +
+ + +
Authorization Code Grant
+ + This is most commonly known as standard OAuth2, where the user + follows an Authorization Flow in return for an access code which the + application can exchange for the user's access token. + + + All endpoints expect HTTP Basic Auth using your applications + CLIENT_ID as the username and CLIENT_SECRET as the password + + + Example Authorization URL + +https://suzzygames.com/oauth2?response_type=code&redirect_uri=https%3A%2F%2Fexample.org&client_id=1273&redirect_uri=https%3A%2F%2Fexample.org&scope=identify%20email&state=1734831153572 + + - response_type must always be code, as it is currently the only supported type. + - redirect_uri is one of the Redirect URIs set in your application settings, url encoded. + - client_id is your Application ID. + - scope is a list of OAuth2 Scopes delimited by url encoded spaces (%20). + - state is a unique string that will be returned if set. + + Example Redirect URL + +https://example.org?code=9AED9A46641F3E3F489523C643C177BB.oboQpOkOSQJvdYYa4WuPq8J3TaYqNgylfq97sa99DRk&state=1734831153572 + + + The code can now be exchanged for a user's access token by + making a POST request to the Token URL + with the following parameters: + + - code - parameter taken from the query string + - grant_type - value must be authorization_code + - redirect_uri - the redirect_uri from the authorization url + In response you will be bestowed an Access Token Response + + Access Token Response + + + Name + Type + Description + + + access_token + string + Token to include in authorization header + + + token_type + string + String to prepend to token in authorization header + + + expires_in + number + Seconds until access token expires + + + refresh_token + string + Token to submit when making a refresh request + + + scope + string + The relevant OAuth2 Scopes for this token (delimited with a space) + +
+ + Refreshing Access Token + + Once an access token has expired you may refresh it by making a + POST request to the Token URL with + the following parameters: + + - grant_type - value must be refresh_token + - refresh_token - the user's refresh token + In response you will be bestowed a fresh Access Token Response + + Revoking Access Token + + You can revoke an access token by making a POST request to the + Token Revocation URL with the following + parameters: + + - token - the user's access token or refresh token + +
\ No newline at end of file diff --git a/suzzy-v0-standalone/views/library/articles/api/Reference.xml b/suzzy-v0-standalone/views/library/articles/api/Reference.xml new file mode 100644 index 0000000..1d05cff --- /dev/null +++ b/suzzy-v0-standalone/views/library/articles/api/Reference.xml @@ -0,0 +1,108 @@ +
+ +
Sending Requests
+ + All requests to the api should be made to the following URL: https://apis.suzzygames.com/v1/ + + + +
Ratelimiting
+ + Each response will include headers related to ratelimiting, follow them otherwise + the server will automatically reject your request and respond with a + 429 Too Many Requests error. + + + + Header + Description + + + RateLimit-Limit + Request Quota + + + RateLimit-Remaining + Requests Remaining + + + RateLimit-Reset + Float seconds until Quota Resets + +
+ + Sending too many bad requests too quickly will result in your IP Address + being blocked temporarily. + + + Frequent abusers will get the ban hammer! + + + +
Content URLs
+ + User Uploads (Avatars, Banners, etc.) are available at + https://content.suzzygames.com/ using the following paths: + + + + Name + Path + + + User Avatars + /avatars/{user.id}/{user.avatar}/{extension}/{size} + + + User Banners + /banners/{user.id}/{user.banner}/{extension}/{size} + +
+ Image Sizes: Large (192px), Medium (96px), Small (48px) + Image Extensions: webp, png, gif (if animated) + Animated images will have their hash begin with "a_" + + +
Default Assets
+ + Ocasionally users won't have uploaded an avatar, you can use the provided + default ones by calculating the index with index = {user.id} % 6 and using the follow URL: + + + https://suzzygames.com/assets/images/default-user{index}.png + + Signed out users use "default-anon.png" instead + + Default Colors + + Default colors for user profiles are available below: + + + + Name + Color (Hex) + + + Profile Banner + #1D1E2E + + + Profile Background + #333550 + + + Profile Border + #282A3F + +
+ +
\ No newline at end of file diff --git a/suzzy-v0-standalone/views/library/articles/api/Tutorials.xml b/suzzy-v0-standalone/views/library/articles/api/Tutorials.xml new file mode 100644 index 0000000..524b095 --- /dev/null +++ b/suzzy-v0-standalone/views/library/articles/api/Tutorials.xml @@ -0,0 +1,28 @@ +
+
Creating an Application
+ + You can create an application using the My Applications + page in the account management portal. Ensure your application has a + memorable name and that you keep its secret key secure and private. + + Click on the Create Application Button + You can then edit your Application by clicking on the Edit Button + +
\ No newline at end of file diff --git a/suzzy-v0-standalone/views/library/articles/api/Users.xml b/suzzy-v0-standalone/views/library/articles/api/Users.xml new file mode 100644 index 0000000..91a5c61 --- /dev/null +++ b/suzzy-v0-standalone/views/library/articles/api/Users.xml @@ -0,0 +1,212 @@ +
+ +
User Object
+ + Fields for a user object and the required oAuth2 Scope to view them. + Optional fields (Type ending with ?) or hidden fields (Missing oAuth2 Scope) + will have their value set as null instead. + + + + Field + Type + Description + Required oAuth2 Scope + + + id + integer + their unique account id + identify + + + created + timestamp + their account creation timestamp + identify + + + public_flags + integer + their public flags (badges) + identify + + + username + string + their username + identify + + + displayname + string + their displayname + identify + + + subtitle + string + their pronouns, status, etc. + identify + + + biography + string + their biography + identify + + + accent_background + integer + their profile background color + identify + + + accent_banner + integer + their profile banner color + identify + + + accent_border + integer + their profile border color + identify + + + avatar + string? + their avatar image hash + identify + + + banner + string? + their banner image hash + identify + + + verified + boolean + has email been verified? + identify + + + email + string? + their email address + email + +
+ Usernames will always be alphanumeric with underscores and between 3-32 characters long + Usernames may change without warning, use account IDs instead! + Displaynames and Subtitles are up to 32 characters long + Biographies are up to 320 chracters long + Colors are in Decimal Representation + + +
Public Flags
+ + Use a Bitwise AND operation in your language of choice to see if a flag is + set for a user. Flags are primarily used for badges which are special achievements + that were obtainable for a limited time or gifted to specific users. + + + + Value + Name + Description + Obtainable? + + + 1 (1 << 0) + BADGE_CROWN + Reserved for bakonpancakz + Never + + + 2 (1 << 1) + BADGE_VIP + Reserved for important users + No + + + 4 (1 << 2) + BADGE_PICROSS + Awarded by completing Challenge #1 + Yes + + + 8 (1 << 3) + PUBLIC_UNAVAILABLE + Account is Unavailable + TOS Violation + +
+ + Unavailable accounts, may be banned or deleted. + The reasoning will be sent to the account owners email. + + + +
Get Current User
+ GET /users/@me + + Returns the current user if you have the identify scope, also + returns email if you also have the email scope. + + + +
Get Connections for Current User
+ GET /users/@me/connections + + Requires the connections OAuth2 Scope. + Returns an array of Connection Objects the user is actively connected to. + + Connection Object + + + Field + Type + Description + + + id + integer + Unique Connection ID + + + created + Date + ISO Timestamp of when connection was created + + + scopes + integer + Associated OAuth2 scopes with connection + + + application + Application Object + An Application Object + +
+ Quick link to Application Object. + + +
Get User by Username
+ GET /users/<user.username> + + Returns a user object for a given username, + doesn't require any authorization. + +
\ No newline at end of file diff --git a/suzzy-v0-standalone/views/library/articles/home/Branding.xml b/suzzy-v0-standalone/views/library/articles/home/Branding.xml new file mode 100644 index 0000000..016ecdd --- /dev/null +++ b/suzzy-v0-standalone/views/library/articles/home/Branding.xml @@ -0,0 +1,64 @@ +
+ +
Usage
+ + This page contains high quality versions of promotional assets from most + suzzy projects and games. Please use common sense and don't hesistate to + contact me if you have any + questions. + + + Do not use these assets commercially or for impersonation. + + Hint: You can click on any image to download it to your device. + + +
suzzy games
+ + + Trivia: The name for our ghost mascot is Boo! You can find them in the logo. + Trivia: The accent color for suzzy is #A1A8FF and is actually considered to be a light sky blue. Not purple! + + +
BECCA2D
+ + BECCA2D is a 2D Web Game Engine developed by suzzy games and is + used in the upcoming super marimari_eno fan-game. + + + Trivia: BECCA2D is named after my sister, since my nickname for her is Becca. + + +
Project Monke
+ Project Monke is an upcoming platformer game developed by suzzy games. + + + + + + Trivia: The Polka Dot pattern was inspired by DDLC+ promo art. + + +
Cart Ride Around a Fumo
+ + Cart Ride around a Fumo is a ROBLOX experience developed by suzzy games. + Promo and Game Icon Art drawn by @mondaeakari. + + + + + + + Trivia: The first icon didn't do so well to bring in new players. + Which sucks because I liked it ;-; + +
\ No newline at end of file diff --git a/suzzy-v0-standalone/views/library/articles/home/Welcome.xml b/suzzy-v0-standalone/views/library/articles/home/Welcome.xml new file mode 100644 index 0000000..b91e2a1 --- /dev/null +++ b/suzzy-v0-standalone/views/library/articles/home/Welcome.xml @@ -0,0 +1,27 @@ +
+ + Welcome to the library! + + This site contains documentation and guides intended for developers. + You can navigate the website using the links on the side. Or above if you're on mobile! + + + Heads up! + + Documentation and features are still early in development, you may occasionally + encounter an unexpected 500 Internal Server Error or find a random typo. + + + Please send bug reports or feature requests to + bakonpancakz@gmail.com + +
\ No newline at end of file diff --git a/suzzy-v0-standalone/views/library/articles/legal/Privacy Policy.xml b/suzzy-v0-standalone/views/library/articles/legal/Privacy Policy.xml new file mode 100644 index 0000000..b1555d9 --- /dev/null +++ b/suzzy-v0-standalone/views/library/articles/legal/Privacy Policy.xml @@ -0,0 +1,24 @@ +
+
Introduction
+ + This document entails the general privacy policy, covering all suzzy services. + Specific services may override it as necessary — please refer to their respective documents for more information. + + + Last Updated on May 11th 2024 + + +
1. todo
+ + here's where i would put my privacy policy. IF I HAD ONE! + +
\ No newline at end of file diff --git a/suzzy-v0-standalone/views/library/articles/legal/Terms of Service.xml b/suzzy-v0-standalone/views/library/articles/legal/Terms of Service.xml new file mode 100644 index 0000000..23dda76 --- /dev/null +++ b/suzzy-v0-standalone/views/library/articles/legal/Terms of Service.xml @@ -0,0 +1,24 @@ +
+
Introduction
+ + This document entails the general terms of service, covering all suzzy services. + Specific services may override it as necessary — please refer to their respective documents for more information. + + + Last Updated on May 11th 2024 + + +
1. todo
+ + here's where i would put my terms of service. IF I HAD ONE! + +
\ No newline at end of file diff --git a/suzzy-v0-standalone/views/library/articles/status/Endpoints.xml b/suzzy-v0-standalone/views/library/articles/status/Endpoints.xml new file mode 100644 index 0000000..e160fbb --- /dev/null +++ b/suzzy-v0-standalone/views/library/articles/status/Endpoints.xml @@ -0,0 +1,135 @@ +
+ +
Get Widget
+ GET /widget.json + + Returns a unique Widget Object. This + is the same endpoint use for the widget displayed in the top-right + corner of the suzzy games webpage. + + + Widget Object + + + Field + Type + Description + + + status + string + Current status either "online", "degraded", or "down" + + + counter_players_online + number? + Estimated number of players online + +
+ This endpoint returns the headers 'X-Refresh' and 'Etag' + + +
Get Status
+ GET /status.json + + Returns a unique Status Object. Includes + the current status, an array of + tracker objects, + and an array of + event objects + which occurred today. + + Status Object + + + Field + Type + Description + + + status + string + Current status either "online", "degraded", or "down" + + + trackers + Array<Trackers> + List of Trackers + + + events + Array<Events> + List of Events for Today + +
+ This endpoint returns the headers 'X-Refresh' and 'Etag' + + Notice: This endpoint is compressed with gzip. + Your client must support and notify the server that it accepts gzip + encoded content via the Accept-Encoding header. Otherwise + your request will be rejected with a 415 Unsupported Media Type + status code. + + + +
Get Events
+ GET /events + + Returns an array of Event Objects + for today. You can request a specific day using the query parameter date in + MM/DD/YYYY format (ex. date=06/26/2003). + + + +
Get Events Summary
+ GET /events/summary + + + Returns a Unique Summary Object for this month. + You can request a specific month using the query parameter date in + MM/YYYY format. (ex. date=06/2003) + + Summary Object + + + Field + Type + Description + + + uptime + string + Uptime Percentage in Float + + + results + ... + See Description Below + +
+ + The results field is a JSON object with the keys being the day of the month, and it's + value being an Array with three integers. Each integer represents how many seconds of + Degredation, Downtime, and Maintenance occurred + that day, respectively. + + + +{ + "uptime": "99.99", // Uptime Percent as Float + "results": { + "0": [0, 0, 3600], // "Day": [Seconds Degraded, Downtime, Maintenance] + "12": [300, 0, 0], // Ex. 5 Minutes of Downtime on the 12th day of the month + } +} + +
\ No newline at end of file diff --git a/suzzy-v0-standalone/views/library/articles/status/Reference.xml b/suzzy-v0-standalone/views/library/articles/status/Reference.xml new file mode 100644 index 0000000..e7cd6e3 --- /dev/null +++ b/suzzy-v0-standalone/views/library/articles/status/Reference.xml @@ -0,0 +1,310 @@ +
+ +
Sending Requests
+ + All requests to the Status Service should be made to the following URL: + https://status.suzzygames.com/api/ + + +
Automatic Refreshing
+ + This service updates the widget + and status endpoints + every minute. + + + You can use the X-Refresh header to calculate how many float seconds + your client should wait before attempting to make another request. + + + This service also returns a ETag header which you can optionally include + in the next request. If nothing has changed the service will respond with a + 304 Not Modified and an empty body. + + +X-Refresh: 12.73 # Seconds until next refresh +ETag: 1735594572 # Unique Cache Identifier + + + Note: The 'X-Refresh' header is randomly offset up to 5 seconds to + help mitigate the sudden spike in traffic. + + +
Service Unavailable
+ + On rare occasions the Service may return a 503 Service Unavailable + error. This happens when the service has just restarted (usually due to an update) + and is doing some digital housekeeping before preparing the widget + and status endpoints. + + + The error will however return the X-Refresh header and the + aforementioned endpoints should become available then. + + + Note: The status service always returns an empty body when an error occurs. + Your client should not attempt to parse any JSON. + + +
About Trackers
+ + Tracker Object + + All trackers share the same base fields, but some fields are only + included for their respective types. + + + Charts take in values from multiple servers and average them + every specified interval compiling it into a Data Point. + Charts return the interval, history, and history_length fields. + + + Counters take in individual numbers from multiple servers and accumulate + them together as one number. Counters return the value field. + + + Services keep track of multiple servers to ensure they're healthy and operational. + If the health check fails their status is changed to either degraded or down + generating an event and alerting an engineer. + Services return the status field. + + + + Field + Type + Description + + + id + string + Unique Tracker Identifier + + + public + boolean + Is this tracker public? (Internal) + + + ephemeral + boolean + Clear tracker data on startup? (Internal) + + + name + string + User-Friendly name for this tracker + + + description + string + Description that appears when mouse is hovered over + + + tags + Array<string> + Strings used to categorize this tracker + + + type + integer + The Type of Tracker + + + value + integer? + COUNTER: Current Tracker Value + + + status + integer? + SERVICE: Current Service Status + + + interval + integer? + CHART: Data Point Interval + + + history_length + integer? + CHART: Data Point History Length + + + history + Array<DataPoint>? + CHART: Data Point History + +
+ + Tracker Types + + + Enum + Value + + + COUNTER + 1 + + + CHART + 2 + + + SERVICE + 3 + +
+ + Service Status + + + Enum + Value + + + HEALTHY + 0 + + + DEGRADED + 1 + + + DOWN + 2 + +
+ + Chart Data Point + + Data Points are Arrays that contain two integers. + The first integer is a timestamp for when the service compiled + the data into a point and can be converted into a UNIX Timestamp with + TIMESTAMP * (INTERVAL * 60) * 1000. The second integer is the + calculated average value. + + + You can then reverse iterate over the trackers history field + to plot the values on a line chart. + + + + Index + Type + Description + + + 0 + integer + Data Timestamp + + + 1 + integer + Data Value + +
+ +
About Events
+ Event Object + + Events are generated when downtime is detected or for when downtime is + scheduled. They include "helpful" messages either generated by + the service itself or personally written by an engineer. + + + + field + Type + Description + + + id + string + Unique Event Identifier + + + type + integer + Event Type + + + affects + string + The ID of Tracker this event pertains to + + + occurred + ISO Timestamp + When this event began + + + resolved + ISO Timestamp? + When this event was resolved + + + messages + Array<Message> + List of Message Objects (Unsorted) + +
+ + Event Types + + + Enum + Value + + + DEGREDATION + 0 + + + DOWNTIME + 1 + + + MAINTENANCE + 2 + +
+ Their respective Hex Colors: #f1c40f, #ff005b, #6cc0e0 + + Message Object + + These message contain HTML Content. Which usually only use the + standard text formatting tags <b> <s> <i>. + + + They may also occasionally include an image tag. + + + + Field + Type + Description + + + created + ISO Timestamp + When this message was written + + + message + string + HTML Content + +
+ +
\ No newline at end of file diff --git a/suzzy-v0-standalone/views/library/browser.pug b/suzzy-v0-standalone/views/library/browser.pug new file mode 100644 index 0000000..595d474 --- /dev/null +++ b/suzzy-v0-standalone/views/library/browser.pug @@ -0,0 +1 @@ +extends template \ No newline at end of file diff --git a/suzzy-v0-standalone/views/library/manifest.json b/suzzy-v0-standalone/views/library/manifest.json new file mode 100644 index 0000000..9c20bc3 --- /dev/null +++ b/suzzy-v0-standalone/views/library/manifest.json @@ -0,0 +1,47 @@ +{ + "itemsPerPage": 8, + "noBrowser": true, + "authors": [ + { + "id": "", + "name": "suzzy", + "icon": "" + } + ], + "categories": [ + { + "id": "home", + "name": "Home", + "icon": "" + }, + { + "id": "legal", + "name": "Legal", + "icon": "" + }, + { + "id": "api", + "name": "API", + "icon": "" + }, + { + "id": "status", + "name": "Status", + "icon": "" + } + ], + "articles": [ + "home/Welcome.xml", + "home/Branding.xml", + "legal/Privacy Policy.xml", + "legal/Terms of Service.xml", + "api/Users.xml", + "api/OAuth2.xml", + "api/Applications.xml", + "api/Reference.xml", + "api/Tutorials.xml", + "api/Changelog.xml", + "status/Reference.xml", + "status/Endpoints.xml" + ] +} \ No newline at end of file diff --git a/suzzy-v0-standalone/views/library/post.pug b/suzzy-v0-standalone/views/library/post.pug new file mode 100644 index 0000000..595d474 --- /dev/null +++ b/suzzy-v0-standalone/views/library/post.pug @@ -0,0 +1 @@ +extends template \ No newline at end of file diff --git a/suzzy-v0-standalone/views/library/template.pug b/suzzy-v0-standalone/views/library/template.pug new file mode 100644 index 0000000..c6416ae --- /dev/null +++ b/suzzy-v0-standalone/views/library/template.pug @@ -0,0 +1,110 @@ +- + // Create Sidebar Categories + const t = {} + for (const a of articles) { + const k = categories.find(c => c.id === a.category.id).name + t[k] ? t[k].push(a) : t[k] = [a] + } + // Sort Sidebar Articles + for (const v of Object.values(t)) { + v.sort((a, b) => parseInt(a.info.created) - parseInt(b.info.created)) + } + +doctype html +html(lang='en') + head + meta(charset='UTF-8') + meta(name='viewport' content='width=device-width, initial-scale=1.0') + link(rel='icon' type='image/png' href='file://library/favicon.png') + link(rel='stylesheet' href='file://styles/template-global.css') + link(rel='stylesheet' href='file://styles/template-article.css') + link(rel='stylesheet' href='file://styles/browser-library.css') + link(rel='stylesheet' href='file://fonts/font-poppins.css') + meta(property='theme-color' content='#A1A8FF') + meta(property='og:image' content='file://library/logo-og.png?direct=true') + meta(property='og:title' content=`library | ${relevantArticle.category.name} > ${relevantArticle.info.title}`) + meta(property='og:description' content=relevantArticle.info.snippet) + meta(name='description' content=relevantArticle.info.snippet) + title library - #{relevantArticle.info.title} (#{relevantArticle.category.name}) + body + div(class='wrapper-search') + div(class='container-search special-dropshadow') + + div(class='search-actions') + input(class='search-input' tabindex='-1' type='text' name='Search Library' placeholder='Search Library') + button(class='search-close' tabindex='-1') × + + div(class='search-body') + //- Loading Icon + div(class='pane' id='loading') + img(src='file://icons/diagram-loading.svg' style='width: 64px;') + + //- Error Message + div(class='pane' id='error') + img(src='file://library/search/search-error.png') + span I ran into an issue... + p(class='text-tip text-muted') TypeError: Failed to Fetch + + //- First Open/Empty Query + div(class='pane' id='help') + img(src='file://library/search/search-initial.png') + span Can't find something? Let me help you out! + p(class='text-tip text-muted') start typing to begin our search + + //- Empty Results + div(class='pane' id='empty') + img(src='file://library/search/search-empty.png') + span I couldn't find anything! + p(class='text-tip text-muted') try again with some different keywords + + //- Some Results + div(class='pane' id='results') + + div(class='search-footer') + p(class='text-tip text-muted') + div(style='flex-basis: 100%;') + p Close #[span ESC] + p Navigate #[span ⬆⬇] + p Select #[span ↩] + + + div(class='wrapper-layout') + div(class='layout-navigation background-scroll special-shadow') + //- Logo and Search Box + a(class='navigation-button' id='skipper' href='#home') + | Skip to Content + a(href='/') + img( + class='navigation-logo' + alt='Logo for suzzy games' + src='file://library/logo-navigation.png' + ) + button(class='navigation-button' id='search-open') + | Search Library + + //- Render Sidebar + each [categoryName, categoryArticles] of Object.entries(t) + p(class='navigation-header') #{categoryName} + each someArticle of categoryArticles + - var thisLink = `${relevantArticle.category.id}/${relevantArticle.info.id}` + - var someLink = `${someArticle.category.id}/${someArticle.info.id}` + - var matchLink = (thisLink === someLink) + a(class='navigation-link' href=`/library/${someLink}` active=matchLink) + | #{someArticle.info.title} + if matchLink + each element of someArticle.elements + if element.type === 'header' + a(class='navigation-chapter' href=`#${formatString(element.value)}`) + | #{element.value} + + div(class='layout-content' id='home') + //- Article Content + include ../_TemplateArticle + + //- Mobile Users get a Return to Top Button + //- Also includes spacer so content doesn't cover button + div(class='layout-spacer') + a(class='default layout-return' href='#home') ▲ + + script(src='file://scripts/shared-article.ts') + script(src='file://scripts/browser-library.ts') \ No newline at end of file diff --git a/suzzy-v0-standalone/views/profile/applications.pug b/suzzy-v0-standalone/views/profile/applications.pug new file mode 100644 index 0000000..2bf305a --- /dev/null +++ b/suzzy-v0-standalone/views/profile/applications.pug @@ -0,0 +1,102 @@ +extends ../_TemplateProfile +block pageHeader + title suzzy - Manage Applications +block pageContent + - var loading_message = 'Applications' + - var empty_message = 'No Applications' + - var empty_source = 'file://icons/icon-app-none.svg' + - var empty_alt = 'No Apps' + p(class='text-hero') Applications + p(class='text-tip') Create applications that interface with suzzy games and it's users. + p(class='text-tip') + | Check out the #[a(href='/library' class='text-tip') library] + | for documentation on how to develop with suzzy games. + div(class='collection-divider') + button(class='default wide' id='action-create' hidden) + | Create New Application + include ../_TemplateProfilePage + + //- //- Template Redirect URL + //- div(class='input-redirect' hidden) + //- input( + //- type='text' + //- class='text' + //- autocomplete='off' + //- maxlength=128 + //- placeholder='https://example.org/oauth2-redirect' + //- required=true + //- pattern='^(http|https)://.*' + //- ) + //- button(class='default destructive') Delete + + //- //- Templat Application + //- div(class='collection-item special-shadow' id='application-template' hidden) + //- //- Application Metadata + //- div(class='application-header') + //- img( + //- id='app-icon' + //- aria-hidden='true' + //- class='application-icon' + //- onload='this.style.opacity=1' + //- ) + //- div(class='application-info') + //- p(id='app-name' class='text-header') Name + //- p(id='app-desc' class='text-tip') Description + //- p(class='text-tip text-muted' id='verified-message' hidden) ★ Verified Application + //- button(id='input-edit' class='default') Edit + //- //- Application Options + //- div(class='application-options options-hidden') + //- div(class='divider-line') + //- //- Personalization + //- p(class='text-header') About + //- input( + //- id='input-name' + //- placeholder='Name' + //- autocomplete='off' + //- type='text' + //- class='text' + //- maxlength=32 + //- ) + //- textarea( + //- id='input-desc' + //- placeholder='Description' + //- autocomplete='off' + //- class='text' + //- maxlength=320 + //- rows=5 + //- style='max-height:200px;' + //- ) + //- input( + //- type='file' + //- id='input-icon-file' + //- accept='image/jpeg, image/webp, image/gif, image/png' + //- hidden + //- ) + //- button(class='default wide' id='input-icon') + //- | Upload Icon + //- button(class='small text-tip' id='input-icon-remove') + //- | Remove Icon + + //- //- Redirects + //- div(class='divider-line') + //- p(class='text-header') Redirects + //- div(id='container-redirects') + //- button(class='default wide' id='input-redirect-new') + //- | New Redirect + //- p(class='text-tip' id='input-clientid') Client ID: + + //- //- Actions + //- p(id='status-message') + //- button(class='default wide destructive' id='input-discard' disabled) + //- | Discard + //- button(class='default wide' id='input-save' disabled) + //- | Save Changes + + //- //- Danger Zone + //- div(class='divider-line') + //- button(class='default destructive wide' id='input-reset') + //- | Reset Secret Key + //- button(class='default destructive wide' id='input-delete') + //- | Delete Application + + script(src='file://scripts/profile-applications.ts') \ No newline at end of file diff --git a/suzzy-v0-standalone/views/profile/connections.pug b/suzzy-v0-standalone/views/profile/connections.pug new file mode 100644 index 0000000..eba7fdc --- /dev/null +++ b/suzzy-v0-standalone/views/profile/connections.pug @@ -0,0 +1,37 @@ +extends ../_TemplateProfile +block pageHeader + title suzzy - Manage Connections +block pageContent + - var loading_message = 'Connections' + - var empty_message = 'No Connections' + - var empty_source = 'file://icons/icon-plug-socket.svg' + - var empty_alt = 'Disconnect' + p(class='text-hero') Connections + p(class='text-tip') Manage the connections your account has with applications. + p(class='text-tip') + | Official Applications are labelled with + | #[span(class='text-tip text-muted') (★ Verified Application)] + | under their description. + div(class='collection-divider') + include ../_TemplateProfilePage + + //- //- Template Scope + //- div(class='scope' id='scope-template' hidden) + //- div(class='dot-green') + //- span Scope Description + + //- //- Template Item + //- div(class='collection-item special-shadow' id='application-template' hidden) + //- div(class='application-header') + //- img(aria-hidden='true' class='application-icon' onload='this.style.opacity=1') + //- div(class='application-info') + //- p(class='text-header') Name + //- p(class='text-tip') Description + //- p(class='text-tip text-muted' id='verified-message' hidden) ★ Verified Application + //- p(class='text-error' id='error-message') + //- button(class='default destructive') Disconnect + //- div(class='text-box') + //- p(class='text-tip') This application can: + //- div(class='scopes') + + script(src='file://scripts/profile-connections.ts') \ No newline at end of file diff --git a/suzzy-v0-standalone/views/profile/devices.pug b/suzzy-v0-standalone/views/profile/devices.pug new file mode 100644 index 0000000..d342f63 --- /dev/null +++ b/suzzy-v0-standalone/views/profile/devices.pug @@ -0,0 +1,30 @@ +extends ../_TemplateProfile +block pageHeader + title suzzy - Manage Devices +block pageContent + - var loading_message = 'Devices' + - var empty_message = 'No Devices' + - var empty_source = 'file://icons/icon-app-none.svg' + - var empty_alt = 'Devices' + p(class='text-hero') Devices + p(class='text-tip') Here's a list of devices that are currently logged in with your account. + p(class='text-tip') If you see a device you don't recognize, log it out and change your password immediately. + div(class='collection-divider') + include ../_TemplateProfilePage + + //- //- Template Item + //- div(class='item-device collection-item special-shadow' id='device-template' hidden) + //- div(class='device-icon') + //- img(aria-hidden='true' src='/assets/images/icons/icon-desktop.svg' alt='Icon for Desktop') + //- div(class='device-info') + //- div(class='info-alongside') + //- p(id='browser') Chrome on Windows 10 + //- p(id='me' class='text-tip text-muted' hidden) (This Device) + //- div(class='info-alongside') + //- p(id='location') Modesto, United States + //- p(id='lastseen' class='text-tip text-muted') 3 hours ago + //- p(class='text-error' id='error-message') + //- button(class='device-action') + //- img(src='/assets/images/icons/icon-cross.svg' alt='Icon for Cross') + + script(src='file://scripts/profile-devices.ts') diff --git a/suzzy-v0-standalone/views/profile/index.pug b/suzzy-v0-standalone/views/profile/index.pug new file mode 100644 index 0000000..64d795a --- /dev/null +++ b/suzzy-v0-standalone/views/profile/index.pug @@ -0,0 +1,96 @@ +extends ../_TemplateProfile +block pageHeader + title suzzy - Manage Profile +block pageContent + - var loading_message = 'Profile' + p(class='text-hero') Profile + p(class='text-tip') Personalize your profile to make it uniquely yours! + p(class='text-tip') Animated Avatars and Banners are supported! Uploads up to 8MB are allowed. + div(class='collection-divider') + include ../_TemplateProfilePage + + //- div(class='profile-options text-nopad') + //- //- About Section + //- p(class='text-header') About Me + //- input( + //- type='text' + //- id='input-displayname' + //- class='text' + //- placeholder='Displayname' + //- maxlength=32 + //- ) + //- input( + //- type='text' + //- id='input-subtitle' + //- class='text' + //- placeholder='Subtitle' + //- maxlength=32 + //- ) + //- textarea( + //- id='input-bio' + //- class='text' + //- placeholder='Biography' + //- rows=3 + //- maxlength=320 + //- style='max-height:200px;' + //- ) + + //- //- Images Section + //- p(class='text-header') Images + //- input( + //- type='file' + //- id='input-avatar-file' + //- accept='image/jpeg, image/webp, image/gif, image/png' + //- hidden + //- ) + //- input( + //- type='file' + //- id='input-banner-file' + //- accept='image/jpeg, image/webp, image/gif, image/png' + //- hidden + //- ) + //- button(class='default wide' id='button-avatar') + //- | Upload Avatar + //- button(class='small text-tip' id='button-avatar-remove') + //- | Remove Avatar + //- button(class='default wide' id='button-banner') + //- | Upload Banner + //- button(class='small text-tip' id='button-banner-remove') + //- | Remove Banner + + //- //- Colors Section + //- p(class='text-header') Accents + //- div(class='widget-container') + //- div(class='widget-color') + //- label(class='text-tip' for='input-banner') Banner + //- input(id='input-banner' type='color' class='color-preview') + //- div(class='widget-color') + //- label(class='text-tip' for='input-border') Border + //- input(id='input-border' type='color' class='color-preview') + //- div(class='widget-color') + //- label(class='text-tip' for='input-background') Background + //- input(id='input-background' type='color' class='color-preview') + + //- //- Actions Section + //- div(class='divider-line') + //- p(id='profile-status') + //- button(class='default wide destructive' id='button-cancel' disabled) + //- | Discard + //- button(class='default wide' id='button-submit' disabled) + //- | Save Changes + + //- //- Preview + //- div(class='profile-container special-shadow') + //- div(id='profile-banner') + //- div(class='profile-header') + //- img(id='profile-avatar' alt='My Avatar') + //- div(id='profile-badges') + //- div(class='profile-about text-nopad') + //- div(class='info-alongside') + //- p(id='profile-displayname') + //- p(id='profile-username') + //- p(id='profile-subtitle' class='text-tip') + //- div(class='divider-line' id='profile-divider' hidden) + //- pre(id='profile-bio' class='text-tip') + + script(src='file://scripts/profile-index.ts') \ No newline at end of file diff --git a/suzzy-v0-standalone/views/profile/security.pug b/suzzy-v0-standalone/views/profile/security.pug new file mode 100644 index 0000000..07c1506 --- /dev/null +++ b/suzzy-v0-standalone/views/profile/security.pug @@ -0,0 +1,57 @@ +extends ../_TemplateProfile +block pageHeader + title suzzy - Manage Account +block pageContent + - var loading_message = 'Account' + p(class='text-hero') Account + p(class='text-tip') Welcome to your account's security settings. + p(class='text-tip') Here, you can manage various security options to help protect your account. + div(class='collection-divider') + include ../_TemplateProfilePage + + //- div(id='security-wrapper' hidden) + //- //- Email Address + //- p(class='text-header') Email Address + //- p(class='text-tip' id='preview-email' aria-live='polite') + //- | Current Email Address: + //- p(class='text-error' id='error-email') + //- div(class='collection-wrapper') + //- button(class='default' id='email-reveal') + //- | Reveal Email Address + //- button(class='default' id='email-edit') + //- | Edit Email Address + //- button(class='default' id='email-resend' hidden) + //- | Resend Verification Email + //- div(class='divider-spacer') + + //- //- Password + //- p(class='text-header') Password + //- p(class='text-tip') Change your account password, doing so will sign you out on all other devices. + //- button(class='default' id='password-change') + //- | Change Password + //- div(class='divider-spacer') + + //- //- Multi-Factor + //- p(class='text-header') Multi-Factor Authentication (MFA) + //- p(class='text-tip') Use an Authenticator App to add an extra layer of security to your account. + //- p(class='text-error' id='error-mfa') + //- div(class='collection-wrapper') + //- button(class='default' id='mfa-setup-start' hidden) + //- | Setup Authenticator App + //- button(class='default destructive' id='mfa-setup-remove' hidden) + //- | Remove Authenticator App + //- button(class='default' id='mfa-recovery-view' hidden) + //- | View Recovery Codes + //- button(class='default destructive' id='mfa-recovery-regen' hidden) + //- | Generate New Codes + //- div(class='divider-spacer') + + //- //- Account Deletion + //- div(class='divider-spacer') + //- p(class='text-header') Danger Zone + //- p(class='text-tip') Buttons that will cause major changes to your account. + //- p(class='text-error' id='error-danger') + //- button(class='default destructive' id='danger-deleteaccount') + //- | Request Account Deletion + + script(src='file://scripts/profile-security.ts') diff --git a/suzzy-v0-standalone/views/public/challenge-picross.pug b/suzzy-v0-standalone/views/public/challenge-picross.pug new file mode 100644 index 0000000..9bad184 --- /dev/null +++ b/suzzy-v0-standalone/views/public/challenge-picross.pug @@ -0,0 +1,187 @@ +extends ../_TemplateSpecial +block pageHeader + meta(name='robots' content='noindex') + title Secret Challenge - Picross! + // Was bored and I love picross puzzles. Enjoy this little minigame. + // This is also a one-time thing! There are no more easter eggs that give out badges! + +block pageEscaped + script(src='file://scripts/shared-utils.ts') + script(src='file://scripts/challenge-picross.ts') + +block pageContent + // In case you're wondering I sampled my keyboard for the audio. + // Also no you cannot mute it, now suffer. + audio(hidden src='file://audio/picross_2.mp3' id='set') + audio(hidden src='file://audio/picross_1.mp3' id='release') + + p(class='text-header') Secret Challenge: Picross! + p(class='text-tip') Solve this puzzle to earn the picross badge for your profile. + p(class='text-tip text-muted') Controls: Left click to fill, right click to mark. + + // thank you gzip for savin bandwidth + div(class='special-picross') + div(class='picross-row') + p(class='picross-hint') + p(class='picross-hint') + p(class='picross-hint') + p(class='picross-hint') + p(class='picross-hint') + p(class='picross-hint') 1 + p(class='picross-hint') + p(class='picross-hint') 1 + p(class='picross-hint') + p(class='picross-hint') + p(class='picross-hint') + p(class='picross-hint') + div(class='picross-row') + p(class='picross-hint') + p(class='picross-hint') + p(class='picross-hint') + p(class='picross-hint') + p(class='picross-hint') + p(class='picross-hint') 1 + p(class='picross-hint') 3 + p(class='picross-hint') 1 + p(class='picross-hint') + p(class='picross-hint') 1 + p(class='picross-hint') + p(class='picross-hint') + div(class='picross-row') + p(class='picross-hint') + p(class='picross-hint') + p(class='picross-hint') + p(class='picross-hint') 1 + p(class='picross-hint') 7 + p(class='picross-hint') 5 + p(class='picross-hint') 5 + p(class='picross-hint') 4 + p(class='picross-hint') 8 + p(class='picross-hint') 3 + p(class='picross-hint') 2 + p(class='picross-hint') 2 + div(class='picross-row') + p(class='picross-hint') + p(class='picross-hint') + p(class='picross-hint') 3 + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + div(class='picross-row') + p(class='picross-hint') 1 + p(class='picross-hint') 1 + p(class='picross-hint') 1 + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + div(class='picross-row') + p(class='picross-hint') + p(class='picross-hint') + p(class='picross-hint') 5 + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + div(class='picross-row') + p(class='picross-hint') + p(class='picross-hint') 2 + p(class='picross-hint') 2 + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + div(class='picross-row') + p(class='picross-hint') + p(class='picross-hint') 3 + p(class='picross-hint') 1 + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + div(class='picross-row') + p(class='picross-hint') + p(class='picross-hint') 5 + p(class='picross-hint') 1 + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + div(class='picross-row') + p(class='picross-hint') + p(class='picross-hint') + p(class='picross-hint') 8 + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + div(class='picross-row') + p(class='picross-hint') + p(class='picross-hint') + p(class='picross-hint') 7 + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + div(class='picross-row') + p(class='picross-hint') + p(class='picross-hint') + p(class='picross-hint') 5 + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + button(class='picross-button' dynamic) + + //- User Actions + div(class='divider-line') + p(class='text-tip' id='time-spent') Time Spent: 00:00:00 + div(class='special-alongside') + // NEVER GIVE UP!!! + a(dynamic class='default' target='_blank' href='https://www.youtube.com/watch?v=KxGRhd_iWuE&t=2s' onclick='this.onclick=()=>{ alert("quitter..."); window.location.assign("/") }') I Give Up... + a(dynamic class='default' id='done') Check it! \ No newline at end of file diff --git a/suzzy-v0-standalone/views/public/index.pug b/suzzy-v0-standalone/views/public/index.pug new file mode 100644 index 0000000..d4a5f50 --- /dev/null +++ b/suzzy-v0-standalone/views/public/index.pug @@ -0,0 +1,28 @@ +extends ../_TemplatePublic +block pageHeader + link(rel='stylesheet' href=`file://styles/public-homepage.css`) + meta(property='og:title' content='suzzy games') + meta(property='og:description' content='The official suzzy games website.') + meta(name='description' content='The official suzzy games website.') + meta(property='og:image' content='file://images/public-logo-og.png') + title suzzy games +block pageEscaped + //- Widget: Hero + div(class='widget widget-hero-wrapper') + div(class='widget-hero') + p(class='text-hero') Welcome! + p + | suzzy games is a personal project from the mind of + | #[a(target='_blank' href='https://twitter.com/bakonpancakz') bakonpancakz] + i + p(class='text-muted text-tip') ❝There is no theme, there is no incentive, just pure passion and efficient code❞ + div(class='divider-spacer') + p All projects are stuck in development hell until they suddenly aren't + i + p(class='text-tip') + | Check out the #[a(class='text-tip' href='/blog') blog] to see what im up to + | or play some old #[a(class='text-tip' href='/projects') projects] + div(class='divider-spacer') + p(class='text-tip text-muted' id='birthday') + | Active since May 13th, 2021 + script(src='file://scripts/public-homepage.ts') \ No newline at end of file diff --git a/suzzy-v0-standalone/views/public/login.pug b/suzzy-v0-standalone/views/public/login.pug new file mode 100644 index 0000000..d0c3545 --- /dev/null +++ b/suzzy-v0-standalone/views/public/login.pug @@ -0,0 +1,6 @@ +extends ../_TemplateSpecial +block pageHeader + title suzzy games - Login +block pageEscaped + script(src='file://scripts/login-index.ts') + img(class='special-corner' src='file://images/login-globe.png') \ No newline at end of file diff --git a/suzzy-v0-standalone/views/public/password-reset.pug b/suzzy-v0-standalone/views/public/password-reset.pug new file mode 100644 index 0000000..f8fee95 --- /dev/null +++ b/suzzy-v0-standalone/views/public/password-reset.pug @@ -0,0 +1,6 @@ +extends ../_TemplateSpecial +block pageHeader + title suzzy games - Reset Password +block pageEscaped + script(src='file://scripts/login-passwordreset.ts') + img(class='special-corner' src='file://images/login-globe.png') \ No newline at end of file diff --git a/suzzy-v0-standalone/views/public/password-update.pug b/suzzy-v0-standalone/views/public/password-update.pug new file mode 100644 index 0000000..5ada35f --- /dev/null +++ b/suzzy-v0-standalone/views/public/password-update.pug @@ -0,0 +1,6 @@ +extends ../_TemplateSpecial +block pageHeader + title suzzy games - Update Password +block pageEscaped + script(src='file://scripts/login-passwordupdate.ts') + img(class='special-corner' src='file://images/login-globe.png') \ No newline at end of file diff --git a/suzzy-v0-standalone/views/public/projects.pug b/suzzy-v0-standalone/views/public/projects.pug new file mode 100644 index 0000000..1be9044 --- /dev/null +++ b/suzzy-v0-standalone/views/public/projects.pug @@ -0,0 +1,105 @@ +extends ../_TemplatePublic +block pageHeader + link(rel='stylesheet' href='file://styles/public-projects.css') + meta(property='og:title' content='suzzy games - projects') + meta(property='og:description' content='Projects from over the years.') + meta(name='description' content='Projects from over the years.') + meta(property='og:image' content='https://suzzygames.com/assets/images/og-logo.png') + title suzzy games - projects +block pageEscaped + - + const PROJECT_MANIFEST = [ + { + accent: '#ff9d2f', + brightness: 0.75, + background: 'file://projects/monke/background.jpeg', + name: 'Project Monke', + state: 'In Development', + platforms: ['desktop', 'mobile'], + images: [ + { label: 'Promo Banner', url: 'file://projects/monke/image-1.jpeg' }, + ], + description: [ + 'Can only be explained as Super Monkey Ball and Super Mario Maker in one.', + 'Check the blog for news!', + ], + actions: [ + { label: 'View Blog Posts', url: '/blog/project-monke', target: '' }, + ], + }, + { + accent: '#91defd', + brightness: 0.75, + name: 'Cart Ride Around a Fumo', + background: 'file://projects/craf/background.jpeg', + platforms: ['desktop', 'mobile'], + state: '2021', + images: [ + { label: 'Promo Artwork #1', url: 'file://projects/craf/image-1.jpeg' }, + { label: 'Promo Artwork #2', url: 'file://projects/craf/image-2.jpeg' }, + ], + description: [ + 'My own spin on the classic ROBLOX cart ride genre.', + 'Centered around fumos for no specific reason.', + 'Warning: Contains explosions', + ], + actions: [ + { label: 'Play on ROBLOX', url: 'https://www.roblox.com/games/9987852646/Cart-Ride-Around-a-Fumo', target: '_blank' }, + ], + } + ] + + //- Layout: Header + div(class='wrapper-widget-hero') + div(class='widget-hero') + p(class='text-hero') > Projects < + p All of my projects in reverse chronological order (a.k.a. new to old) + + //- Layout: Projects + each p in PROJECT_MANIFEST + div(class='widget widget-project' style=`--accent: ${p.accent}; --brightness: ${p.brightness}; --background: url('${p.background}');`) + + //- Stack div elements with background underneath the foreground + //- Image will be darkened using 'brightness' css variable + div(class='project-background') + div(class='project-foreground') + + //- Project: Metadata + div(class='project-content') + + //- Name, Platforms, Descriptions + div(class='project-info') + div(class='project-header') + p(class='text-header') #{p.name} + div(class='project-platforms') + if p.platforms.find(p => p === 'desktop') + img(class='platform-icon' alt='Icon for Desktop Devices' src='file://icons/icon-computer.svg') + if p.platforms.find(p => p === 'mobile') + img(class='platform-icon' alt='Icon for Mobile Devices' src='file://icons/icon-mobile.svg') + p • #{p.state} + + div(class='divider-line' style='background-color: var(--accent);') + div(class='project-description') + each s in p.description + p #{s} + + //- Screenshots + div(class='project-gallery') + div(class='project-gallery-upper') + button(class='project-gallery-button' id='last') < + div(class='project-gallery-scroll') + each i in p.images + img(class='gallery-image' alt=`${p.name} - ${i.label}` src=i.url) + button(class='project-gallery-button' id='next') > + + div(class='project-content project-lower') + div(class='project-actions') + each a in p.actions + a(class='default wide' target=a.target href=a.url) #{a.label} + + //- Screenshot Gallery Controls + div(class='project-gallery-actions') + each _, i in p.images + div(class=`gallery-dot${i===0 ? ' gallery-dot-active' : ''}`) + + script(src='file://scripts/public-projects.ts') \ No newline at end of file diff --git a/suzzy-v0-standalone/views/public/signup.pug b/suzzy-v0-standalone/views/public/signup.pug new file mode 100644 index 0000000..fea1394 --- /dev/null +++ b/suzzy-v0-standalone/views/public/signup.pug @@ -0,0 +1,6 @@ +extends ../_TemplateSpecial +block pageHeader + title suzzy games - Signup +block pageEscaped + script(src='file://scripts/login-signup.ts') + img(class='special-corner' src='file://images/login-globe.png') \ No newline at end of file diff --git a/suzzy-v0-standalone/views/public/verify-email.pug b/suzzy-v0-standalone/views/public/verify-email.pug new file mode 100644 index 0000000..fe36585 --- /dev/null +++ b/suzzy-v0-standalone/views/public/verify-email.pug @@ -0,0 +1,6 @@ +extends ../_TemplateSpecial +block pageHeader + title suzzy games - Verify Email +block pageEscaped + script(src='file://scripts/login-verifyemail.ts') + img(class='special-corner' src='file://images/login-globe.png') \ No newline at end of file diff --git a/suzzy-v0-standalone/views/public/verify-login.pug b/suzzy-v0-standalone/views/public/verify-login.pug new file mode 100644 index 0000000..c48716d --- /dev/null +++ b/suzzy-v0-standalone/views/public/verify-login.pug @@ -0,0 +1,6 @@ +extends ../_TemplateSpecial +block pageHeader + title suzzy games - Verify Login +block pageEscaped + script(src='file://scripts/login-verifylogin.ts') + img(class='special-corner' src='file://images/login-globe.png') \ No newline at end of file
+
Welcome to suzzy games!
+ + I made this site to serve as a hub for my game development hobby. There + is no theme, there is no incentive, just pure passion and efficient code. + I'll be sharing previews, progress, and generally whatever comes to mind. + So that's what this site is for! And this post contains a bunch of previews + of things I've worked on that might become their own game in the + future. + + Except for Cart Ride that one was just an update :P + + +
Cart Ride Around a Fumo
+ +
Cart Ride Around a Fumo + is my own spin on the cart ride genre that was all over in the olden days + of ROBLOX. It's a labor of love with way too much effort put into it, but + I like to look back at it and try to match that level of quality for + everything I do. + + Why Fumos? Because they're funny. + + I spent a whole two months on it and again, put way more effort into it + than I should have, but that's just how I roll (⌐■_■). I actually got a + lot of my cousins to playtest it and everybody had a lot of fun blowing + each other up. Which I like to think is what kickstarted my passion. + (my origin story) + + Overhead view of the in-game track + + Around ten people play it daily and people are still donating despite + its age, so I definitely did something right. (^_^) It even has a + couple videos on it, one of friends just having fun, + and a super duper edited one. + + Thanks guys <3 + + I actually planned to release a major update for it which would have added + cart customization and a track editor. But the update grew too large in + scope and is now being developed as its own standalone game. + (stay tuned!) + + + It's nothing too interesting in my opinion but here's what one of the + menus looked like. + + Screenshot of a scrapped concept showcasing cart customization + + +
Five Nights at Kanye 2
+ + The original FNAK + was made in only a few hours but my friends really liked the idea so I fleshed it + out a bit more and released a remastered version + just for fun. A few people liked it and sent me messages asking for a second + entry so I actually worked on it for a bit. + + Main Menu for Five Nights at Kanyes 2, taking place in a dimly lit restaurant + Wide angle view of the courtyard in Kanye's Mansion + Overhead view of Kanye's Mansion with each room labelled + + The story I had in mind was that this was a prequel to the first game, + and this was the night you broke into Kanye's house to steal DONDA 3 + for Twitter clout (now called x lol). Over the course of five + nights you (and up to 3 other friends) were supposed to collect + story items that would unlock another room after each night. + + + Kanye was also intended to have a better than average AI that would actively + hunt you down as you made mistakes. Something as small as leaving a light on + would make him suspicious and have him start to slowly but surely hunt you + down (working as a soft time limit). + + + For some added suspense, the music changes as Kanye's suspicious level changes. + All the way from clueless to chasing you down. It is based on Kanye's track + Closed on Sunday + and made by Dakota (he'll get a proper shoutout later!) + +