feat(orders): tg notifications, ya metrika, meta tags
This commit is contained in:
@@ -4,10 +4,11 @@
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Vite + Vue</title>
|
||||
<title>OpenCart Telegram Mini App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="app-error"></div>
|
||||
<script src="https://telegram.org/js/telegram-web-app.js?58"></script>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -22,22 +22,21 @@ disableVerticalSwipes();
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const backButton = useBackButton();
|
||||
|
||||
watch(
|
||||
() => route,
|
||||
() => route.name,
|
||||
() => {
|
||||
if (route.name === 'home') {
|
||||
backButton.hide?.();
|
||||
window.Telegram.WebApp.BackButton.hide();
|
||||
window.Telegram.WebApp.BackButton.offClick();
|
||||
} else {
|
||||
backButton.show?.();
|
||||
backButton.onClick?.(() => {
|
||||
window.Telegram.WebApp.BackButton.show();
|
||||
window.Telegram.WebApp.BackButton.onClick(() => {
|
||||
window.Telegram.WebApp.HapticFeedback.impactOccurred('light');
|
||||
router.back();
|
||||
|
||||
});
|
||||
}
|
||||
},
|
||||
{immediate: true, deep: true}
|
||||
{immediate: true}
|
||||
);
|
||||
</script>
|
||||
|
||||
21
spa/src/ApplicationError.vue
Normal file
21
spa/src/ApplicationError.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div style="z-index: 99999" class="fixed top-0 left-0 w-full h-full bg-base-100">
|
||||
<div class="flex flex-col items-center justify-center h-full">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-20">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9v6m-4.5 0V9M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
|
||||
<h1 class="font-semibold text-2xl mb-2">Магазин временно недоступен</h1>
|
||||
<p class="text-sm text-muted">Мы на перерыве, скоро всё снова заработает 🛠️</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
error: Error,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="route.name !== 'cart.show'" class="fixed right-2 bottom-30 z-50 opacity-90">
|
||||
<div v-if="isCartBtnShow" class="fixed right-2 bottom-30 z-50 opacity-90">
|
||||
<div class="indicator">
|
||||
<span class="indicator-item indicator-top indicator-start badge badge-secondary">{{ cart.productsCount }}</span>
|
||||
<button class="btn btn-primary btn-lg btn-circle" @click="openCart">
|
||||
@@ -15,7 +15,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted} from "vue";
|
||||
import {computed, onMounted} from "vue";
|
||||
import {useCartStore} from "@/stores/CartStore.js";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
|
||||
@@ -23,6 +23,11 @@ const cart = useCartStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const isCartBtnShow = computed(() => {
|
||||
return route.name !== 'cart.show' && route.name !== 'checkout';
|
||||
});
|
||||
|
||||
|
||||
function openCart() {
|
||||
window.Telegram.WebApp.HapticFeedback.selectionChanged();
|
||||
router.push({name: 'cart.show'});
|
||||
|
||||
@@ -11,7 +11,3 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -104,11 +104,8 @@ watch(() => route.params.id, async newId => {
|
||||
|
||||
onMounted(async () => {
|
||||
const saved = productsStore.savedCategoryId === categoryId;
|
||||
|
||||
console.log("Saved Category: ", saved);
|
||||
if (saved && productsStore.products.data.length > 0) {
|
||||
await nextTick();
|
||||
console.log("Products exists, scrolling to ", productsStore.savedScrollY);
|
||||
// повторяем до тех пор, пока высота не станет больше savedScrollY
|
||||
const interval = setInterval(() => {
|
||||
const maxScroll = document.documentElement.scrollHeight - window.innerHeight
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { createApp } from 'vue'
|
||||
import {createApp} from 'vue'
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
import { VueTelegramPlugin } from 'vue-tg';
|
||||
import { router } from './router';
|
||||
import { createPinia } from 'pinia';
|
||||
import {VueTelegramPlugin} from 'vue-tg';
|
||||
import {router} from './router';
|
||||
import {createPinia} from 'pinia';
|
||||
|
||||
import {useCategoriesStore} from "@/stores/CategoriesStore.js";
|
||||
import {useSettingsStore} from "@/stores/SettingsStore.js";
|
||||
import ApplicationError from "@/ApplicationError.vue";
|
||||
import AppMetaInitializer from "@/utils/AppMetaInitializer.ts";
|
||||
import {injectYaMetrika} from "@/utils/yaMetrika.js";
|
||||
|
||||
const pinia = createPinia();
|
||||
const app = createApp(App);
|
||||
@@ -12,22 +18,26 @@ app
|
||||
.use(router)
|
||||
.use(VueTelegramPlugin);
|
||||
|
||||
app.mount('#app');
|
||||
|
||||
const settings = useSettingsStore();
|
||||
const categoriesStore = useCategoriesStore();
|
||||
categoriesStore.fetchTopCategories();
|
||||
categoriesStore.fetchCategories();
|
||||
|
||||
import {useCategoriesStore} from "@/stores/CategoriesStore.js";
|
||||
import {useSettingsStore} from "@/stores/SettingsStore.js";
|
||||
|
||||
if (settings.night_auto) {
|
||||
window.Telegram.WebApp.onEvent('themeChanged', function () {
|
||||
document.documentElement.setAttribute('data-theme', settings.theme[this.colorScheme]);
|
||||
settings.load()
|
||||
.then(() => {
|
||||
document.documentElement.setAttribute('data-theme', settings.theme[window.Telegram.WebApp.colorScheme]);
|
||||
if (settings.night_auto) {
|
||||
window.Telegram.WebApp.onEvent('themeChanged', function () {
|
||||
document.documentElement.setAttribute('data-theme', settings.theme[this.colorScheme]);
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => new AppMetaInitializer(settings).init())
|
||||
.then(() => app.mount('#app'))
|
||||
.then(() => window.Telegram.WebApp.ready())
|
||||
.then(() => settings.ya_metrika_enabled && injectYaMetrika())
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorApp = createApp(ApplicationError, {error});
|
||||
errorApp.mount('#app-error');
|
||||
});
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', settings.theme.light);
|
||||
}
|
||||
|
||||
window.Telegram.WebApp.ready();
|
||||
|
||||
@@ -12,6 +12,7 @@ export const useCheckoutStore = defineStore('checkout', {
|
||||
phone: "+79999999999",
|
||||
address: "Москва, Красная площадь, 1",
|
||||
comment: "Доставить срочно❗️",
|
||||
tgData: null,
|
||||
},
|
||||
|
||||
validationErrors: {},
|
||||
@@ -26,6 +27,19 @@ export const useCheckoutStore = defineStore('checkout', {
|
||||
actions: {
|
||||
async makeOrder() {
|
||||
try {
|
||||
const data = window.Telegram.WebApp.initDataUnsafe;
|
||||
|
||||
if (! data.allows_write_to_pm) {
|
||||
await window.Telegram.WebApp.requestWriteAccess((granted) => {
|
||||
if (granted) {
|
||||
console.log('Пользователь разрешил отправку сообщений');
|
||||
} else {
|
||||
alert('Вы не дали разрешение — бот не сможет отправлять вам уведомления');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.customer.tgData = data.user;
|
||||
await storeOrder(this.customer);
|
||||
await window.Telegram.WebApp.HapticFeedback.notificationOccurred('success');
|
||||
await useCartStore().getProducts();
|
||||
@@ -44,6 +58,6 @@ export const useCheckoutStore = defineStore('checkout', {
|
||||
|
||||
clearError(field) {
|
||||
this.validationErrors[field] = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,12 +1,37 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {fetchSettings} from "@/utils/ftch.js";
|
||||
|
||||
export const useSettingsStore = defineStore('settings', {
|
||||
state: () => ({
|
||||
app_name: 'OpenCart Telegram магазин',
|
||||
app_icon: '',
|
||||
app_icon192: '',
|
||||
app_icon180: '',
|
||||
app_icon152: '',
|
||||
app_icon120: '',
|
||||
manifest_url: null,
|
||||
night_auto: true,
|
||||
ya_metrika_enabled: false,
|
||||
theme: {
|
||||
light: 'light',
|
||||
dark: 'dark',
|
||||
},
|
||||
noMoreProductsMessage: '🔚 Ну всё, разгрузили всё, что было. Даже кладовщика разбудить не удалось.',
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async load() {
|
||||
const settings = await fetchSettings();
|
||||
this.manifest_url = settings.manifest_url;
|
||||
this.app_name = settings.app_name;
|
||||
this.app_icon = settings.app_icon;
|
||||
this.app_icon192 = settings.app_icon192;
|
||||
this.app_icon180 = settings.app_icon180;
|
||||
this.app_icon152 = settings.app_icon152;
|
||||
this.app_icon120 = settings.app_icon120;
|
||||
this.theme.light = settings.theme_light;
|
||||
this.theme.dark = settings.theme_dark;
|
||||
this.ya_metrika_enabled = settings.ya_metrika_enabled;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
46
spa/src/utils/AppMetaInitializer.ts
Normal file
46
spa/src/utils/AppMetaInitializer.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
class AppMetaInitializer {
|
||||
private readonly settings: object;
|
||||
|
||||
constructor(settings: object) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public init() {
|
||||
document.title = this.settings.app_name;
|
||||
this.setMeta('application-name', this.settings.app_name);
|
||||
this.setMeta('apple-mobile-web-app-title', this.settings.app_name);
|
||||
this.setMeta('mobile-web-app-capable', 'yes');
|
||||
this.setMeta('apple-mobile-web-app-capable', 'yes');
|
||||
this.setMeta('apple-mobile-web-app-status-bar-style', 'default');
|
||||
this.setMeta('theme-color', '#000000');
|
||||
this.setMeta('msapplication-navbutton-color', '#000000');
|
||||
this.setMeta('apple-mobile-web-app-status-bar-style', 'black-translucent');
|
||||
this.addLink('manifest', this.settings.manifest_url);
|
||||
|
||||
this.addLink('icon', this.settings.app_icon192, '192x192');
|
||||
this.addLink('apple-touch-icon', this.settings.app_icon192);
|
||||
this.addLink('apple-touch-icon', this.settings.app_icon180, '180x180');
|
||||
this.addLink('apple-touch-icon', this.settings.app_icon152, '152x152');
|
||||
this.addLink('apple-touch-icon', this.settings.app_icon120, '120x120');
|
||||
}
|
||||
|
||||
private setMeta(name: string, content: string) {
|
||||
let meta = document.querySelector(`meta[name="${name}"]`);
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta');
|
||||
meta.setAttribute('name', name);
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
meta.setAttribute('content', content);
|
||||
}
|
||||
|
||||
private addLink(rel: string, href: string, sizes?: string) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = rel;
|
||||
link.href = href;
|
||||
if (sizes) link.sizes = sizes;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
}
|
||||
|
||||
export default AppMetaInitializer;
|
||||
@@ -59,4 +59,8 @@ export async function cartEditItem(data) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchSettings() {
|
||||
return await ftch('settings');
|
||||
}
|
||||
|
||||
export default ftch;
|
||||
|
||||
7
spa/src/utils/yaMetrika.js
Normal file
7
spa/src/utils/yaMetrika.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export function injectYaMetrika() {
|
||||
const script = document.createElement('script');
|
||||
script.src = '/index.php?route=extension/tgshop/handle/ya_metrika';
|
||||
script.async = true;
|
||||
document.head.appendChild(script);
|
||||
console.log('Yandex Metrika injected to the page.');
|
||||
}
|
||||
Reference in New Issue
Block a user