diff --git a/resources/js/app.ts b/resources/js/app.ts
index 1db690c..0083c31 100644
--- a/resources/js/app.ts
+++ b/resources/js/app.ts
@@ -1,5 +1,6 @@
import { createInertiaApp, router } from '@inertiajs/vue3';
import { registerSW } from 'virtual:pwa-register';
+import { initPwa } from './lib/usePwa';
import AdminLayout from './layouts/AdminLayout.vue';
import AppLayout from './layouts/AppLayout.vue';
@@ -17,6 +18,7 @@ if (
}
registerSW({ immediate: true });
+initPwa();
router.on('navigate', () => {
if (typeof window !== 'undefined' && (window as any).posthog) {
diff --git a/resources/js/components/NavBar.vue b/resources/js/components/NavBar.vue
index 567f574..4cc8b44 100644
--- a/resources/js/components/NavBar.vue
+++ b/resources/js/components/NavBar.vue
@@ -19,6 +19,7 @@ import {
HeartHandshake,
Search,
MessageCircle,
+ Download,
} from 'lucide-vue-next';
import {
computed,
@@ -30,6 +31,7 @@ import {
} from 'vue';
import { globalSearchQuery } from '@/lib/searchStore';
import { useDarkMode } from '@/lib/useDarkMode';
+import { usePwa } from '@/lib/usePwa';
import AppLogo from './AppLogo.vue';
import AuthModal from './AuthModal.vue';
@@ -41,6 +43,15 @@ defineProps({
});
const { theme, toggle } = useDarkMode();
+const { deferredPrompt, isInstalled, promptInstall } = usePwa();
+
+const canInstallApp = computed(
+ () => !isInstalled.value && Boolean(deferredPrompt.value),
+);
+
+const handleInstallApp = async () => {
+ await promptInstall();
+};
const page = usePage();
const user = computed(() => page.props.auth?.user);
@@ -452,6 +463,21 @@ onBeforeUnmount(() => {
/>
{{ isAdmin ? 'Home' : 'Staff Panel' }}
+
+
@@ -619,6 +645,44 @@ onBeforeUnmount(() => {
+
+
+
+
+
-import { ref, shallowRef, onMounted, onUnmounted } from 'vue';
+import { Download, X, CheckCircle2, Loader2 } from 'lucide-vue-next';
+import { ref, onMounted, watch } from 'vue';
+import { usePwa } from '@/lib/usePwa';
defineOptions({
inheritAttrs: false,
@@ -10,14 +12,26 @@ const props = defineProps({
type: String,
default: 'modal',
},
+ title: {
+ type: String,
+ default: 'HSCStack অ্যাপ ইনস্টল করুন',
+ },
+ description: {
+ type: String,
+ default: 'দ্রুতগতির ব্রাউজিং ও নিরবচ্ছিন্ন স্টাডি এক্সপেরিয়েন্স',
+ },
});
+const { deferredPrompt, isInstalled, promptInstall } = usePwa();
const isVisible = ref(false);
-const deferredPrompt = shallowRef(null);
+const isInstalling = ref(false);
-const handleBeforeInstall = (e: Event) => {
- e.preventDefault();
- deferredPrompt.value = e;
+const updateVisibility = () => {
+ if (isInstalled.value || !deferredPrompt.value) {
+ isVisible.value = false;
+
+ return;
+ }
if (props.variant === 'modal') {
if (!sessionStorage.getItem('pwa_prompt_dismissed')) {
@@ -28,27 +42,12 @@ const handleBeforeInstall = (e: Event) => {
}
};
-const handleAppInstalled = () => {
- isVisible.value = false;
- deferredPrompt.value = null;
-};
-
onMounted(() => {
- const isStandalone =
- window.matchMedia('(display-mode: standalone)').matches ||
- (window.navigator as Record).standalone === true;
-
- if (isStandalone) {
- return;
- }
-
- window.addEventListener('beforeinstallprompt', handleBeforeInstall);
- window.addEventListener('appinstalled', handleAppInstalled);
+ updateVisibility();
});
-onUnmounted(() => {
- window.removeEventListener('beforeinstallprompt', handleBeforeInstall);
- window.removeEventListener('appinstalled', handleAppInstalled);
+watch([deferredPrompt, isInstalled], () => {
+ updateVisibility();
});
const handleInstall = async () => {
@@ -56,16 +55,16 @@ const handleInstall = async () => {
return;
}
+ isInstalling.value = true;
+
try {
- await deferredPrompt.value.prompt();
- const { outcome } = await deferredPrompt.value.userChoice;
+ const success = await promptInstall();
- if (outcome === 'accepted') {
+ if (success) {
isVisible.value = false;
- deferredPrompt.value = null;
}
- } catch (error) {
- console.error('PWA install error:', error);
+ } finally {
+ isInstalling.value = false;
}
};
@@ -79,94 +78,204 @@ const handleDismiss = () => {
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
- 📱
+
-
+
- Install Our App
+ {{ title }}
- Get fast, offline access right from your home
- screen.
+ {{ description }}
-
-
-
+
+
-
-
-
-
- 📱
-
-
-
- Install Our App
-
-
- Fast, offline home screen access.
-
-
+
+
+
+
+
-
-
-
+
- Install
-
-
+
- ✕
-
+ {{ description }}
+
-
+
+
+
+
+ ইনস্টল করুন
+
+
+ ✕
+
+
+
diff --git a/resources/js/lib/usePwa.ts b/resources/js/lib/usePwa.ts
new file mode 100644
index 0000000..b3b333a
--- /dev/null
+++ b/resources/js/lib/usePwa.ts
@@ -0,0 +1,67 @@
+import { ref, shallowRef } from 'vue';
+
+const deferredPrompt = shallowRef
(null);
+const isInstalled = ref(false);
+
+let isInitialized = false;
+
+export function initPwa() {
+ if (typeof window === 'undefined' || isInitialized) {
+ return;
+ }
+
+ isInitialized = true;
+
+ const checkStandalone = () => {
+ const isStandalone =
+ window.matchMedia('(display-mode: standalone)').matches ||
+ (window.navigator as Record).standalone === true;
+ isInstalled.value = isStandalone;
+ };
+
+ checkStandalone();
+
+ window.addEventListener('beforeinstallprompt', (e: Event) => {
+ e.preventDefault();
+ deferredPrompt.value = e;
+ });
+
+ window.addEventListener('appinstalled', () => {
+ isInstalled.value = true;
+ deferredPrompt.value = null;
+ });
+}
+
+export function usePwa() {
+ if (!isInitialized) {
+ initPwa();
+ }
+
+ const promptInstall = async (): Promise => {
+ if (!deferredPrompt.value) {
+ return false;
+ }
+
+ try {
+ await deferredPrompt.value.prompt();
+ const { outcome } = await deferredPrompt.value.userChoice;
+
+ if (outcome === 'accepted') {
+ isInstalled.value = true;
+ deferredPrompt.value = null;
+
+ return true;
+ }
+ } catch (err) {
+ console.error('PWA install error:', err);
+ }
+
+ return false;
+ };
+
+ return {
+ deferredPrompt,
+ isInstalled,
+ promptInstall,
+ };
+}
diff --git a/resources/js/pages/Chat/Index.vue b/resources/js/pages/Chat/Index.vue
index 5472b02..54f33a4 100644
--- a/resources/js/pages/Chat/Index.vue
+++ b/resources/js/pages/Chat/Index.vue
@@ -22,6 +22,7 @@ import {
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
import ChatBanModal from '@/components/ChatBanModal.vue';
import type { ChatBanUser } from '@/components/ChatBanModal.vue';
+import PwaInstallPrompt from '@/components/PwaInstallPrompt.vue';
import VerifiedBadge from '@/components/VerifiedBadge.vue';
import { getEcho } from '@/lib/echo';
import { usePermissions } from '@/lib/usePermissions';
@@ -1735,6 +1736,9 @@ onUnmounted(() => {
@close="isBanModalOpen = false"
/>
+
+
+