injectManifest, a new typed SW source, runtime caching, and a non‑blocking update toast with an action button. The
guest shell now links a dedicated manifest and theme color, and background upload sync is managed in a single
PwaManager component.
Key changes (where/why)
- vite.config.ts: added VitePWA injectManifest config, guest manifest, and output to /public so the SW can control /
scope.
- resources/js/guest/guest-sw.ts: new Workbox SW (precache + runtime caching for guest navigation, GET /api/v1/*,
images, fonts) and preserves push/sync/notification logic.
- resources/js/guest/components/PwaManager.tsx: registers SW, shows update/offline toasts, and processes the upload
queue on sync/online.
- resources/js/guest/components/ToastHost.tsx: action-capable toasts so update prompts can include a CTA.
- resources/js/guest/i18n/messages.ts: added common.updateAvailable, common.updateAction, common.offlineReady.
- resources/views/guest.blade.php: manifest + theme color + apple touch icon.
- .gitignore: ignore generated public/guest-sw.js and public/guest.webmanifest; public/guest-sw.js removed since it’s
now build output.
200 lines
6.3 KiB
TypeScript
200 lines
6.3 KiB
TypeScript
import { wayfinder } from '@laravel/vite-plugin-wayfinder';
|
||
import tailwindcss from '@tailwindcss/vite';
|
||
import react from '@vitejs/plugin-react';
|
||
import laravel from 'laravel-vite-plugin';
|
||
import { defineConfig, type PluginOption } from 'vite';
|
||
import path from 'path';
|
||
import { tamaguiPlugin } from '@tamagui/vite-plugin';
|
||
import { sentryVitePlugin } from '@sentry/vite-plugin';
|
||
import { VitePWA } from 'vite-plugin-pwa';
|
||
|
||
const devServerHost = process.env.VITE_DEV_SERVER_HOST ?? 'fotospiel-app.test';
|
||
const devServerPort = Number.parseInt(process.env.VITE_DEV_SERVER_PORT ?? '5173', 10);
|
||
const devServerOrigin = process.env.VITE_DEV_SERVER_URL ?? `http://fotospiel-app.test:${devServerPort}`;
|
||
const parsedOrigin = new URL(devServerOrigin);
|
||
const hmrPort = parsedOrigin.port === '' ? devServerPort : Number.parseInt(parsedOrigin.port, 10);
|
||
const appUrl = process.env.APP_URL ?? 'http://fotospiel-app.test';
|
||
const sentryEnabled = Boolean(
|
||
process.env.SENTRY_AUTH_TOKEN &&
|
||
process.env.SENTRY_ORG &&
|
||
process.env.SENTRY_PROJECT &&
|
||
process.env.SENTRY_URL
|
||
);
|
||
|
||
const plugins: PluginOption[] = [
|
||
laravel({
|
||
input: ['resources/css/app.css','resources/js/app.js', 'resources/js/app.tsx', 'resources/js/guest/main.tsx', 'resources/js/admin/main.tsx'],
|
||
ssr: 'resources/js/ssr.tsx',
|
||
refresh: [
|
||
'resources/views/**/*.blade.php',
|
||
'resources/lang/**/*.php',
|
||
'app/Http/Livewire/**', // falls genutzt
|
||
// NICHT beobachten: storage/logs, vendor, public/build, etc.
|
||
],
|
||
}),
|
||
react(),
|
||
tailwindcss(),
|
||
wayfinder({
|
||
formVariants: true,
|
||
}),
|
||
VitePWA({
|
||
strategies: 'injectManifest',
|
||
srcDir: 'resources/js/guest',
|
||
filename: 'guest-sw.ts',
|
||
manifestFilename: 'guest.webmanifest',
|
||
outDir: 'public',
|
||
injectRegister: null,
|
||
registerType: 'prompt',
|
||
includeAssets: [
|
||
'favicon.ico',
|
||
'favicon.svg',
|
||
'apple-touch-icon.png',
|
||
'logo-transparent-md.png',
|
||
'logo-transparent-lg.png',
|
||
],
|
||
injectManifest: {
|
||
globDirectory: 'public/build',
|
||
globPatterns: ['**/*.{js,css,woff,woff2,svg,png,webp,ico,txt}'],
|
||
},
|
||
manifest: {
|
||
name: 'Fotospiel',
|
||
short_name: 'Fotospiel',
|
||
id: '/event',
|
||
start_url: '/event',
|
||
scope: '/',
|
||
display: 'standalone',
|
||
lang: 'de-DE',
|
||
description: 'Offline-fähige Event-Galerie für Gäste – Fotos aufnehmen, Aufgaben lösen und teilen.',
|
||
background_color: '#0f172a',
|
||
theme_color: '#ec4899',
|
||
orientation: 'portrait',
|
||
categories: ['photo-video', 'social'],
|
||
icons: [
|
||
{
|
||
src: '/favicon.svg',
|
||
sizes: 'any',
|
||
type: 'image/svg+xml',
|
||
purpose: 'any',
|
||
},
|
||
{
|
||
src: '/apple-touch-icon.png',
|
||
sizes: '166x169',
|
||
type: 'image/png',
|
||
purpose: 'any',
|
||
},
|
||
{
|
||
src: '/logo-transparent-lg.png',
|
||
sizes: '698x684',
|
||
type: 'image/png',
|
||
purpose: 'any',
|
||
},
|
||
],
|
||
},
|
||
devOptions: {
|
||
enabled: false,
|
||
},
|
||
}),
|
||
tamaguiPlugin({
|
||
config: './tamagui.config.ts',
|
||
components: ['@tamagui/core', '@tamagui/stacks', '@tamagui/text', '@tamagui/button'],
|
||
optimize: false,
|
||
disableExtraction: true,
|
||
}),
|
||
];
|
||
|
||
if (sentryEnabled) {
|
||
plugins.push(
|
||
sentryVitePlugin({
|
||
org: process.env.SENTRY_ORG as string,
|
||
project: process.env.SENTRY_PROJECT as string,
|
||
authToken: process.env.SENTRY_AUTH_TOKEN as string,
|
||
url: process.env.SENTRY_URL as string,
|
||
release: process.env.SENTRY_RELEASE ?? process.env.VITE_SENTRY_RELEASE,
|
||
telemetry: false,
|
||
})
|
||
);
|
||
}
|
||
|
||
export default defineConfig({
|
||
server: {
|
||
host: devServerHost,
|
||
port: devServerPort,
|
||
strictPort: true,
|
||
origin: devServerOrigin,
|
||
hmr: {
|
||
host: parsedOrigin.hostname,
|
||
protocol: parsedOrigin.protocol.replace(':','') as 'http' | 'https',
|
||
clientPort: hmrPort,
|
||
},
|
||
fs: {
|
||
strict: true,
|
||
// Erlaube nur das App-Package (ggf. Pfade anpassen)
|
||
allow: [__dirname],
|
||
},
|
||
cors: {
|
||
origin: appUrl,
|
||
credentials: true,
|
||
},
|
||
watch: {
|
||
// WENIGER ist mehr: Alles ausklammern, was nicht für HMR nötig ist
|
||
ignored: [
|
||
'**/node_modules/**',
|
||
'**/.git/**',
|
||
'**/dist/**',
|
||
'**/build/**',
|
||
'**/.next/**',
|
||
'**/coverage/**',
|
||
'**/.cache/**',
|
||
// Laravel-spezifisch
|
||
'**/public/build/**',
|
||
'**/storage/**',
|
||
'**/vendor/**',
|
||
'**/bootstrap/cache/**',
|
||
// Monorepo-Nachbarn
|
||
'../**/node_modules/**',
|
||
'../**/dist/**',
|
||
'../**/build/**',
|
||
'../**/coverage/**',
|
||
],
|
||
// Falls ihr auf gemounteten FS seid und Events fehlen:
|
||
// usePolling: true, interval: 500,
|
||
},
|
||
proxy: {
|
||
'/fonts': {
|
||
target: appUrl,
|
||
changeOrigin: true,
|
||
},
|
||
},
|
||
},
|
||
plugins,
|
||
esbuild: {
|
||
jsx: 'automatic',
|
||
},
|
||
optimizeDeps: {
|
||
// Bei großen Monorepos hilfreich:
|
||
entries: ['resources/js/**/*'],
|
||
exclude: [
|
||
// füge notfalls große/selten genutzte Pakete hinzu
|
||
],
|
||
include: [
|
||
'react-native-web',
|
||
'@tamagui/core',
|
||
'@tamagui/stacks',
|
||
'@tamagui/text',
|
||
'@tamagui/button',
|
||
],
|
||
},
|
||
define: {
|
||
'process.env.TAMAGUI_TARGET': JSON.stringify('web'),
|
||
},
|
||
|
||
// Build-Optionen wirken vor allem bei `vite build`, schaden aber nicht:
|
||
build: {
|
||
sourcemap: sentryEnabled,
|
||
target: 'es2020',
|
||
rollupOptions: {
|
||
// keine externen Monster-Globs
|
||
},
|
||
},
|
||
});
|