Initial Release
@@ -0,0 +1 @@
|
||||
dist
|
||||
@@ -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 `<article>` 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:
|
||||
- `<h>: Hint` Makes text grey and small
|
||||
- `<c>: Code` Highlights text and adds a dark background
|
||||
- `<a>: Link` Similar to Anchor, supports **target** and **href** attributes
|
||||
- `<b>: Bold` Renders text as bold or extra thicc
|
||||
- `<i>: Italic` Render Text as Italicized
|
||||
- `<s>: 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
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
})
|
||||
})()
|
||||
@@ -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)
|
||||
})
|
||||
})()
|
||||
@@ -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)
|
||||
})
|
||||
})()
|
||||
@@ -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)
|
||||
})
|
||||
})()
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
})()
|
||||
@@ -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)
|
||||
}),
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})()
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 978 B |
|
After Width: | Height: | Size: 887 B |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 205 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.2" baseProfile="tiny" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 300 256" overflow="visible" xml:space="preserve">
|
||||
<path fill="#181818" d="M295.5,195.1l-1.3-1.3c-2.4-2.3-5.3-3.7-8.4-4.1c7.9-9.8,11.9-22.2,11.9-36.5s-3.8-26.3-11.1-35.4
|
||||
c-5.7-7-12.8-12.6-20.9-16.3h12.4c3.9,0,7.7-1.6,10.5-4.4c2.8-2.8,4.3-6.7,4.3-10.6c0-18.9-7.4-34.8-21.3-45.8
|
||||
c-13-10.3-29.4-15.5-48.9-15.5c-11.7-0.1-23.2,2.1-34.1,6.7c0,0-0.1,0-0.1,0c-10.9,4.7-19.8,11.5-26.3,20.1
|
||||
c-4.4,5.8-7.6,12.6-9.2,19.8c-1.1-19.4-8.4-36.7-21.8-50C110.4,1.1,78-5.7,50.6,5C20.8,16.7,2.4,45.8,0.1,84.7c0,0.2,0,0.4,0,0.6
|
||||
c-0.8,33.6,10.4,66.1,32.3,94c19.5,24.9,46.8,45.4,78.7,59.1c26.8,11.5,55.7,17.6,83,17.6c5.1,0,10.1-0.2,15-0.6
|
||||
c35.1-3,65.1-16.4,86.7-38.9c2.8-2.9,4.3-6.7,4.2-10.7C300,201.8,298.3,197.9,295.5,195.1L295.5,195.1z"/>
|
||||
<path fill="#DCDCDC" d="M284,204.7c-46.5,30.5-110,24.6-143-28.2c-9.3-21.4-13.9-40.3-8.5-67.4C169.6,4.3,21.5-26,14.8,85.7
|
||||
c-2.9,129.6,190.5,203,270.4,120.3L284,204.7L284,204.7z M36.3,69c-0.8-1-1.2-2.3-1.1-3.6c0-1.3,0.7-2.6,1.7-3.5
|
||||
c1-0.8,2.3-1.2,3.6-1l21.4,1.9c1.3,0.1,2.5,0.7,3.3,1.7c0.8,1,1.2,2.4,1,3.7c-0.1,1.3-0.7,2.5-1.6,3.4c-1,0.8-2.3,1.2-3.6,1
|
||||
l-21.4-1.9C38.4,70.6,37.2,70,36.3,69z M91.1,109.3c2.8,2.8,2.9,7.3,0.2,10.2c-2.7,2.9-7.2,3-10,0.2c-0.1-0.1-0.2-0.2-0.3-0.3
|
||||
c-0.7-0.7-1.2-1.4-1.5-2.3c-0.2,0.1-0.4,0.2-0.6,0.3c-8.4,3.6-18-0.3-21.6-8.9c-3.6-8.5,0.3-18.4,8.7-22s18,0.3,21.6,8.9
|
||||
c1.6,3.8,1.7,8.2,0.4,12.1C89.2,107.9,90.2,108.5,91.1,109.3L91.1,109.3L91.1,109.3z M87.5,74c-1.7-2.2-1.4-5.3,0.6-7.1
|
||||
c1-0.8,2.3-1.2,3.6-1l21.4,1.9c1.3,0.1,2.5,0.7,3.3,1.7c0.8,1,1.2,2.4,1,3.7c-0.1,2.6-2.2,4.6-4.8,4.5c-0.1,0-0.3,0-0.4,0l-21.4-1.9
|
||||
C89.6,75.6,88.4,75,87.5,74L87.5,74L87.5,74z"/>
|
||||
<path fill="#DCDCDC" d="M194,193.9c-8.6-3.5-16.1-9.3-21.7-16.8c-5.5-7.4-8.2-16.2-8.2-26.2h28.5c0,6.6,3,11.8,8.9,15.8
|
||||
s13.5,5.9,22.6,5.9c9.3,0,16.5-1.9,21.5-5.6s7.6-8.6,7.6-14.8c0-6.8-2.8-11.6-8.5-14.5c-5.7-2.9-14.5-5.7-26.4-8.3
|
||||
c-10.9-2.5-19.8-5-26.7-7.6c-6.9-2.6-13.1-7.1-17.7-13c-5-6.1-7.4-14.3-7.4-24.9c-0.1-8.2,2.5-16.1,7.4-22.6
|
||||
c4.9-6.6,11.7-11.7,20.3-15.4c9-3.8,18.6-5.7,28.4-5.6c16.2,0,29.5,4.1,39.9,12.3c10.4,8.2,15.6,19.6,15.6,34h-28.5
|
||||
c0-5.8-2.7-10.4-8-14.1c-5.4-3.6-11.9-5.4-19.6-5.4c-7.5,0-13.6,1.8-18.5,5.3s-7.3,8-7.3,13.6s2.6,9.9,7.9,12.5
|
||||
c5.3,2.6,13.6,5.2,24.9,7.9c9.4,2,18.6,4.6,27.6,7.7c7.3,2.7,13.7,7.3,18.7,13.4c5.2,6.3,7.8,14.9,7.8,25.8c0,15-5.5,26.5-16.5,34.4
|
||||
s-25.3,11.9-42.9,11.9C213.4,199.8,203.4,197.8,194,193.9L194,193.9z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 590 B |
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 44" fill="#f7931e"><path d="M40 36H0v4a4 4 0 0 0 4 4h32a4 4 0 0 0 4-4v-4zM30 16.25L20 .5 10.16 16 0 0v32h20.32H40V0L30 16.25z"/></svg>
|
||||
|
After Width: | Height: | Size: 190 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 176 176"><path d="M160,80h-64V16c0-8.84-7.16-16-16-16H16C7.16,0,0,7.16,0,16v64c0,8.84,7.16,16,16,16h64v64c0,8.84,7.16,16,16,16h64c8.84,0,16-7.16,16-16v-64c0-8.84-7.16-16-16-16ZM16,80V16h64v64H16ZM160,160h-64v-64h64v64Z" style="fill:#f0f0f0; stroke-width:0px;"/><path d="M51.3,136.49l18.69-18.69c3.12-3.12,3.12-8.19,0-11.31-3.12-3.12-8.19-3.12-11.31,0l-18.69,18.69-18.33-18.33c-3.12-3.12-8.19-3.12-11.31,0-3.12,3.12-3.12,8.19,0,11.31l18.33,18.33-18,18c-3.12,3.12-3.12,8.19,0,11.31,1.56,1.56,3.61,2.34,5.66,2.34s4.09-.78,5.66-2.34l18-18,18.36,18.36c1.56,1.56,3.61,2.34,5.66,2.34s4.09-.78,5.66-2.34c3.12-3.12,3.12-8.19,0-11.31l-18.36-18.36Z" style="fill:#f0f0f0; stroke-width:0px;"/><path d="M147.47,40.3l18.69-18.69c3.12-3.12,3.12-8.19,0-11.31-3.12-3.12-8.19-3.12-11.31,0l-18.69,18.69-18.33-18.33c-3.12-3.12-8.19-3.12-11.31,0-3.12,3.12-3.12,8.19,0,11.31l18.33,18.33-18,18c-3.12,3.12-3.12,8.19,0,11.31,1.56,1.56,3.61,2.34,5.66,2.34s4.09-.78,5.66-2.34l18-18,18.36,18.36c1.56,1.56,3.61,2.34,5.66,2.34s4.09-.78,5.66-2.34c3.12-3.12,3.12-8.19,0-11.31l-18.36-18.36Z" style="fill:#f0f0f0; stroke-width:0px;"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 99.11 94.5" fill="#f7931e"><path d="M99.01,35.85c-.24-.72-.86-1.25-1.62-1.36l-31.83-4.57L51.29,1.11c-.34-.68-1.03-1.11-1.79-1.11h0c-.76,0-1.46.43-1.79,1.12l-14.18,28.85L1.71,34.65c-.75.11-1.38.64-1.61,1.36-.23.72-.04,1.52.51,2.05l23.06,22.41-5.38,31.7c-.13.75.18,1.51.8,1.96.35.25.76.38,1.17.38.32,0,.64-.08.93-.23l28.43-15.01,28.48,14.92c.67.35,1.49.29,2.11-.16.62-.45.92-1.21.79-1.96l-5.49-31.68,22.99-22.48c.54-.53.74-1.33.5-2.05Z"/></svg>
|
||||
|
After Width: | Height: | Size: 495 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 219 32.12"><style>path {fill:#a1a8ff;}</style><defs><linearGradient id="b" gradientUnits="userSpaceOnUse"><stop offset=".75" stop-color="#a1a8ff" /><stop offset="1" stop-color="#a1a8ff" stop-opacity="0" /></linearGradient><linearGradient id="c" gradientTransform="translate(78 32) rotate(-180)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a1a8ff" stop-opacity="0" /><stop offset=".25" stop-color="#a1a8ff" /></linearGradient></defs><path d="M91.99,32.12c3.91,0,7.15-2.8,7.85-6.5h10.05c1.47,0,2.67-1.34,2.67-3,0-1.66-1.19-3-2.67-3h-9.9v-7.5h9.9c1.47,0,2.67-1.34,2.67-3s-1.19-3-2.67-3h-10.17c-.89-3.45-4.01-6-7.74-6-7.26,0-13.29,5.21-14.59,12.09-.27-.06-.27,7.88,0,7.82,1.3,6.88,7.33,12.09,14.59,12.09Z" /><path d="M141.44,12.09c-1.3-6.88-7.33-12.09-14.59-12.09-3.91,0-7.15,2.8-7.85,6.5l.12,19.5c.89,3.45,4.01,6,7.74,6,7.26,0,13.29-5.21,14.59-12.09.27.06.27-7.88,0-7.82Z"/><rect x="141" y="12" width="78" height="8" style="fill:url(#b);" /><rect y="12" width="78" height="8" transform="translate(78 32) rotate(180)" style="fill:url(#c);" /></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,28 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60">
|
||||
<!-- why is it called a throbber? -->
|
||||
<style>
|
||||
rect {fill: #333550; animation: a 800ms infinite}
|
||||
rect:nth-child(1) {animation-delay: 0ms}
|
||||
rect:nth-child(2) {animation-delay: 100ms}
|
||||
rect:nth-child(3) {animation-delay: 200ms}
|
||||
rect:nth-child(4) {animation-delay: 300ms}
|
||||
rect:nth-child(5) {animation-delay: 400ms}
|
||||
rect:nth-child(6) {animation-delay: 500ms}
|
||||
rect:nth-child(7) {animation-delay: 600ms}
|
||||
rect:nth-child(8) {animation-delay: 700ms}
|
||||
@keyframes a {
|
||||
0%, 100% {fill: #333550}
|
||||
50% {fill: #a1a8ff}
|
||||
}
|
||||
</style>
|
||||
<g>
|
||||
<rect x="1" y="1" width="18" height="18"/>
|
||||
<rect x="21" y="1" width="18" height="18"/>
|
||||
<rect x="41" y="1" width="18" height="18"/>
|
||||
<rect x="41" y="21" width="18" height="18"/>
|
||||
<rect x="41" y="41" width="18" height="18"/>
|
||||
<rect x="21" y="41" width="18" height="18"/>
|
||||
<rect x="1" y="41" width="18" height="18"/>
|
||||
<rect x="1" y="21" width="18" height="18"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 85"><circle cx="24" cy="28" r="8" style="fill:#a1a8ff; stroke-width:0px;"/><circle cx="56" cy="28" r="8" style="fill:#a1a8ff; stroke-width:0px;"/><path d="M54,0h-28C11.66,0,0,11.66,0,26v47c0,6.62,5.38,12,12,12h56c6.62,0,12-5.38,12-12V26c0-14.34-11.66-26-26-26ZM72,73c0,2.21-1.79,4-4,4H12c-2.21,0-4-1.79-4-4v-17h64v17ZM72,48H8v-22c0-9.94,8.06-18,18-18h28c9.94,0,18,8.06,18,18v22Z" style="fill:#a1a8ff; stroke-width:0px;"/><path d="M24,28s-.01,0-.01,0h.01Z" style="fill:#000; stroke-width:0px;"/></svg>
|
||||
|
After Width: | Height: | Size: 601 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 85"><path d="M56,0H24C10.75,0,0,10.75,0,24v32h80V24c0-13.25-10.75-24-24-24ZM24,36c-4.42,0-8-3.58-8-8s3.58-8,8-8,8,3.58,8,8-3.58,8-8,8ZM56,36c-4.42,0-8-3.58-8-8s3.58-8,8-8,8,3.58,8,8-3.58,8-8,8Z" style="fill:#f0f0f0; stroke-width:0px;"/><path d="M24,28s-.01,0-.01,0h.01Z" style="fill:#f0f0f0; stroke-width:0px;"/><path d="M0,61h80v8c0,8.83-7.17,16-16,16H16c-8.83,0-16-7.17-16-16v-8h0Z" style="fill:#f0f0f0; stroke-width:0px;"/></svg>
|
||||
|
After Width: | Height: | Size: 533 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 77.12"><path d="M92 0H8a8 8 0 0 0-8 8v40.25a8 8 0 0 0 8 8h37v10.88H34c-2.76 0-5 2.24-5 5s2.24 5 5 5h32c2.76 0 5-2.24 5-5s-2.24-5-5-5H55V56.25h37a8 8 0 0 0 8-8V8a8 8 0 0 0-8-8zm0 44.12a4 4 0 0 1-4 4H12a4 4 0 0 1-4-4v-32a4 4 0 0 1 4-4h76a4 4 0 0 1 4 4v32z" fill="#f0f0f0"/></svg>
|
||||
|
After Width: | Height: | Size: 334 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20.97 20.97"><path d="M13.31 10.49l7.07-7.07c.78-.78.78-2.05 0-2.83s-2.05-.78-2.83 0l-7.07 7.07L3.41.59C2.63-.2 1.37-.2.59.59c-.79.78-.79 2.04 0 2.82l7.07 7.07-7.07 7.08c-.78.78-.78 2.05 0 2.83.39.39.9.59 1.41.59s1.02-.2 1.41-.59l7.07-7.07 7.07 7.07c.39.39.9.59 1.41.59s1.02-.2 1.41-.59c.78-.78.78-2.05 0-2.83l-7.07-7.07z" fill="#a1a8ff"/></svg>
|
||||
|
After Width: | Height: | Size: 398 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20.97 20.97"><path d="M13.31 10.49l7.07-7.07c.78-.78.78-2.05 0-2.83s-2.05-.78-2.83 0l-7.07 7.07L3.41.59C2.63-.2 1.37-.2.59.59c-.79.78-.79 2.04 0 2.82l7.07 7.07-7.07 7.08c-.78.78-.78 2.05 0 2.83.39.39.9.59 1.41.59s1.02-.2 1.41-.59l7.07-7.07 7.07 7.07c.39.39.9.59 1.41.59s1.02-.2 1.41-.59c.78-.78.78-2.05 0-2.83l-7.07-7.07z" fill="#f0f0f0"/></svg>
|
||||
|
After Width: | Height: | Size: 398 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 77.12"><path d="M92 0H8a8 8 0 0 0-8 8v40.25a8 8 0 0 0 8 8h37v10.88H34c-2.76 0-5 2.24-5 5s2.24 5 5 5h32c2.76 0 5-2.24 5-5s-2.24-5-5-5H55V56.25h37a8 8 0 0 0 8-8V8a8 8 0 0 0-8-8zm0 44.12a4 4 0 0 1-4 4H12a4 4 0 0 1-4-4v-32a4 4 0 0 1 4-4h76a4 4 0 0 1 4 4v32z" fill="#a1a8ff"/></svg>
|
||||
|
After Width: | Height: | Size: 334 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 70"><rect y="54" width="64" height="16" rx="4" ry="4" style="fill:#f0f0f0; stroke-width:0px;"/><path d="M46.77,25h-7.77V4c0-2.21-1.79-4-4-4h-8c-2.21,0-4,1.79-4,4v21h-7.77c-3.64,0-5.39,4.47-2.71,6.94l15.77,14.56c1.53,1.41,3.89,1.41,5.43,0l15.77-14.56c2.68-2.47.93-6.94-2.71-6.94Z" style="fill:#f0f0f0; stroke-width:0px;"/></svg>
|
||||
|
After Width: | Height: | Size: 428 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 57.5 48" fill="#f0f0f0"><path d="M53.5 20H38v-9.5c-1.02 0-2.05.25-2.83.75L18.2 22.17c-1.56 1.01-1.56 2.64 0 3.64l16.97 10.92c.78.5 1.8.75 2.83.75v-9.5h15.5a4 4 0 1 0 0-8zM8 47.5C3.86 47.5.5 44.14.5 40V8C.5 3.86 3.86.5 8 .5h23.5v7H16A8.51 8.51 0 0 0 7.5 16v16a8.51 8.51 0 0 0 8.5 8.5h15.5v7H8z"/><path d="M31 1v6H16c-4.96 0-9 4.04-9 9v16c0 4.96 4.04 9 9 9h15v6H8a7.01 7.01 0 0 1-7-7V8a7.01 7.01 0 0 1 7-7h23m1-1H8a8 8 0 0 0-8 8v32a8 8 0 0 0 8 8h24v-8H16a8 8 0 0 1-8-8V16a8 8 0 0 1 8-8h16V0h0z"/></svg>
|
||||
|
After Width: | Height: | Size: 553 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 92 72"><path d="M92,6c0-3.31-2.68-6-6-6H6C3.57,0,1.39,1.46.46,3.7-.47,5.95.04,8.53,1.76,10.24l29.74,29.74v20.01c0,6.62,4.49,12,10,12h10c5.51,0,10-5.38,10-12v-21.01l28.67-28.67c1.13-1.09,1.83-2.62,1.83-4.31Z" style="fill:#a1a8ff; stroke-width:0px;"/></svg>
|
||||
|
After Width: | Height: | Size: 353 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 49"><path d="M13,15V5h14v10h6V3c0-1.66-1.34-3-3-3H10c-1.66,0-3,1.34-3,3v12h6Z" style="fill:#f0f0f0; stroke-width:0px;"/><path d="M36,17H4c-2.21,0-4,1.79-4,4v24c0,2.21,1.79,4,4,4h32c2.21,0,4-1.79,4-4v-24c0-2.21-1.79-4-4-4ZM23,35.19v3.81c0,1.66-1.34,3-3,3s-3-1.34-3-3v-3.81c-1.79-1.04-3-2.97-3-5.19,0-3.31,2.69-6,6-6s6,2.69,6,6c0,2.22-1.21,4.15-3,5.19Z" style="fill:#f0f0f0; stroke-width:0px;"/></svg>
|
||||
|
After Width: | Height: | Size: 500 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 60"><path d="M32,0H8C3.58,0,0,3.58,0,8v44c0,4.42,3.58,8,8,8h24c4.42,0,8-3.58,8-8V8c0-4.42-3.58-8-8-8ZM20,57c-2.76,0-5-2.24-5-5s2.24-5,5-5,5,2.24,5,5-2.24,5-5,5ZM32,44H8V8h24v36Z" style="fill:#f0f0f0; stroke-width:0px;"/></svg>
|
||||
|
After Width: | Height: | Size: 327 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect x="44" width="20" height="64" rx="4" ry="4" style="fill:#f0f0f0; stroke-width:0px;"/><rect width="20" height="64" rx="4" ry="4" style="fill:#f0f0f0; stroke-width:0px;"/></svg>
|
||||
|
After Width: | Height: | Size: 286 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 71 80.76"><path d="M69,36.91L6,.54C3.33-1,0,.93,0,4.01v72.75c0,3.08,3.33,5,6,3.46l63-36.37c2.67-1.54,2.67-5.39,0-6.93Z" style="fill:#f0f0f0; stroke-width:0px;"/></svg>
|
||||
|
After Width: | Height: | Size: 265 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 71.04 71.4" fill="#a1a8ff"><path d="M34.72 59.32c2.76-2.76 3.08-7.04.96-10.15l7.11-7.11c1.04-1.04.94-2.84-.24-4.01s-2.97-1.28-4.01-.24l-7 7-5.3-5.3 7-7c1.04-1.04.94-2.84-.24-4.01s-2.97-1.28-4.01-.24l-7.19 7.19c-3.07-1.81-7.08-1.41-9.71 1.23-5.13 5.13-5.72 13.08-1.77 18.87-.23.15-.45.32-.66.53l-8.43 8.43C0 65.74-.4 67.64.41 69.17c1.34 2.56 4.57 2.91 6.42 1.05l8.49-8.49c.2-.2.38-.43.53-.66 5.78 3.95 13.73 3.37 18.87-1.77zM69.87 6.83c1.56-1.56 1.56-4.09 0-5.66-1.56-1.56-4.09-1.56-5.66 0l-8.49 8.49c-.2.2-.38.43-.53.66-5.78-3.95-13.73-3.37-18.87 1.77-2.76 2.76-3.08 7.04-.96 10.15l13.87 13.71c3.07 1.81 7.08 1.41 9.71-1.23 5.13-5.13 5.72-13.08 1.77-18.87.23-.15.45-.32.66-.53l8.49-8.49z"/></svg>
|
||||
|
After Width: | Height: | Size: 749 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 52"><path d="M32 20.57a7.99 7.99 0 0 0-6.5-7.85V2.67c0-1.47-1.34-2.67-3-2.67s-3 1.19-3 2.67v9.9H12v-9.9C12 1.2 10.66 0 9 0S6 1.19 6 2.67v10.17c-3.45.89-6 4.01-6 7.74 0 7.26 5.21 13.29 12.09 14.59a3.86 3.86 0 0 0-.09.84v12a4 4 0 1 0 8 0v-12a3.86 3.86 0 0 0-.09-.84C26.79 33.87 32 27.84 32 20.58z" fill="#f0f0f0"/></svg>
|
||||
|
After Width: | Height: | Size: 374 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 99.11 94.5" fill="#f0f0f0"><path d="M99.01,35.85c-.24-.72-.86-1.25-1.62-1.36l-31.83-4.57L51.29,1.11c-.34-.68-1.03-1.11-1.79-1.11h0c-.76,0-1.46.43-1.79,1.12l-14.18,28.85L1.71,34.65c-.75.11-1.38.64-1.61,1.36-.23.72-.04,1.52.51,2.05l23.06,22.41-5.38,31.7c-.13.75.18,1.51.8,1.96.35.25.76.38,1.17.38.32,0,.64-.08.93-.23l28.43-15.01,28.48,14.92c.67.35,1.49.29,2.11-.16.62-.45.92-1.21.79-1.96l-5.49-31.68,22.99-22.48c.54-.53.74-1.33.5-2.05Z"/></svg>
|
||||
|
After Width: | Height: | Size: 495 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 54"><path d="M49,54H1c-.55,0-1-.45-1-1,0-12.12,5.01-23.24,13.08-29.02.42-.3,1-.23,1.34.16,2.67,3.09,6.53,4.86,10.58,4.86s7.91-1.77,10.58-4.86c.34-.39.92-.46,1.34-.16,8.07,5.78,13.08,16.9,13.08,29.02,0,.55-.45,1-1,1Z" style="fill:#f0f0f0; stroke-width:0px;"/><path d="M25,26c-7.17,0-13-5.83-13-13S17.83,0,25,0s13,5.83,13,13-5.83,13-13,13Z" style="fill:#f0f0f0; stroke-width:0px;"/></svg>
|
||||
|
After Width: | Height: | Size: 487 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 112.28 92"><path d="M66.78,33.24v25c8.84,0,16-5.6,16-12.5s-7.16-12.5-16-12.5h0Z" style="fill:#f0f0f0; stroke-width:0px;"/><path d="M57.99,92c3.31,0,6-2.68,6-6V6c0-2.43-1.46-4.61-3.7-5.54-2.25-.93-4.83-.42-6.54,1.3l-29.74,29.74h-12.01c-6.62,0-12,4.49-12,10v10c0,5.51,5.38,10,12,10h13.01l28.67,28.67c1.09,1.13,2.62,1.83,4.31,1.83Z" style="fill:#f0f0f0; stroke-width:0px;"/><path d="M70.78,71.97c-2.21,0-4-1.69-4-3.76s1.79-3.76,4-3.76c11.58,0,21-8.38,21-18.69s-9.42-18.69-21-18.69c-2.21,0-4-1.69-4-3.76s1.79-3.76,4-3.76c15.99,0,29,11.76,29,26.22s-13.01,26.22-29,26.22Z" style="fill:#f0f0f0; stroke-width:0px;"/><path d="M70.78,86.24c-2.21,0-4-1.69-4-3.76s1.79-3.76,4-3.76c18.47,0,33.5-14.57,33.5-32.47s-15.03-32.47-33.5-32.47c-2.21,0-4-1.69-4-3.76s1.79-3.76,4-3.76c22.88,0,41.5,17.94,41.5,40s-18.62,40-41.5,40Z" style="fill:#f0f0f0; stroke-width:0px;"/></svg>
|
||||
|
After Width: | Height: | Size: 954 B |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 485 B |
|
After Width: | Height: | Size: 413 B |
|
After Width: | Height: | Size: 583 B |
|
After Width: | Height: | Size: 576 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 539 B |
|
After Width: | Height: | Size: 499 B |
|
After Width: | Height: | Size: 351 B |
|
After Width: | Height: | Size: 521 B |
|
After Width: | Height: | Size: 480 B |
|
After Width: | Height: | Size: 581 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 1.4 KiB |