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

1
frontend/.gitignore vendored
View File

@@ -2,4 +2,5 @@ node_modules
.DS_Store
dist
dist-ssr
dev-dist
*.local

View File

@@ -67,6 +67,7 @@ declare module 'vue' {
IconPicker: typeof import('./src/components/Controls/IconPicker.vue')['default']
IndicatorIcon: typeof import('./src/components/Icons/IndicatorIcon.vue')['default']
InlineLessonMenu: typeof import('./src/components/Notes/InlineLessonMenu.vue')['default']
InstallPrompt: typeof import('./src/components/InstallPrompt.vue')['default']
InviteIcon: typeof import('./src/components/Icons/InviteIcon.vue')['default']
JobApplicationModal: typeof import('./src/components/Modals/JobApplicationModal.vue')['default']
JobCard: typeof import('./src/components/JobCard.vue')['default']

View File

@@ -3,6 +3,202 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" href="{{ favicon }}" />
<link rel="apple-touch-icon" href="public/manifest/apple-icon-180.png" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#0F0F0F" media="(prefers-color-scheme: dark)" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="msapplication-navbutton-color" content="#ffffff" />
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2048-2732.jpg"
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2732-2048.jpg"
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1668-2388.jpg"
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2388-1668.jpg"
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1536-2048.jpg"
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2048-1536.jpg"
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1640-2360.jpg"
media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2360-1640.jpg"
media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1668-2224.jpg"
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2224-1668.jpg"
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1620-2160.jpg"
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2160-1620.jpg"
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1488-2266.jpg"
media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2266-1488.jpg"
media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1320-2868.jpg"
media="(device-width: 440px) and (device-height: 956px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2868-1320.jpg"
media="(device-width: 440px) and (device-height: 956px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1206-2622.jpg"
media="(device-width: 402px) and (device-height: 874px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2622-1206.jpg"
media="(device-width: 402px) and (device-height: 874px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1290-2796.jpg"
media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2796-1290.jpg"
media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1179-2556.jpg"
media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2556-1179.jpg"
media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1170-2532.jpg"
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2532-1170.jpg"
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1284-2778.jpg"
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2778-1284.jpg"
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1125-2436.jpg"
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2436-1125.jpg"
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1242-2688.jpg"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2688-1242.jpg"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-828-1792.jpg"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1792-828.jpg"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1242-2208.jpg"
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2208-1242.jpg"
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-750-1334.jpg"
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1334-750.jpg"
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-640-1136.jpg"
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1136-640.jpg"
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ title }}</title>
<meta name="title" content="{{ meta.title }}" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

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,

View File

@@ -2,6 +2,7 @@ import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import frappeui from 'frappe-ui/vite'
import { VitePWA } from 'vite-plugin-pwa'
// https://vitejs.dev/config/
export default defineConfig({
@@ -23,6 +24,40 @@ export default defineConfig({
propsDestructure: true,
},
}),
VitePWA({
registerType: 'autoUpdate',
devOptions: {
enabled: true,
},
workbox: {
cleanupOutdatedCaches: true,
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
},
manifest: {
display: 'standalone',
name: 'Frappe Learning',
short_name: 'Frappe Learning',
start_url: '/lms',
description:
'Easy to use, 100% open source Learning Management System',
theme_color: '#0f7159',
background_color: '#ffffff',
icons: [
{
src: '/assets/lms/frontend/manifest/manifest-icon-192.maskable.png',
sizes: '192x192',
type: 'image/png',
purpose: 'maskable any',
},
{
src: '/assets/lms/frontend/manifest/manifest-icon-512.maskable.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable any',
},
],
},
}),
],
server: {
host: '0.0.0.0', // Accept connections from any network interface