feat: PWA

This commit is contained in:
Jannat Patel
2025-08-11 15:43:34 +05:30
parent ea289e02da
commit 4d25d185c3
52 changed files with 2740 additions and 52 deletions

View File

@@ -5,6 +5,7 @@
<router-view />
</div>
</Layout>
<InstallPrompt v-if="isMobile" />
<Dialogs />
</FrappeUIProvider>
</template>
@@ -13,14 +14,15 @@ import { FrappeUIProvider } from 'frappe-ui'
import { Dialogs } from '@/utils/dialogs'
import { computed, onUnmounted, ref, watch } from 'vue'
import { useScreenSize } from './utils/composables'
import DesktopLayout from './components/DesktopLayout.vue'
import MobileLayout from './components/MobileLayout.vue'
import NoSidebarLayout from './components/NoSidebarLayout.vue'
import { usersStore } from '@/stores/user'
import { useRouter } from 'vue-router'
import { posthogSettings } from '@/telemetry'
import DesktopLayout from './components/DesktopLayout.vue'
import MobileLayout from './components/MobileLayout.vue'
import NoSidebarLayout from './components/NoSidebarLayout.vue'
import InstallPrompt from './components/InstallPrompt.vue'
const screenSize = useScreenSize()
const { isMobile } = useScreenSize()
const router = useRouter()
const noSidebar = ref(false)
const { userResource } = usersStore()
@@ -38,10 +40,9 @@ const Layout = computed(() => {
if (noSidebar.value) {
return NoSidebarLayout
}
if (screenSize.width < 640) {
if (isMobile.value) {
return MobileLayout
}
return DesktopLayout
})

View File

@@ -0,0 +1,97 @@
<template>
<Dialog v-model="showDialog">
<template #body-title>
<h2 class="text-lg font-bold">{{ __('Install Frappe Learning') }}</h2>
</template>
<template #body-content>
<p>
{{
__(
'Get the app on your device for easy access & a better experience!'
)
}}
</p>
</template>
<template #actions>
<Button variant="solid" class="w-full py-5" @click="install">
<template #prefix><FeatherIcon name="download" class="w-4" /></template>
{{ __('Install') }}
</Button>
</template>
</Dialog>
<Popover :show="iosInstallMessage" placement="bottom">
<template #body>
<div
class="mx-2 mt-[calc(100vh-15rem)] flex flex-col gap-3 rounded bg-blue-100 py-5 drop-shadow-xl"
>
<div
class="mb-1 flex flex-row items-center justify-between px-3 text-center"
>
<span class="text-base font-bold text-gray-900">
{{ __('Install Frappe Learning') }}
</span>
<span class="inline-flex items-baseline">
<FeatherIcon
name="x"
class="ml-auto h-4 w-4 text-gray-700"
@click="iosInstallMessage = false"
/>
</span>
</div>
<div class="px-3 text-xs text-gray-800">
<span class="flex flex-col gap-2">
<span>
{{
__(
'Get the app on your iPhone for easy access & a better experience'
)
}}
</span>
<span class="inline-flex items-start whitespace-nowrap">
<span>{{ __('Tap') }}&nbsp;</span>
<FeatherIcon name="share" class="h-4 w-4 text-blue-600" />
<span>&nbsp;{{ __("and then 'Add to Home Screen'") }}</span>
</span>
</span>
</div>
</div>
</template>
</Popover>
</template>
<script setup>
import { ref } from 'vue'
import { Button, Dialog, FeatherIcon, Popover } from 'frappe-ui'
const deferredPrompt = ref(null)
const showDialog = ref(false)
const iosInstallMessage = ref(false)
const isIos = () => {
const userAgent = window.navigator.userAgent.toLowerCase()
return /iphone|ipad|ipod/.test(userAgent)
}
const isInStandaloneMode = () =>
'standalone' in window.navigator && window.navigator.standalone
if (isIos() && !isInStandaloneMode()) iosInstallMessage.value = true
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault()
deferredPrompt.value = e
if (isIos() && !isInStandaloneMode()) iosInstallMessage.value = true
else showDialog.value = true
})
window.addEventListener('appinstalled', () => {
showDialog.value = false
deferredPrompt.value = null
})
const install = () => {
deferredPrompt.value.prompt()
showDialog.value = false
}
</script>

View File

@@ -16,11 +16,11 @@
</div>
</header>
<div class="py-5">
<div class="px-20 pb-5 space-y-5 border-b mb-5">
<div class="px-5 md:px-20 pb-5 space-y-5 border-b mb-5">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Details') }}
</div>
<div class="grid grid-cols-2 gap-5">
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div class="space-y-5">
<FormControl
v-model="batch.title"
@@ -48,11 +48,11 @@
</div>
</div>
<div class="px-20 pb-5 space-y-5 border-b mb-5">
<div class="px-5 md:px-20 pb-5 space-y-5 border-b mb-5">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Settings') }}
</div>
<div class="grid grid-cols-3 gap-5">
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
<FormControl
v-model="batch.published"
type="checkbox"
@@ -71,11 +71,11 @@
</div>
</div>
<div class="px-20 pb-5 space-y-5 border-b mb-5">
<div class="px-5 md:px-20 pb-5 space-y-5 border-b mb-5">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Date and Time') }}
</div>
<div class="grid grid-cols-3 gap-10">
<div class="grid grid-cols-1 md:grid-cols-3 gap-10">
<div class="space-y-5">
<FormControl
v-model="batch.start_date"
@@ -127,7 +127,7 @@
</div>
</div>
<div class="px-20 pb-5 space-y-5 border-b mb-5">
<div class="px-5 md:px-20 pb-5 space-y-5 border-b mb-5">
<div>
<label class="block text-sm text-ink-gray-5 mb-1">
{{ __('Batch Details') }}
@@ -143,11 +143,11 @@
</div>
</div>
<div class="px-20 pb-5 space-y-5 border-b mb-5">
<div class="px-5 md:px-20 pb-5 space-y-5 border-b mb-5">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Configurations') }}
</div>
<div class="grid grid-cols-3 gap-10">
<div class="grid grid-cols-1 md:grid-cols-3 gap-10">
<div class="space-y-5">
<FormControl
v-model="batch.seat_count"
@@ -217,7 +217,7 @@
>
<div class="flex items-center">
<div
class="border rounded-md w-fit py-5 px-20 cursor-pointer"
class="border rounded-md w-fit py-5 px-5 md:px-20 cursor-pointer"
@click="openFileSelector"
>
<Image class="size-5 stroke-1 text-ink-gray-7" />
@@ -260,7 +260,7 @@
</div>
</div>
<div class="px-20 pb-5 space-y-5">
<div class="px-5 md:px-20 pb-5 space-y-5">
<div class="text-lg text-ink-gray-9 font-semibold">
{{ __('Pricing') }}
</div>
@@ -269,7 +269,10 @@
type="checkbox"
:label="__('Paid Batch')"
/>
<div v-if="batch.paid_batch" class="grid grid-cols-3 gap-5">
<div
v-if="batch.paid_batch"
class="grid grid-cols-1 md:grid-cols-3 gap-5"
>
<FormControl
v-model="batch.amount"
:label="__('Amount')"
@@ -284,7 +287,7 @@
</div>
</div>
<div class="px-20 pb-5 space-y-5 border-b">
<div class="px-5 md:px-20 pb-5 space-y-5 border-b">
<div class="text-lg text-ink-gray-9 font-semibold">
{{ __('Meta Tags') }}
</div>

View File

@@ -1,6 +1,6 @@
<template>
<div class="h-full">
<div class="grid md:grid-cols-[70%,30%] h-full">
<div class="grid grid-cols-1 md:grid-cols-[70%,30%] h-full">
<div>
<header
class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
@@ -20,11 +20,11 @@
</div>
</header>
<div class="mt-5 mb-5">
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold mb-4">
{{ __('Details') }}
</div>
<div class="grid grid-cols-2 gap-5">
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<FormControl
v-model="course.title"
:label="__('Title')"
@@ -37,7 +37,7 @@
:onCreate="(value, close) => openSettings('Categories', close)"
/>
</div>
<div class="grid grid-cols-2 gap-5">
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<MultiSelect
v-model="instructors"
doctype="User"
@@ -74,7 +74,7 @@
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-5">
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div class="mb-4">
<div class="text-xs text-ink-gray-5 mb-2">
{{ __('Course Image') }}
@@ -137,11 +137,11 @@
</div>
</div>
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold">
{{ __('Settings') }}
</div>
<div class="grid grid-cols-2 gap-5">
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div class="flex flex-col space-y-5">
<FormControl
type="checkbox"
@@ -174,7 +174,7 @@
</div>
</div>
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold">
{{ __('About the Course') }}
</div>
@@ -230,11 +230,11 @@
/>
</div>
<div class="px-10 pb-5 space-y-5 border-b">
<div class="px-5 md:px-10 pb-5 space-y-5 border-b">
<div class="text-lg font-semibold mt-5">
{{ __('Pricing and Certification') }}
</div>
<div class="grid grid-cols-3">
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
<FormControl
type="checkbox"
v-model="course.paid_course"
@@ -251,7 +251,7 @@
:label="__('Paid Certificate')"
/>
</div>
<div class="grid grid-cols-2 gap-5">
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div class="space-y-5">
<FormControl
v-if="course.paid_course || course.paid_certificate"
@@ -278,7 +278,7 @@
</div>
</div>
<div class="px-10 pb-5 space-y-5">
<div class="px-5 md:px-10 pb-5 space-y-5">
<div class="text-lg font-semibold mt-5">
{{ __('Meta Tags') }}
</div>

View File

@@ -1,4 +1,4 @@
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
export function useScreenSize() {
const size = reactive({
@@ -6,6 +6,8 @@ export function useScreenSize() {
height: window.innerHeight,
})
const isMobile = computed(() => size.width < 640)
const onResize = () => {
size.width = window.innerWidth
size.height = window.innerHeight
@@ -19,9 +21,11 @@ export function useScreenSize() {
window.removeEventListener('resize', onResize)
})
return size
return {
size,
isMobile,
}
}
// write a composable for detecting swipe gestures in mobile devices
export function useSwipe() {
const swipe = reactive({
initialX: null,