Connected
-Signed in
- --
-
- User ID -
- — +
- Auth -
- — +
- API -
- — -
- Talk to a site of your choice with a user login or an API token -
- Keep the token in the OS keychain, not in the renderer -
- Load plugin-backed tools as the HTTP API grows -
-
+
+
${item.createdAt}
+ ${item.html} + + `
+ )
+ .join('')
+ )
+
+ fillDropdown(
+ document.getElementById('message-menu'),
+ demoMessages
+ .map(
+ (item) => `
+ -
+
+ ${item.otherUser}
+
${item.lastMessageAt}
+ ${item.preview} + + `
+ )
+ .join('')
+ )
}
/**
- * Run an auth IPC call and render the result or an error.
+ * Replace demo rows at the top of a header dropdown.
*
- * @param {HTMLButtonElement} button - submit button to disable
+ * @param {HTMLElement} list - dropdown ul
+ * @param {string} html - li markup
+ * @return {void}
+ */
+function fillDropdown(list, html) {
+ list.querySelectorAll('[data-demo-item]').forEach((node) => node.remove())
+ list.insertAdjacentHTML('afterbegin', html)
+}
+
+/**
+ * Fill the in-app pages from demo (and later API) data.
+ *
+ * @return {void}
+ */
+function renderPages() {
+ if (!session?.connected) {
+ return
+ }
+
+ document.getElementById('home-username').textContent = session.username || 'this account'
+ const siteLink = document.getElementById('home-site')
+ siteLink.textContent = session.siteUrl
+ siteLink.href = session.siteUrl
+
+ document.getElementById('notification-list').innerHTML = demoNotifications
+ .map(
+ (item) => `
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Search
+ +Results will come from the site search API next.
+
+
+
+
+
+
+
+ Notifications
+
+
+
+
+
+
+
+
+
+ Messages
+ +| With | +Last message | +Updated | +
|---|
+
+
+
+
+
+
diff --git a/src/renderer/src/demo.js b/src/renderer/src/demo.js
new file mode 100644
index 0000000..bacb71e
--- /dev/null
+++ b/src/renderer/src/demo.js
@@ -0,0 +1,96 @@
+/**
+ * Placeholder lists until the TTP API grows. Shapes match the live plugins.
+ */
+
+export const previewSession = {
+ connected: true,
+ preview: true,
+ siteUrl: 'https://ttp.joeykimsey.com',
+ username: 'Joey',
+ userId: 1,
+ authMethod: 'password',
+ apiReady: false,
+ lastSiteUrl: 'https://ttp.joeykimsey.com'
+}
+
+export const demoNotifications = [
+ {
+ id: 'n1',
+ unread: true,
+ createdAt: '2 hours ago',
+ html: 'Welcome to Capsule. Notifications will load from the site API next.'
+ },
+ {
+ id: 'n2',
+ unread: false,
+ createdAt: 'Yesterday',
+ html: 'Your profile preferences can be edited here. Email, password, and phone stay on the site.'
+ }
+]
+
+export const demoMessages = [
+ {
+ id: 'm1',
+ unread: true,
+ otherUser: 'Alex',
+ preview: 'Did the desktop shell pick up the new header?',
+ lastMessageAt: 'Today'
+ },
+ {
+ id: 'm2',
+ unread: false,
+ otherUser: 'Sam',
+ preview: 'Search should stay in the top middle — full width.',
+ lastMessageAt: 'Monday'
+ }
+]
+
+export const demoProfile = {
+ username: 'Joey',
+ usernamePretty: 'Joey',
+ gender: 'unspecified',
+ newsletter: true,
+ timezone: 'America/New_York',
+ dateFormat: 'F-j-Y',
+ timeFormat: 'g:i:s A',
+ pageLimit: '10',
+ darkMode: false,
+ registered: 'September 2024',
+ lastLogin: 'Just now'
+}
+
+export const dateFormatOptions = [
+ { label: 'January 8, 1991', value: 'F-j-Y' },
+ { label: '8 January, 1991', value: 'j-F-Y' },
+ { label: 'Jan 8, 1991', value: 'M-j-Y' },
+ { label: '1-8-1991', value: 'n-j-Y' },
+ { label: '01-08-1991', value: 'm-d-Y' },
+ { label: '8-1-1991', value: 'j-n-Y' },
+ { label: '08-01-1991', value: 'd-m-Y' }
+]
+
+export const timeFormatOptions = [
+ { label: '3:33:33 AM', value: 'g:i:s A' },
+ { label: '03:33:33 AM', value: 'h:i:s A' },
+ { label: '3:33:33 (military)', value: 'G:i:s' },
+ { label: '03:33:33 (military)', value: 'H:i:s' }
+]
+
+export const timezoneOptions = [
+ 'America/New_York',
+ 'America/Chicago',
+ 'America/Denver',
+ 'America/Los_Angeles',
+ 'America/Phoenix',
+ 'UTC',
+ 'Europe/London',
+ 'Europe/Paris'
+]
+
+export const pageLimitOptions = ['10', '15', '20', '25', '50']
+
+export const searchResources = [
+ { value: 'all', label: 'All' },
+ { value: 'posts', label: 'Posts' },
+ { value: 'pages', label: 'Pages' }
+]
diff --git a/src/renderer/src/main.js b/src/renderer/src/main.js
index 3b59b9f..a41cc3d 100644
--- a/src/renderer/src/main.js
+++ b/src/renderer/src/main.js
@@ -1,10 +1,37 @@
/**
- * Login and home views. The token never enters this process.
+ * Login, chrome, and in-app views. API lists are placeholders until the next step.
*/
-const loginView = document.getElementById('view-login')
-const homeView = document.getElementById('view-home')
-const topStatus = document.getElementById('top-status')
+import {
+ dateFormatOptions,
+ demoMessages,
+ demoNotifications,
+ demoProfile,
+ pageLimitOptions,
+ previewSession,
+ timeFormatOptions,
+ timezoneOptions
+} from './demo.js'
+
+const views = {
+ login: document.getElementById('view-login'),
+ home: document.getElementById('view-home'),
+ stub: document.getElementById('view-stub'),
+ search: document.getElementById('view-search'),
+ notifications: document.getElementById('view-notifications'),
+ messages: document.getElementById('view-messages'),
+ profile: document.getElementById('view-profile'),
+ settings: document.getElementById('view-settings')
+}
+
+const mainPages = {
+ game: 'Game',
+ friends: 'Friends',
+ cabal: 'Cabal',
+ hiscores: 'HiScores',
+ shop: 'Shop'
+}
+
const loginForm = document.getElementById('login-form')
const loginError = document.getElementById('login-error')
const loginSubmit = document.getElementById('login-submit')
@@ -15,17 +42,13 @@ const tokenError = document.getElementById('token-error')
const tokenSubmit = document.getElementById('token-submit')
const tokenSite = document.getElementById('token-site')
const tokenUsername = document.getElementById('token-username')
-const homeUsername = document.getElementById('home-username')
-const homeSite = document.getElementById('home-site')
-const homeUserId = document.getElementById('home-userid')
-const homeMethod = document.getElementById('home-method')
-const homeApi = document.getElementById('home-api')
-const homeNote = document.getElementById('home-note')
-const openSite = document.getElementById('open-site')
-const logoutButton = document.getElementById('logout-button')
+const previewButton = document.getElementById('preview-shell')
+
+let session = null
+let profile = { ...demoProfile }
/**
- * Show or hide a status/error line.
+ * Show or hide a status line.
*
* @param {HTMLElement} el - message node
* @param {string} [message] - text to show; empty hides the node
@@ -38,61 +61,327 @@ function setMessage(el, message) {
}
/**
- * Fill both site URL fields from the last used host.
+ * Site path that opens in the system browser.
*
- * @param {string} siteUrl - canonical site base
- * @return {void}
+ * @param {string} path - path after the site root
+ * @return {string} - absolute URL, or #
*/
-function fillSiteFields(siteUrl) {
- if (!siteUrl) {
- return
+function siteHref(path) {
+ if (!session?.siteUrl) {
+ return '#'
}
- loginSite.value = siteUrl
- tokenSite.value = siteUrl
+ return `${session.siteUrl.replace(/\/+$/, '')}/${String(path).replace(/^\/+/, '')}`
}
/**
- * Render the login or home view from a public session.
+ * Avatar URL for the connected site, or empty when none.
*
- * @param {object} session - connection state from main
+ * @return {string} - image URL
+ */
+function avatarUrl() {
+ if (!session?.siteUrl) {
+ return ''
+ }
+ return `${session.siteUrl.replace(/\/+$/, '')}/images/defaultAvatar.png`
+}
+
+/**
+ * Logo on navy chrome.
+ *
+ * @return {string} - image URL
+ */
+function logoUrl() {
+ if (!session?.siteUrl) {
+ return ''
+ }
+ return `${session.siteUrl.replace(/\/+$/, '')}/images/logoWhite.png`
+}
+
+/**
+ * Fill a select from label/value pairs or plain strings.
+ *
+ * @param {HTMLSelectElement} select - target
+ * @param {Array} options - choices
+ * @param {string} selected - current value
* @return {void}
*/
-function render(session) {
+function fillSelect(select, options, selected) {
+ select.innerHTML = ''
+ options.forEach((option) => {
+ const value = typeof option === 'string' ? option : option.value
+ const label = typeof option === 'string' ? option : option.label
+ const node = document.createElement('option')
+ node.value = value
+ node.textContent = label
+ if (value === selected) {
+ node.selected = true
+ }
+ select.appendChild(node)
+ })
+}
+
+/**
+ * Paint header identity, badges, and site links.
+ *
+ * @return {void}
+ */
+function renderChrome() {
const connected = Boolean(session?.connected)
- loginView.hidden = connected
- homeView.hidden = !connected
- fillSiteFields(session?.lastSiteUrl || session?.siteUrl || '')
+ document.body.dataset.shell = connected ? 'app' : 'login'
+ document.getElementById('header-username').textContent = session?.username || 'Account'
- if (!connected) {
- topStatus.hidden = true
- return
+ const logo = document.getElementById('header-logo')
+ const avatar = document.getElementById('header-avatar')
+ const avatarFallback = document.getElementById('header-avatar-fallback')
+ const nextLogo = logoUrl()
+ const nextAvatar = avatarUrl()
+
+ logo.onerror = () => {
+ logo.hidden = true
+ }
+ avatar.onerror = () => {
+ avatar.hidden = true
+ avatarFallback.hidden = false
}
- const username = session.username || 'API token'
- homeUsername.textContent = session.username ? username : 'Signed in with token'
- homeSite.textContent = session.siteUrl
- homeUserId.textContent = session.userId == null ? '—' : String(session.userId)
- homeMethod.textContent = session.authMethod === 'token' ? 'API token' : 'Password'
- homeApi.textContent = session.apiReady ? 'Ready' : 'Limited'
- openSite.href = session.siteUrl
- topStatus.hidden = false
- topStatus.classList.add('is-ok')
- topStatus.textContent = 'Connected'
-
- if (session.apiReady) {
- setMessage(homeNote, '')
- } else {
- setMessage(
- homeNote,
- 'Signed in, but api/users/find did not confirm this user. Turn on personal API access on the site, or add a username when connecting with a token.'
- )
+ logo.hidden = !nextLogo
+ if (nextLogo) {
+ logo.src = nextLogo
}
+
+ avatar.hidden = !nextAvatar
+ avatarFallback.hidden = Boolean(nextAvatar)
+ if (nextAvatar) {
+ avatar.src = nextAvatar
+ document.getElementById('profile-avatar').src = nextAvatar
+ document.getElementById('settings-avatar').src = nextAvatar
+ }
+
+ const email = siteHref('usercp/email')
+ const password = siteHref('usercp/password')
+ const phone = siteHref('usercp/phone')
+ document.getElementById('link-email').href = email
+ document.getElementById('link-password').href = password
+ document.getElementById('link-phone').href = phone
+ document.getElementById('settings-link-email').href = email
+ document.getElementById('settings-link-password').href = password
+ document.getElementById('settings-link-phone').href = phone
+
+ const unreadNotes = demoNotifications.filter((item) => item.unread).length
+ const unreadMail = demoMessages.filter((item) => item.unread).length
+ const noteBadge = document.getElementById('notification-badge')
+ const mailBadge = document.getElementById('message-badge')
+ noteBadge.hidden = unreadNotes === 0
+ mailBadge.hidden = unreadMail === 0
+ noteBadge.textContent = String(unreadNotes)
+ mailBadge.textContent = String(unreadMail)
+
+ fillDropdown(
+ document.getElementById('notification-menu'),
+ demoNotifications
+ .map(
+ (item) => `
+
+ ${item.html}
+ ${item.createdAt}
+
+
+
+ `
+ )
+ .join('')
+
+ document.getElementById('message-list').innerHTML = demoMessages
+ .map(
+ (item) => `
+
+ ${item.otherUser}
+ ${item.preview}
+ ${item.lastMessageAt}
+ `
+ )
+ .join('')
+
+ profile.username = session.username || profile.username
+ profile.usernamePretty = profile.username
+ document.getElementById('profile-name').textContent = profile.usernamePretty
+ document.getElementById('profile-registered').textContent = profile.registered
+ document.getElementById('profile-last-login').textContent = profile.lastLogin
+ document.getElementById('profile-gender').textContent = profile.gender
+
+ document.getElementById('settings-gender').value = profile.gender
+ document.getElementById('settings-newsletter').checked = profile.newsletter
+ document.getElementById('settings-dark').checked = profile.darkMode
+ fillSelect(document.getElementById('settings-timezone'), timezoneOptions, profile.timezone)
+ fillSelect(document.getElementById('settings-date'), dateFormatOptions, profile.dateFormat)
+ fillSelect(document.getElementById('settings-time'), timeFormatOptions, profile.timeFormat)
+ fillSelect(document.getElementById('settings-limit'), pageLimitOptions, profile.pageLimit)
+ applyTheme(profile.darkMode)
+}
+
+/**
+ * Toggle TTP light/dark tokens on the document.
+ *
+ * @param {boolean} dark - dark-mode preference
+ * @return {void}
+ */
+function applyTheme(dark) {
+ document.documentElement.setAttribute('data-bs-theme', dark ? 'dark' : 'light')
+}
+
+/**
+ * Show one named view.
+ *
+ * @param {string} name - views key
+ * @return {void}
+ */
+function showView(name) {
+ Object.entries(views).forEach(([key, node]) => {
+ node.hidden = key !== name
+ })
+}
+
+/**
+ * Mark the matching main-nav link, or none.
+ *
+ * @param {string} path - hash path without query
+ * @return {void}
+ */
+function setMainNav(path) {
+ document.querySelectorAll('.capsule-mainnav [data-nav]').forEach((link) => {
+ link.classList.toggle('active', link.dataset.nav === path)
+ })
+}
+
+/**
+ * Route from the hash. Login when there is no session.
+ *
+ * @return {void}
+ */
+function route() {
+ if (!session?.connected) {
+ showView('login')
+ return
+ }
+
+ const hash = window.location.hash.replace(/^#\/?/, '')
+ const [path] = hash.split('?')
+ setMainNav(path)
+
+ if (mainPages[path]) {
+ document.getElementById('stub-title').textContent = mainPages[path]
+ showView('stub')
+ return
+ }
+ if (path === 'notifications') {
+ showView('notifications')
+ return
+ }
+ if (path === 'messages') {
+ showView('messages')
+ return
+ }
+ if (path === 'profile') {
+ showView('profile')
+ return
+ }
+ if (path === 'settings') {
+ showView('settings')
+ return
+ }
+ if (path === 'search') {
+ showView('search')
+ return
+ }
+ showView('home')
+}
+
+/**
+ * Apply a public session and go to the current route.
+ *
+ * @param {object} next - connection state
+ * @return {void}
+ */
+function applySession(next) {
+ session = next
+ if (!session?.connected) {
+ window.location.hash = ''
+ applyTheme(false)
+ } else if (!window.location.hash) {
+ window.location.hash = '#/'
+ }
+ if (loginSite && session?.lastSiteUrl) {
+ loginSite.value = session.lastSiteUrl
+ tokenSite.value = session.lastSiteUrl
+ }
+ renderChrome()
+ renderPages()
+ route()
+}
+
+/**
+ * Run an auth IPC call.
+ *
+ * @param {HTMLButtonElement} button - submit button
* @param {HTMLElement} errorEl - error line
* @param {() => Promise
Profile
++
+
+
+
+
+
+
+
-
-
-
+
-
-
-
+
+
-
+
+
+
+
+
+
+
+
+ Edit profile
+
+
+ | Registered: | ++ |
|---|---|
| Last Seen: | ++ |
| Gender: | ++ |
-
-
-
-
-
-
- Open site
-
-
-
- Workspace
-Desktop tools land here
-- This area will host the full desktop experience for the plugins enabled on - the connected site. The current API can sign you in, refresh a token, and - look up a user id. Feature screens wait on those API expansions. -
--
-
+
+
+ Settings
++
+
+
+
+
+
+
+
+ Email, password, and phone stay on the site.
+
+
+