Initial Release

This commit is contained in:
2026-05-24 21:58:23 -07:00
commit 0a782e0096
209 changed files with 11657 additions and 0 deletions
@@ -0,0 +1,47 @@
// Widget: Latest Articles
(() => {
let articleIndex = 0
let autoScrollEnabled = true
const elemControls = document.querySelector<HTMLDivElement>('div.widget-latest-controls')!
const elemArticles = document.querySelector<HTMLDivElement>('div.widget-latest-articles')!
const listArticles = elemArticles.querySelectorAll<HTMLDivElement>('div.latest-item')
const listDots = elemControls.querySelectorAll<HTMLDivElement>('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<HTMLButtonElement>('button#latest-next')!
.addEventListener('click', () => transitionNextArticle(true))
document
.querySelector<HTMLButtonElement>('button#latest-last')!
.addEventListener('click', () => transitionNextArticle(false))
})()
@@ -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<HTMLDivElement>('div.pane#loading')!
const paneError = document.querySelector<HTMLDivElement>('div.pane#error')!
const paneHelp = document.querySelector<HTMLDivElement>('div.pane#help')!
const paneEmpty = document.querySelector<HTMLDivElement>('div.pane#empty')!
const paneResults = document.querySelector<HTMLDivElement>('div.pane#results')!
const elementWrapper = document.querySelector<HTMLDivElement>('div.wrapper-search')!
const elementInput = document.querySelector<HTMLInputElement>('input.search-input')!
const elementClose = document.querySelector<HTMLButtonElement>('button.search-close')!
const elementOpen = document.querySelector<HTMLButtonElement>('button#search-open')!
const elementFooter = document.querySelector<HTMLParagraphElement>('div.search-footer > p')!
const elementError = paneError.querySelector<HTMLParagraphElement>('p')!
let searchTimings = 0
let searchElements = new Array<HTMLAnchorElement>()
let searchTimeout: ReturnType<typeof setTimeout>
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<string>()
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)
// }
})()
@@ -0,0 +1,120 @@
(async () => {
{
// Setup Grid Tiles
const audioSet = document.querySelector<HTMLAudioElement>('audio#set')!
const audioRelease = document.querySelector<HTMLAudioElement>('audio#release')!
document.querySelectorAll<HTMLButtonElement>('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<HTMLButtonElement>('a#done')!.addEventListener('click', async () => {
SetDynamic(false)
// Filter: Correct Answer?
let givenAnswer = 0
document.querySelectorAll<HTMLDivElement>('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)
})
})()
@@ -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))
@@ -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." },
}
}
])
@@ -0,0 +1,47 @@
CreateModal([
{
submitLabel: 'Submit',
submitLogic: (modal, values) => new Promise(ok => {
const messages: Record<string, string | undefined> = {}
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<any>(
'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.' },
}
}
])
@@ -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<string, string | undefined> = {}
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<HTMLInputElement>('#username')!.value = Date.now() + ''
document.querySelector<HTMLInputElement>('#email')!.value = Date.now() + '@gmail.com'
document.querySelector<HTMLInputElement>('#password')!.value = 'Googleman00!'
@@ -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')
)
})()
@@ -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')
)
})()
@@ -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 += '🥞'
}
})()
@@ -0,0 +1,41 @@
document
.querySelectorAll<HTMLDivElement>('div.widget-project')
.forEach(project => {
let currentImage = 0
const galleryDots = project.querySelectorAll<HTMLDivElement>('div.gallery-dot')!
const galleryItems = project.querySelectorAll<HTMLImageElement>('img.gallery-image')!
const galleryScroll = project.querySelector<HTMLDivElement>('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))
})
@@ -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<HTMLAudioElement>('audio')!
const elemProgress = audioElement.querySelector<HTMLInputElement>('input')!
const elemButton = audioElement.querySelector<HTMLButtonElement>('button')!
const elemButtonIcon = audioElement.querySelector<HTMLImageElement>('button img')!
const elemTime = audioElement.querySelector<HTMLParagraphElement>('p.audio-time')!
const elemDownload = audioElement.querySelector<HTMLAnchorElement>('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<HTMLAnchorElement>(scroll_search).forEach(anchor => {
const href = anchor.href.split('#').at(-1)
if (!href) return
const head = document.querySelector<HTMLParagraphElement>(`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)
})()
@@ -0,0 +1,98 @@
(() => {
// Have Footer always be just slightly out of view
{
const footerWrapper = document.querySelector<HTMLDivElement>('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<HTMLAnchorElement>('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<MinimalProfileResponse>('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<HTMLDivElement>('div#status-dot')!
const ELEMENT_TXT = document.querySelector<HTMLParagraphElement>('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" })
}
})
}
}
})()
@@ -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<BodyType> {
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<T>(
method: string,
path: string,
authorization: 'mfa' | 'basic' | 'public',
withBody?: Record<string, any>
): Promise<ApiResponse<T>> {
return new Promise(ok => {
// Send HTTP Request
let body: string | undefined
let result: ApiResponse<T>
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<T>(views: ModalView[]) {
return new Promise<T>((modalClose, modalCancel) => {
let currentView = 0
/** Helper: Create Element with Classes and Parent */
function _Create<T = HTMLElement>(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<HTMLAnchorElement>('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<HTMLLabelElement>('label', 'modal-label', elements)
const input = _Create<HTMLInputElement>('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<HTMLAnchorElement>('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<HTMLParagraphElement>('p', 'modal-alert', footer)
if (o.closeLabel) {
cancel = _Create<HTMLButtonElement>('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<HTMLButtonElement>('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<string, string> = {}
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<string, HTMLLabelElement> = {}
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<string, any> // Submit Results
messages?: Record<string, string | undefined> | string // Submit Error
}
interface ModalView {
submitLabel?: string
submitLogic?: (modal: ModalState, values: Record<string, string>) => Promise<ModalResults>
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;
}