diff --git a/packages/frontend-main/src/components/tma/TMAInfo.vue b/packages/frontend-main/src/components/tma/TMAInfo.vue index e4a41ccb..59dc0d6f 100644 --- a/packages/frontend-main/src/components/tma/TMAInfo.vue +++ b/packages/frontend-main/src/components/tma/TMAInfo.vue @@ -17,8 +17,8 @@ onMounted(async () => { const tmaInfo = computed(() => { const params = tmaStore.launchParams; - const initData = tmaStore.initData; - const user = initData?.user(); + const initDataValue = tmaStore.initData; + const user = initDataValue?.user; if (!user) { return { diff --git a/packages/frontend-main/src/composables/useSEO.ts b/packages/frontend-main/src/composables/useSEO.ts new file mode 100644 index 00000000..cc51e223 --- /dev/null +++ b/packages/frontend-main/src/composables/useSEO.ts @@ -0,0 +1,57 @@ +// TODO: Consider migrating to @unhead/vue for reactivity-first SEO management +// See: https://unhead.unjs.io/ +// This would provide better SSR support and reactive composables that could be extended to other pages. + +interface SeoOptions { + title?: string; + description?: string; + ogTitle?: string; + ogDescription?: string; + ogUrl?: string; +} + +function upsertMeta( + attributes: Record, +) { + if (typeof document === 'undefined') return; + + const selectorParts = Object.entries(attributes) + .filter(([key]) => key === 'name' || key === 'property') + .map(([key, value]) => `[${key}="${value}"]`); + + const selector = selectorParts.join(''); + let element = selector ? document.head.querySelector(`meta${selector}`) : null; + + if (!element) { + element = document.createElement('meta'); + document.head.appendChild(element); + } + + Object.entries(attributes).forEach(([key, value]) => { + element?.setAttribute(key, value); + }); +} + +export function useSEO(options: SeoOptions) { + if (typeof document === 'undefined') return; + + if (options.title) { + document.title = options.title; + upsertMeta({ property: 'og:title', content: options.ogTitle ?? options.title }); + } + + if (options.description) { + upsertMeta({ name: 'description', content: options.description }); + } + + if (options.ogDescription || options.description) { + upsertMeta({ + property: 'og:description', + content: options.ogDescription ?? options.description ?? '', + }); + } + + if (options.ogUrl) { + upsertMeta({ property: 'og:url', content: options.ogUrl }); + } +} diff --git a/packages/frontend-main/src/config/externalLinks.ts b/packages/frontend-main/src/config/externalLinks.ts new file mode 100644 index 00000000..f35ca400 --- /dev/null +++ b/packages/frontend-main/src/config/externalLinks.ts @@ -0,0 +1,10 @@ +export const externalLinks = { + siteUrl: 'https://dither.chat', + protocolDocs: 'https://docs.dither.chat', + docsHome: 'https://docs.dither.chat', + privacyPolicy: 'https://docs.dither.chat/policies/privacy-policy', + termsOfService: 'https://docs.dither.chat/policies/terms-of-service', + telegramBot: 'https://t.me/dither_chat_bot', + githubRepo: 'https://github.com/allinbits/dither.chat', + atomoneWebsite: 'https://atom.one/', +}; diff --git a/packages/frontend-main/src/layouts/panels/RightPanel.vue b/packages/frontend-main/src/layouts/panels/RightPanel.vue index 3664e3d5..2493a516 100644 --- a/packages/frontend-main/src/layouts/panels/RightPanel.vue +++ b/packages/frontend-main/src/layouts/panels/RightPanel.vue @@ -1,13 +1,20 @@ diff --git a/packages/frontend-main/src/localization/index.ts b/packages/frontend-main/src/localization/index.ts index 0e73076b..98b988cb 100644 --- a/packages/frontend-main/src/localization/index.ts +++ b/packages/frontend-main/src/localization/index.ts @@ -168,7 +168,15 @@ export const messages = { About: { privacyPolicy: 'Privacy Policy', termsAndConditions: 'Terms & Conditions', - docs: 'Documentation', + heroTagline: 'Social protocol for decentralized networking', + atomoneLink: 'AtomOne', + coreDesc: 'Anyone can run the service or build their own client.', + ctaDocs: 'Protocol Docs', + ctaPrivacy: 'Privacy Policy', + ctaTerms: 'Terms & Conditions', + ctaGitHub: 'Source Code', + ctaTelegram: 'Telegram Bot', + copyright: 'All in Bits', }, Authz: { title: 'Authz', diff --git a/packages/frontend-main/src/stores/useTMAStore.ts b/packages/frontend-main/src/stores/useTMAStore.ts index f728bc1e..1f792c7b 100644 --- a/packages/frontend-main/src/stores/useTMAStore.ts +++ b/packages/frontend-main/src/stores/useTMAStore.ts @@ -1,4 +1,4 @@ -import type { InitData, RetrieveLaunchParamsError, RetrieveLaunchParamsResult } from '@tma.js/sdk-vue'; +import type { RetrieveLaunchParamsResult } from '@tma.js/sdk-vue'; import { initData, @@ -22,7 +22,7 @@ declare global { export const useTMAStore = defineStore('tmaStore', () => { const isInitialized = ref(false); const launchParams = ref(null); - const initDataRef = ref>(); + const initDataSignal = useSignal(initData.state); const error = ref(null); const debugLogs = ref([]); @@ -38,12 +38,6 @@ export const useTMAStore = defineStore('tmaStore', () => { const params = retrieveLaunchParams(); launchParams.value = params; - // Get init data using the signal - const initDataSignal = useSignal(initData.state); - if (initDataSignal.value) { - initDataRef.value = initDataSignal.value as unknown as InitData; - } - isInitialized.value = true; error.value = null; } catch (err) { @@ -66,7 +60,7 @@ export const useTMAStore = defineStore('tmaStore', () => { ), isInitialized, launchParams, - initData: initDataRef, + initData: initDataSignal, error, debugLogs, initialize, diff --git a/packages/frontend-main/src/views/AboutView.vue b/packages/frontend-main/src/views/AboutView.vue index 5d0e7a5a..21c2c81d 100644 --- a/packages/frontend-main/src/views/AboutView.vue +++ b/packages/frontend-main/src/views/AboutView.vue @@ -1,87 +1,93 @@