Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add basic pwa #110

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a241e69
feat: add basic pwa
userquin Nov 25, 2022
a55d78a
chore: replace title with titleTemplate
userquin Nov 25, 2022
862129e
fix: add setup components module
userquin Nov 25, 2022
2cd85e2
chore: allow app work offline
userquin Nov 26, 2022
93f6c30
chore: add online check and error handling
userquin Nov 26, 2022
c2996e6
chore: remove fallback page, we've now error.vue
userquin Nov 26, 2022
852ec77
chore: update sw logic and configuration
userquin Nov 26, 2022
2a16246
Merge branch 'main' into userquin/feat-add-pwa
userquin Nov 26, 2022
1643c4d
chore: remove error plugin
userquin Nov 26, 2022
06365a9
Merge branch 'main' into userquin/feat-add-pwa
userquin Nov 26, 2022
911d5c2
chore: add 200 page
userquin Nov 26, 2022
c222935
chore: add cross-env to change port for local build + start (Windows)
userquin Nov 26, 2022
2a5df00
chore: add `isCI` logic for local build + start
userquin Nov 26, 2022
05595e1
Merge branch 'main' into userquin/feat-add-pwa
userquin Nov 26, 2022
ff0d2b7
chore: update sw offline logic and error again
userquin Nov 26, 2022
b026e8b
Merge branch 'main' into userquin/feat-add-pwa
userquin Nov 26, 2022
7c37dc5
chore: exclude api calls from sw router
userquin Nov 26, 2022
e25e517
chore: fix styles + a11y
userquin Nov 26, 2022
5a4d97a
fix(PWAPrompt): update mobile position (#152)
zyyv Nov 27, 2022
3dad01e
Merge branch 'main' into userquin/feat-add-pwa
userquin Nov 27, 2022
27a8a31
chore: add global error handler
userquin Nov 27, 2022
d835a88
chore: add error handle hint
userquin Nov 27, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 2 additions & 13 deletions app.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,13 @@
<script setup>
import { APP_NAME } from './constants'

useHead({
titleTemplate: title => `${title ? `${title} | ` : ''}${APP_NAME}${import.meta.env.DEV ? ' (dev)' : ''}`,
link: [
{
rel: 'icon', type: 'image/svg+png', href: '/favicon.png',
},
],
})

// eslint-disable-next-line no-unused-expressions
isDark.value
useHeader()
</script>

<template>
<NuxtLoadingIndicator color="repeating-linear-gradient(to right,var(--c-primary) 0%,var(--c-primary-active) 100%)" />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<PWAPrompt />
</template>

<style>
Expand Down
69 changes: 69 additions & 0 deletions components/PWAPrompt.client.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script setup lang="ts">
import { useRegisterSW } from 'virtual:pwa-register/vue'

const online = useOnline()

const {
needRefresh,
updateServiceWorker,
} = useRegisterSW({
immediate: true,
onRegisteredSW(swUrl, r) {
setInterval(async () => {
if (r?.installing)
return

if (!online.value)
return

const resp = await fetch(swUrl, {
cache: 'no-store',
headers: {
'cache': 'no-store',
'cache-control': 'no-cache',
},
})

if (resp?.status === 200)
await r!.update()
}, 60 * 60 * 1000 /* 1 hour */)
},
})
const close = async () => {
needRefresh.value = false
}
</script>

<!-- TODO: remove shadow on mobile and position it above the bottom nav -->
<template>
<div
v-if="needRefresh"
role="alertdialog"
aria-labelledby="pwa-toast-title"
aria-describedby="pwa-toast-description"
animate animate-back-in-up md:animate-back-in-right
z11
fixed
bottom-14 md:bottom-0 right-0
m-2 p-4
bg-base border="~ base"
rounded
text-left
shadow
>
<h2 id="pwa-toast-title" sr-only>
New Elk version available
</h2>
<div id="pwa-toast-message">
New version available, click on reload button to update.
</div>
<div m-t4 flex="~ colum" gap-x="4">
<button type="button" btn-solid text-sm px-2 py-1 text-center @click="updateServiceWorker()">
Reload
</button>
<button type="button" btn-outline px-2 py-1 text-sm text-center @click="close">
Close
</button>
</div>
</div>
</template>
61 changes: 61 additions & 0 deletions composables/header.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { pwaInfo } from 'virtual:pwa-info'
import type { Head } from '@unhead/schema'
import { APP_NAME } from '~/constants'

export function useHeader() {
const head: Head = {
titleTemplate: title => `${title ? `${title} | ` : ''}${APP_NAME}${import.meta.env.DEV ? ' (dev)' : ''}`,
link: [
{
rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg',
},
{
rel: 'alternate icon', type: 'image/x-icon', href: '/favicon.ico',
},
{
rel: 'icon', type: 'image/png', href: '/favicon-16x16.png', sizes: '16x16x',
},
{
rel: 'icon', type: 'image/png', href: '/favicon-32x32.png', sizes: '32x32',
},
],
}

if (pwaInfo && pwaInfo.webManifest) {
head.meta = [
{ name: 'theme-color', content: '#ffffff' },
]
head.link!.push({
rel: 'mask-icon',
href: '/safari-pinned-tab.svg',
color: '#ffffff',
})
head.link!.push({
rel: 'apple-touch-icon',
href: '/apple-touch-icon.png',
sizes: '180x180',
})
const { webManifest } = pwaInfo
if (webManifest) {
const { href, useCredentials } = webManifest
if (useCredentials) {
head.link!.push({
rel: 'manifest',
href,
crossorigin: 'use-credentials',
})
}
else {
head.link!.push({
rel: 'manifest',
href,
})
}
}
}

useHead(head)

// eslint-disable-next-line no-unused-expressions
isDark.value
}
8 changes: 6 additions & 2 deletions composables/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DEFAULT_POST_CHARS_LIMIT, DEFAULT_SERVER, STORAGE_KEY_CURRENT_USER, STO
const users = useLocalStorage<UserLogin[]>(STORAGE_KEY_USERS, [], { deep: true })
const servers = useLocalStorage<Record<string, Instance>>(STORAGE_KEY_SERVERS, {}, { deep: true })
const currentUserId = useLocalStorage<string>(STORAGE_KEY_CURRENT_USER, '')
const online = useOnline()

export const currentUser = computed<UserLogin | undefined>(() => {
let user: UserLogin | undefined
Expand All @@ -33,10 +34,13 @@ export async function loginTo(user: UserLogin & { account?: AccountCredentials }
if (currentUserId.value === existing.account?.id)
return null
currentUserId.value = user.account?.id
await reloadPage()
return true
online.value && await reloadPage()
return online.value
}

if (!online.value)
return false

const masto = await loginMasto({
url: `https://${user.server}`,
accessToken: user.token,
Expand Down
42 changes: 42 additions & 0 deletions error.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script setup lang="ts">
useHeader()

const errorCodes: Record<number, string> = {
404: 'Oops! Page not found',
500: 'Oops! Something went wrong',
}
</script>

<template>
<NuxtLoadingIndicator color="repeating-linear-gradient(to right,var(--c-primary) 0%,var(--c-primary-active) 100%)" />
<NuxtLayout>
<MainContent>
<template #title>
<span text-lg font-bold>Error</span>
</template>
<slot>
<div p5 op50>
Upps, something went wrong.
<!-- TODO: add Retry button and handle status codes -->
</div>
</slot>
</MainContent>
</NuxtLayout>
<PWAPrompt />
</template>

<style>
html, body , #__nuxt{
height: 100vh;
margin: 0;
padding: 0;
}

html.dark {
color-scheme: dark;
}

html {
--at-apply: bg-base text-base;
}
</style>
69 changes: 69 additions & 0 deletions modules/pwa/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { Nuxt } from '@nuxt/schema'
import type { VitePWAOptions } from 'vite-plugin-pwa'
import { resolve } from 'pathe'

export function configurePWAOptions(options: Partial<VitePWAOptions>, nuxt: Nuxt) {
if (!options.outDir) {
const publicDir = nuxt.options.nitro?.output?.publicDir
options.outDir = publicDir ? resolve(publicDir) : resolve(nuxt.options.buildDir, '../.output/public')
}

let config: Partial<
import('workbox-build').BasePartial
& import('workbox-build').GlobPartial
& import('workbox-build').RequiredGlobDirectoryPartial
>

if (options.strategies === 'injectManifest') {
options.injectManifest = options.injectManifest ?? {}
config = options.injectManifest
}
else {
options.workbox = options.workbox ?? {}
if (options.registerType === 'autoUpdate' && (options.injectRegister === 'script' || options.injectRegister === 'inline')) {
options.workbox.clientsClaim = true
options.workbox.skipWaiting = true
}
if (nuxt.options.dev) {
// on dev force always to use the root

options.workbox.navigateFallback = nuxt.options.app.baseURL ?? '/'
if (options.devOptions?.enabled && !options.devOptions.navigateFallbackAllowlist)
options.devOptions.navigateFallbackAllowlist = [new RegExp(nuxt.options.app.baseURL) ?? /\//]
}
config = options.workbox
// todo: change navigateFallback based on the command: use 404 only when using generate
/* else if (nuxt.options.build) {
if (!options.workbox.navigateFallback)
options.workbox.navigateFallback = '/200.html'
} */
}
if (!nuxt.options.dev)
config.manifestTransforms = [createManifestTransform(nuxt.options.app.baseURL ?? '/')]
}

function createManifestTransform(base: string): import('workbox-build').ManifestTransform {
return async (entries) => {
// prefix non html assets with base
/*
entries.filter(e => e && !e.url.endsWith('.html')).forEach((e) => {
if (!e.url.startsWith(base))
e.url = `${base}${e.url}`
})
*/
entries.filter(e => e && e.url.endsWith('.html')).forEach((e) => {
const url = e.url.startsWith('/') ? e.url.slice(1) : e.url
if (url === 'index.html') {
e.url = base
}
else {
const parts = url.split('/')
parts[parts.length - 1] = parts[parts.length - 1].replace(/\.html$/, '')
// e.url = `${base}${parts.length > 1 ? parts.slice(0, parts.length - 1).join('/') : parts[0]}`
e.url = parts.length > 1 ? parts.slice(0, parts.length - 1).join('/') : parts[0]
}
})

return { manifest: entries, warnings: [] }
}
}
Loading