import { computed, ref, inject } from "vue" import { useClient } from "@servicestack/vue" import { appendQueryString, combinePaths } from "@servicestack/client" export const SignInDialog = { template:/*html*/`

Sign In

Sign In
Or continue with
Sign Up →
`, emits:['done','signup'], setup(props, { emit }) { /** @type {Store} */ const store = inject('store') const client = useClient() /** @type {Ref} */ const request = ref(new Authenticate({ provider:'credentials' })) const signInHref = computed(() => appendQueryString(combinePaths(store.BaseUrl, 'auth/google'), { redirect:location.origin })) const errorSummary = computed(() => '') const oauthProviders = [ { name:'Facebook', href:'/auth/facebook', icon: { svg: "" } }, { name:'Google', href:'/auth/google', icon: { svg: "" } }, { name:'Microsoft', href:'/auth/microsoftgraph', icon: { svg: "" } }, ] function providerUrl(provider) { return appendQueryString(combinePaths(store.BaseUrl, provider.href), { 'continue': location.href }) } function providerLabel(provider) { return `Sign In with ${provider.name}` } function done() { emit('done') } async function submit() { const api = await client.api(request.value) if (api.succeeded) { await store.signIn(api.response) await store.loadUserData() done() } } return { request, signInHref, done, errorSummary, submit, oauthProviders, providerUrl, providerLabel, } } } export const SignUpDialog = { template:/*html*/`

Sign Up

Sign Up
← Sign In
`, emits:['done','signin'], setup(props, { emit }) { /** @type {Store} */ const store = inject('store') /** @type {Ref} */ const request = ref(new Register({ autoLogin:true })) const client = useClient() function done() { emit('done') } async function submit() { if (request.value.password !== request.value.confirmPassword) { client.setError({ fieldName:'confirmPassword', message:'Passwords do not match' }) return } if (request.value.password.length < 6) { client.setError({ fieldName:'password', message:'Minimum of 6 characters' }) return } const api = await client.api(request.value) if (api.succeeded) { await store.signIn(api.response) await store.loadUserData() done() } } return { request, submit, done } } } export class Authenticate { /** @param {{provider?:string,state?:string,oauth_token?:string,oauth_verifier?:string,userName?:string,password?:string,rememberMe?:boolean,errorView?:string,nonce?:string,uri?:string,response?:string,qop?:string,nc?:string,cnonce?:string,accessToken?:string,accessTokenSecret?:string,scope?:string,meta?:{ [index: string]: string; }}} [init] */ constructor(init) { Object.assign(this, init) } /** * @type {string} * @description AuthProvider, e.g. credentials */ provider; /** @type {string} */ state; /** @type {string} */ oauth_token; /** @type {string} */ oauth_verifier; /** @type {string} */ userName; /** @type {string} */ password; /** @type {?boolean} */ rememberMe; /** @type {string} */ errorView; /** @type {string} */ nonce; /** @type {string} */ uri; /** @type {string} */ response; /** @type {string} */ qop; /** @type {string} */ nc; /** @type {string} */ cnonce; /** @type {string} */ accessToken; /** @type {string} */ accessTokenSecret; /** @type {string} */ scope; /** @type {{ [index: string]: string; }} */ meta; getTypeName() { return 'Authenticate' } getMethod() { return 'POST' } createResponse() { return new AuthenticateResponse() } } export class AuthenticateResponse { /** @param {{userId?:string,sessionId?:string,userName?:string,displayName?:string,referrerUrl?:string,bearerToken?:string,refreshToken?:string,profileUrl?:string,roles?:string[],permissions?:string[],responseStatus?:ResponseStatus,meta?:{ [index: string]: string; }}} [init] */ constructor(init) { Object.assign(this, init) } /** @type {string} */ userId; /** @type {string} */ sessionId; /** @type {string} */ userName; /** @type {string} */ displayName; /** @type {string} */ referrerUrl; /** @type {string} */ bearerToken; /** @type {string} */ refreshToken; /** @type {string} */ profileUrl; /** @type {string[]} */ roles; /** @type {string[]} */ permissions; /** @type {ResponseStatus} */ responseStatus; /** @type {{ [index: string]: string; }} */ meta; } export class Register { /** @param {{userName?:string,firstName?:string,lastName?:string,displayName?:string,email?:string,password?:string,confirmPassword?:string,autoLogin?:boolean,errorView?:string,meta?:{ [index: string]: string; }}} [init] */ constructor(init) { Object.assign(this, init) } /** @type {string} */ userName; /** @type {string} */ firstName; /** @type {string} */ lastName; /** @type {string} */ displayName; /** @type {string} */ email; /** @type {string} */ password; /** @type {string} */ confirmPassword; /** @type {?boolean} */ autoLogin; /** @type {string} */ errorView; /** @type {{ [index: string]: string; }} */ meta; getTypeName() { return 'Register' } getMethod() { return 'POST' } createResponse() { return new RegisterResponse() } } export class RegisterResponse { /** @param {{userId?:string,sessionId?:string,userName?:string,referrerUrl?:string,bearerToken?:string,refreshToken?:string,roles?:string[],permissions?:string[],responseStatus?:ResponseStatus,meta?:{ [index: string]: string; }}} [init] */ constructor(init) { Object.assign(this, init) } /** @type {string} */ userId; /** @type {string} */ sessionId; /** @type {string} */ userName; /** @type {string} */ referrerUrl; /** @type {string} */ bearerToken; /** @type {string} */ refreshToken; /** @type {string[]} */ roles; /** @type {string[]} */ permissions; /** @type {ResponseStatus} */ responseStatus; /** @type {{ [index: string]: string; }} */ meta; } import { $$, JsonApiClient, leftPart } from "@servicestack/client" import { createApp, reactive } from "vue" import ServiceStackVue from "@servicestack/vue" export const BaseUrl = leftPart(import.meta.url, '/mjs') let AppData = { init: false, Auth: null, UserData: null, } let client = null, store = null, Apps = [] export { client, AppData } /** @param {any} [exports] */ export function init(exports) { if (AppData.init) return client = JsonApiClient.create(BaseUrl) AppData = reactive(AppData) AppData.init = true if (exports) { exports.client = client exports.AppData = AppData exports.Apps = Apps } } const alreadyMounted = el => el.__vue_app__ /** Mount Vue3 Component * @param sel {string|Element} - Element or Selector where component should be mounted * @param component * @param [props] {any} * @param {{ mount?:(app, { client, AppData }) => void }} options= */ export function mount(sel, component, props, options) { if (!AppData.init) { init(globalThis) } const els = $$(sel) els.forEach(el => { if (alreadyMounted(el)) return const elProps = el.getAttribute('data-props') const useProps = elProps ? { ...props, ...(new Function(`return (${elProps})`)()) } : props const app = createApp(component, useProps) app.provide('client', client) app.use(ServiceStackVue) if (options?.mount) { options.mount(app, { client, AppData }) } app.mount(el) Apps.push(app) }) return Apps.length === 1 ? Apps[0] : Apps } import { computed, onMounted, onUnmounted, ref, watch } from "vue" import { useClient, useUtils } from "@servicestack/vue" import { QueryContacts, ViewAppData } from "../dtos.mjs" const SelectEmail = { template:`
  • {{sub.firstName}} {{sub.lastName}}

    {{sub.email}}

Loading...
`, props:['modelValue','inputElement'], setup(props) { const client = useClient() const { focusNextElement } = useUtils() const popupStyle = ref('') const email = ref() const show = ref(false) const api = ref() const active = ref(-1) const results = computed(() => api.value?.response?.results || []) const loading = computed(() => client.loading.value) async function update() { await (async (search) => { const apiSearch = await client.api(new QueryContacts({ search, take:8, orderBy:'nameLower' })) if (apiSearch.succeeded && search === props.modelValue) { api.value = apiSearch active.value = -1 } })(props.modelValue) } function selectIndex(index) { const contact = index >= 0 ? results.value[index] : null if (contact) { const setFields = ['email','firstName','lastName'] setFields.forEach(id => { const el = props.inputElement.form[id] if (el) { el.value = contact[id] el.dispatchEvent(new Event('input', {bubbles:true})); } }) focusNextElement({ after:props.inputElement.form['lastName'] }) } } const inputEvents = { focus(e) { show.value = true }, blur(e) { setTimeout(() => show.value = false, 200) }, keydown(e) { if (e.key === 'ArrowDown') { if (!show.value) show.value = true if (active.value === -1) { active.value = 0 } else { active.value = (active.value + 1) % results.value.length } } else if (e.key === 'ArrowUp') { if (active.value >= 0) { active.value = (active.value - 1) % results.value.length if (active.value < 0) active.value = results.value.length - 1 } } else if (e.key === 'Enter') { show.value = false e.preventDefault() selectIndex(active.value) } else if (e.key === 'Escape') { if (show.value) { show.value = false e.stopPropagation() } } }, } onMounted(() => { client.swr(new QueryContacts({ take:8, orderBy:'nameLower' }), r => api.value = r) const el = email.value = document.querySelector('#email') el.setAttribute('autocomplete','no-autofill') Object.keys(inputEvents).forEach(evt => { inputEvents[evt] = inputEvents[evt].bind(el) el.addEventListener(evt, inputEvents[evt]) }) const rect = el.getBoundingClientRect() popupStyle.value = `top:${Math.floor(rect.y+rect.height+2)}px;left:26px;width:${Math.floor(rect.width-4)}px` }) onUnmounted(() => { Object.keys(inputEvents).forEach(evt => { email.value?.removeEventListener(evt, inputEvents[evt]) }) }) watch(() => props.modelValue, update) return { show, results, loading, active, selectIndex } } } export const EmailInput = { components: { SelectEmail }, template: ` `, emits:['update:modelValue'], props: [], setup(props) { return { } } } const InsertVariableButton = { template:`
Insert template variable (CTRL+SPACE)
  • {{name}}
`, props:['instance'], setup(props) { const client = useClient() const { transition } = useUtils() const expanded = ref({}) const api = ref() const vars = computed(() => ({ contact: { Email: 'email@example.org', FirstName: 'First', LastName: 'Last', ExternalRef: '0123456789' }, ...api.value?.response?.vars })) const toggleState = ref(false) const transition1 = ref('hidden') const rule1 = { entering: { cls:'transition ease-out duration-100', from:'transform opacity-0 scale-95', to:'transform opacity-100 scale-100'}, leaving: { cls:'transition ease-in duration-75', from:'transform opacity-100 scale-100', to:'transform opacity-0 scale-95'} } function toggle(show) { if (show == null) show = !toggleState.value transition(rule1, transition1, show) if (show) toggleState.value = show else setTimeout(() => toggleState.value = show, 100) } onMounted(async () => { await client.swr(new ViewAppData(), r => api.value = r) }) function toggleVar(label) { expanded.value[label] = !expanded.value[label] } function select(group,name) { if (group === 'contact') { props.instance.insert('{{' + name + '}}','') } else if (group === 'images') { props.instance.insert('![]({{' + `${group}.${name}` + '}})','') } else { props.instance.insert('{{' + `${group}.${name}` + '}}','') } toggle(false) } /** @param {KeyboardEvent} e */ function handleKeyDown(e) { console.log('handleKeyDown', e) if (e.code === 'Space' && e.ctrlKey) { toggle(true) e.stopPropagation() } } onMounted(() => props.instance.textarea.value?.addEventListener('keydown', handleKeyDown)) onUnmounted(() => props.instance.textarea.value?.removeEventListener('keydown', handleKeyDown)) return { toggle, toggleState, transition1, vars, expanded, select, toggleVar } } } export const MarkdownEmailInput = { components: { InsertVariableButton }, template: ` `, emits:['update:modelValue'], props: [], setup(props) { return { } } }import { computed, onMounted, ref } from "vue" import { $$, appendQueryString, combinePaths, queryString, rightPart } from "@servicestack/client" import ServiceStackVue, { useClient } from "@servicestack/vue" import { SubscribeToMailingList, UpdateContactMailingLists, FindContact } from "../Mail.dtos.mjs" import { BaseUrl, mount } from "./init.mjs" export const JoinMailingList = { template:`
{{ submitLabel || 'Subscribe' }}

{{thanksHeading || 'Thanks for signing up!'}}

{{thanksMessage || 'To complete sign up, look for the verification email in your inbox and click the link in that email.'}}

`, props:['mailingLists','placeholder','submitLabel','thanksIcon','thanksHeading','thanksMessage'], setup(props) { const client = useClient() const expand = ref(false) const submitted = ref(false) /** @param {SubmitEvent} e */ async function submit(e) { const api = await client.apiFormVoid(new SubscribeToMailingList(), new FormData(e.target)) if (api.succeeded) { submitted.value = true } } function asStrings(o) { return typeof o == 'string' ? o.split(',') : o || [] } return { expand, submitted, submit, asStrings } } } export const MailPreferences = { template:`
Loading...

{{ updatedHeading || 'Updated!' }}

{{ updatedMessage || 'Your email preferences have been saved.' }}

{{ unsubscribeHeading || 'Updated!' }}

{{ unsubscribeMessage || "You've been unsubscribed from all email subscriptions, we're sorry to see you go!" }}

{{ unsubscribePrompt || 'Unsubscribe from all future email communications:' }}

{{ submitUnsubscribeLabel || 'Unsubscribe' }}

Manage mail preferences for {{contact.email}}:

{{ submitLabel || 'Save Changes' }}

{{ emailPrompt || 'Enter your email to manage your email preferences:' }}

{{ submitEmailLabel || 'Submit' }}
`, props:['emailPrompt','submitEmailPrompt','updatedHeading','updatedMessage','unsubscribePrompt','unsubscribeHeading','unsubscribeMessage','submitLabel','submitUnsubscribeLabel'], setup(props) { const client = useClient() const contact = ref() const email = ref() const metadata = ref() const mailingListType = computed(() => metadata.value?.api.types.find(x => x.name === 'MailingList')) const contactMailingLists = ref([]) const saved = ref(false) const unsubscribeView = ref(false) const unsubscribed = ref(false) const loaded = ref(false) async function findContact(e) { if (!email.value) return const api = await client.api(new FindContact({ email: email.value, })) if (api.succeeded) { contact.value = api.response.result if (contact.value) { contactMailingLists.value = enumFlags(contact.value.mailingLists) } else { client.setError({ message: 'No existing email subscription was found' }) } } } async function submitUnsubscribe(e) { const api = await client.apiVoid(new UpdateContactMailingLists({ ref: contact.value.externalRef, unsubscribeFromAll: true, })) if (api.succeeded) { unsubscribed.value = true } } async function submit(e) { const api = await client.apiVoid(new UpdateContactMailingLists({ ref: contact.value.externalRef, mailingLists: contactMailingLists.value })) if (api.succeeded) { saved.value = true } } function enumFlags(value) { const enumType = mailingListType.value if (!enumType) throw new Error(`Could not find MailingList`) const to = [] for (let i=0; i 0 && (enumValue & value) === enumValue) { to.push(enumType.enumDescriptions?.[i] || enumType.enumNames?.[i] || `${value}`) } } return to } onMounted(async () => { metadata.value = await (await fetch(appendQueryString(combinePaths(BaseUrl, `/metadata/app.json`), {includeTypes: 'MailingList'}))).json() const search = location.search ? location.search : location.hash.includes('?') ? '?' + rightPart(location.hash,'?') : '' let qs = queryString(search) if (qs.email || qs.ref) { const api = await client.api(new FindContact({ email: qs.email, ref: qs.ref })) if (api.succeeded) { contact.value = api.response.result if (contact.value) { contactMailingLists.value = enumFlags(contact.value.mailingLists) } } } loaded.value = true unsubscribeView.value = !!qs.unsubscribe }) return { loaded, contact, email, findContact, submit, enumFlags, mailingListType, contactMailingLists, saved, unsubscribeView, unsubscribed, submitUnsubscribe } } } const components = { JoinMailingList, MailPreferences } export function mail(selector, args) { const mountOptions = { mount(app, { client, AppData }) { app.component('RouterLink', ServiceStackVue.component('RouterLink')) } } $$(selector).forEach(el => { const mail = el.getAttribute('data-mail') if (!mail) throw new Error(`Missing data-mail=Component`) const component = components[mail] if (!component) throw new Error(`Unknown component '${mail}', available components: ${Object.keys(components).join(', ')}`) mount(el, component, args, mountOptions) }) } import { onMounted, watch, computed, ref, inject, reactive, createApp, nextTick, getCurrentInstance } from "vue" import ServiceStackVue, { useClient, useAuth, useUtils } from "@servicestack/vue" import { isDate, toDate, fromXsdDuration, indexOfAny, map, leftPart, $$, enc, EventBus } from "@servicestack/client" import { GetThread, GetThreadUserData, CreateThreadLike, DeleteThreadLike, QueryComments, CreateComment, CreateCommentVote, DeleteCommentVote, DeleteComment, CreateCommentReport, PostReport, } from '../Posts.dtos.mjs' import { Authenticate, SignUpDialog, SignInDialog } from "./Auth.mjs" import { BaseUrl, mount } from "./init.mjs" export class Store { BaseUrl = BaseUrl DefaultProfileUrl = 'data:image/svg+xml,%3Csvg style=\'color:rgb(8 145 178);border-radius: 9999px;overflow: hidden;\' xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 200 200\'%3E%3Cpath fill=\'currentColor\' d=\'M200,100 a100,100 0 1 0 -167.3,73.9 a3.6,3.6 0 0 0 0.9,0.8 a99.9,99.9 0 0 0 132.9,0 l0.8,-0.8 A99.6,99.6 0 0 0 200,100 zm-192,0 a92,92 0 1 1 157.2,64.9 a75.8,75.8 0 0 0 -44.5,-34.1 a44,44 0 1 0 -41.4,0 a75.8,75.8 0 0 0 -44.5,34.1 A92.1,92.1 0 0 1 8,100 zm92,28 a36,36 0 1 1 36,-36 a36,36 0 0 1 -36,36 zm-59.1,42.4 a68,68 0 0 1 118.2,0 a91.7,91.7 0 0 1 -118.2,0 z\' /%3E%3C/svg%3E%0A' /** @type {AuthenticateResponse|null} */ auth = null /** @type {string|null} */ authKey = null /** @type {GetThreadUserDataResponse|null} */ userData = null /** @type {Thread|null} */ thread = null /** @type {JsonServiceClient} */ client = null events = new EventBus() config = { commentLink: null } constructor(client) { this.client = client const { swrCacheKey } = useUtils() this.authKey = swrCacheKey(new Authenticate()) this.userDataKey = swrCacheKey(new GetThreadUserData()) } signIn(auth) { const { signIn } = useAuth() this.auth = auth this.userData = { upVoted: [], downVoted: [], } signIn(auth) auth._date = new Date().valueOf() localStorage.setItem(this.authKey, JSON.stringify(auth)) this.loadCachedUserData() this.events.publish('signIn', auth) } signOut() { this.auth = null this.userData = null this.userAlbumArtifactsKey = this.userLikesKey = null const { signOut } = useAuth() signOut() this.events.publish('signOut') } loadCachedUserData() { const cacheKey = this.userDataKey const json = localStorage.getItem(cacheKey) if (json) { this.userData = JSON.parse(json) this.events.publish('userData', this.userData) } } async loadUserData() { if (!this.thread) return const cacheKey = this.userDataKey const api = await this.client.api(new GetThreadUserData({ threadId: this.thread.id })) if (api.succeeded) { this.userData = api.response localStorage.setItem(cacheKey, JSON.stringify(this.userData)) this.events.publish('userData', this.userData) } else { this.userData = null localStorage.removeItem(cacheKey) } } } const ModalForm = { template: /*html*/`` } const NewReport = { components: { ModalForm }, template: /*html*/`
Report Comment
Cancel Submit
`, emits:['done'], props: { commentId:Number }, setup(props, { emit }) { const visibleFields = ['PostReport', 'Description'] const postReport = ref('') const description = ref('') const client = useClient() function submit() { const { commentId } = props client.apiVoid(new CreateCommentReport({ commentId, postReport:PostReport[postReport.value], description })) .then(r => emit('done')) } return { PostReport, visibleFields, postReport, description, submit, } } } export const ThreadDialogs = { components: { SignInDialog, SignUpDialog, NewReport, }, template:`
`, emits:['showDialog','done'], props: { show:String, commentId:Number }, setup(props) { return { } } } export const InputComment = { template: /*html*/`
{{ remainingChars }} Post
`, props: ['threadId','replyId'], emits: ['updated'], setup(props, { attrs, emit }) { /** @type {Store} */ const store = inject('store') const client = useClient() const request = ref(new CreateComment({ threadId: props.threadId, replyId: props.replyId, content: '' })) const remainingChars = computed(() => 280 - request.value.content.length) async function submit() { const { threadId, replyId } = props const api = await client.api(request.value) if (api.succeeded) { request.value.content = '' emit('updated', api.response) } } return { store, request, loading: client.loading, remainingChars, submit, } }, } const Comment = { components: { InputComment }, template: /*html*/`
{{comment.handle || comment.displayName}} {{ timeAgo }}
[flagged] {{comment.content}}
unvote vote {{ comment.votes }} unvote down vote
close
`, props: ['comment', 'nested'], emits: ['voted', 'unvoted', 'showDialog', 'refresh'], setup(props, { emit }) { /** @type {Store} */ const store = inject('store') const { user } = useAuth() const instance = getCurrentInstance() const timeAgo = computed(() => Relative.from(props.comment.createdDate)) const client = useClient() const showMenu = ref(false) const showReply = ref(false) const replyDone = () => { showReply.value = false; emit('refresh'); } let hasUpVoted = () => store.userData?.upVoted.indexOf(props.comment.id) >= 0 let hasDownVoted = () => store.userData?.downVoted.indexOf(props.comment.id) >= 0 function showDialog(dialog) { showMenu.value = false emit('showDialog', dialog, props.comment) } function toggleMenu() { if (!user.value) { emit('showDialog', 'SignInDialog') return } showMenu.value =! showMenu.value } function toggleReply() { if (!user.value) { emit('showDialog', 'SignInDialog') return } showReply.value =! showReply.value } async function vote(value) { if (user.value) { if (!hasUpVoted() && !hasDownVoted()) { props.comment.votes += value const api = await client.apiVoid(new CreateCommentVote({ commentId: props.comment.id, vote: value, })) if (!api.succeeded) { props.comment.votes -= value } else { emit('voted', props.comment, value, api) } } else { if (hasUpVoted()) { props.comment.votes += -1 } if (hasDownVoted()) { props.comment.votes += 1 } const api = await client.apiVoid(new DeleteCommentVote({ commentId: props.comment.id })) if (!api.succeeded) { if (hasUpVoted.value) { props.comment.votes += 1 } if (hasDownVoted.value) { props.comment.votes += -1 } } else { emit('unvoted', props.comment, value, api) } } } else { emit('showDialog','SignInDialog') } } async function deleteComment() { showMenu.value = false const api = await client.apiVoid(new DeleteComment({ id: props.comment.id })) if (api.succeeded) { emit('refresh') } } const upVote = () => vote(1) const downVote = () => vote(-1) onMounted(() => { store.events.subscribe('userData', () => { instance?.proxy?.$forceUpdate() }) }) return { showMenu, showReply, toggleMenu, toggleReply, showDialog, replyDone, timeAgo, upVote, downVote, hasUpVoted, hasDownVoted, deleteComment, } } } const Thread = { components: { Comment }, template: /*html*/`
`, props: ['comments','parentId'], emits: ['refresh','refreshUserData','showDialog'], setup(props, { emit }) { /** @type {Store} */ const store = inject('store') const refreshUserData = () => emit('refreshUserData') const showDialog = (dialog, comment) => emit('showDialog', dialog, comment) const refresh = () => emit('refresh') const nested = computed(() => props.parentId != null) return { store, showDialog, nested, refresh, refreshUserData, } } } Thread.components['Thread'] = Thread export const PostComments = { components: { ThreadDialogs, Thread, Comment, InputComment, NewReport }, template: /*html*/`
Recommend Post Unrecommend {{ store.thread?.likesCount || ''}}
Sign in to leave a comment
Sign In Sign Up

{{ comments.length }} Comment{{ comments.length > 1 ? 's' : '' }}

`, props: ['hide','commentLink'], setup(props) { /** @type {Store} */ const store = inject('store') const client = useClient() const { user } = useAuth() const { asStrings } = useUtils() const instance = getCurrentInstance() store.config.commentLink = props.commentLink const hide = computed(() => asStrings(props.hide)) let comments = ref([]) let show = ref('') let showTarget = ref(null) const threadId = computed(() => store.thread?.id || 0) function showDialog(dialog,comment) { show.value = dialog showTarget.value = comment } async function refreshUserData() { await store.loadUserData() } async function refresh() { // console.log('refresh', store.thread?.id) const threadId = store.thread?.id if (!threadId) return const api = await client.api(new QueryComments({ threadId })); if (api.succeeded) { comments.value = api.response.results } await refreshUserData() } async function toggleLike() { if (!user.value) { showDialog('SignInDialog') return } const threadId = store.thread?.id if (!threadId || !store.userData) return const liked = store.userData.liked store.userData.liked = !liked store.thread.likesCount += liked ? -1 : 1 const request = !liked ? new CreateThreadLike({ threadId }) : new DeleteThreadLike({ threadId }) const api = await client.apiVoid(request) if (!api.succeeded) { store.userData.liked = liked store.thread.likesCount += liked ? 1 : -1 } instance?.proxy.$forceUpdate() } onMounted(async () => { store.events.subscribe('thread', refresh) await refresh() }) watch(() => user.value, refresh) return { store, hide, threadId, comments, toggleLike, user, loading: client.loading, refresh, refreshUserData, show, showDialog, showTarget, } } } const defaultFormats = { locale: map(navigator.languages, x => x[0]) || navigator.language || 'en' } const Relative = (function () { let nowMs = () => new Date().getTime() let DateChars = ['/', 'T', ':', '-'] /** @param {string|Date|number} val */ function toRelativeNumber(val) { if (val == null) return NaN if (typeof val == 'number') return val if (isDate(val)) return val.getTime() - nowMs() if (typeof val === 'string') { let num = Number(val) if (!isNaN(num)) return num if (val[0] === 'P' || val.startsWith('-P')) return fromXsdDuration(val) * 1000 * -1 if (indexOfAny(val, DateChars) >= 0) return toDate(val).getTime() - nowMs() } return NaN } let defaultRtf = new Intl.RelativeTimeFormat(defaultFormats.locale, {}) let year = 24 * 60 * 60 * 1000 * 365 let units = { year, month: year / 12, day: 24 * 60 * 60 * 1000, hour: 60 * 60 * 1000, minute: 60 * 1000, second: 1000 } /** @param {number} elapsedMs * @param {Intl.RelativeTimeFormat} [rtf] */ function fromMs(elapsedMs, rtf) { for (let u in units) { if (Math.abs(elapsedMs) > units[u] || u === 'second') return (rtf || defaultRtf).format(Math.round(elapsedMs / units[u]), u) } } /** @param {string|Date|number} val * @param {Intl.RelativeTimeFormat} [rtf] */ function from(val, rtf) { let num = toRelativeNumber(val) if (!isNaN(num)) return fromMs(num, rtf) console.error(`Cannot convert ${val}:${typeof val} to relativeTime`) return '' } /** @param {Date} d * @param {Date} [from] */ let fromDate = (d, from) => fromMs(d.getTime() - (from ? from.getTime() : nowMs())) return { from, fromMs, fromDate, } })(); const components = { PostComments } export function post(selector, args) { const mountOptions = { mount(app, { client, AppData }) { const store = new Store(client) nextTick(async () => { const [api, threadApi] = await Promise.all([ client.api(new Authenticate()), client.api(new GetThread({ url: leftPart(location.href.replace('#','?'),'?') })) ]) if (threadApi.succeeded) { store.thread = threadApi.response.result store.events.publish('thread', store.thread) } if (api.succeeded) { store.signIn(api.response) await store.loadUserData() } else { store.signOut() } }) app.provide('store', store) app.component('RouterLink', ServiceStackVue.component('RouterLink')) app.directive('highlightjs', (el, binding) => { if (binding.value) { el.innerHTML = enc(binding.value) globalThis.hljs.highlightElement(el) } }) globalThis.store = store } } $$(selector).forEach(el => { const post = el.getAttribute('data-post') if (!post) throw new Error(`Missing data-post=Component`) const component = components[post] if (!component) throw new Error(`Unknown component '${post}', available components: ${Object.keys(components).join(', ')}`) mount(el, component, args, mountOptions) }) }