once load splash
This commit is contained in:
+14
-1
@@ -17,7 +17,20 @@ const nextConfig: NextConfig = {
|
|||||||
},
|
},
|
||||||
headers: async () => [
|
headers: async () => [
|
||||||
{
|
{
|
||||||
source: "/:path*",
|
source: "/sw.js",
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: "Cache-Control",
|
||||||
|
value: "public, max-age=0, must-revalidate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Service-Worker-Allowed",
|
||||||
|
value: "/",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/((?!sw\\.js|api/pwa-icon).*)",
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
key: "Cache-Control",
|
key: "Cache-Control",
|
||||||
|
|||||||
+14
-2
@@ -23,16 +23,28 @@
|
|||||||
},
|
},
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/icons/web-app-manifest-192x192.png",
|
"src": "/api/pwa-icon/192",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"purpose": "any"
|
"purpose": "any"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/icons/web-app-manifest-512x512.png",
|
"src": "/api/pwa-icon/512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"purpose": "any"
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/api/pwa-icon/192",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/api/pwa-icon/512",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"purpose": "maskable"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"categories": [
|
"categories": [
|
||||||
|
|||||||
+11
-1
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { fetchWithTimeout } from "@/lib/helpers/fetchWithTimeout";
|
import { fetchWithTimeout } from "@/lib/helpers/fetchWithTimeout";
|
||||||
|
import { PWA_ICON_192 } from "@/lib/helpers/pwaIcons";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
|
|
||||||
@@ -7,7 +8,7 @@ export const dynamic = "force-dynamic";
|
|||||||
export const revalidate = 0;
|
export const revalidate = 0;
|
||||||
|
|
||||||
const FETCH_TIMEOUT_MS = 5000;
|
const FETCH_TIMEOUT_MS = 5000;
|
||||||
const DEFAULT_ICON = "/icons/web-app-manifest-192x192.png";
|
const DEFAULT_ICON = PWA_ICON_192;
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getRestaurant } from './lib/getRestaurant'
|
|||||||
import { notFound } from 'next/navigation'
|
import { notFound } from 'next/navigation'
|
||||||
import CartChecker from '@/components/CartChecker'
|
import CartChecker from '@/components/CartChecker'
|
||||||
import ActiveChecker from '@/components/ActiveChecker'
|
import ActiveChecker from '@/components/ActiveChecker'
|
||||||
|
import { PWA_ICON_192, PWA_ICON_512 } from '@/lib/helpers/pwaIcons'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
export const revalidate = 0
|
export const revalidate = 0
|
||||||
@@ -17,8 +18,8 @@ export async function generateMetadata({
|
|||||||
params: Promise<LayoutParams>
|
params: Promise<LayoutParams>
|
||||||
}): Promise<Metadata> {
|
}): Promise<Metadata> {
|
||||||
const { name } = await params
|
const { name } = await params
|
||||||
const defaultIcon = "/icons/web-app-manifest-192x192.png"
|
const defaultIcon = PWA_ICON_192
|
||||||
const defaultIcon512 = "/icons/web-app-manifest-512x512.png"
|
const defaultIcon512 = PWA_ICON_512
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const restaurant = await getRestaurant(name)
|
const restaurant = await getRestaurant(name)
|
||||||
@@ -56,12 +57,12 @@ export async function generateMetadata({
|
|||||||
manifest: `/${name}/manifest.webmanifest`,
|
manifest: `/${name}/manifest.webmanifest`,
|
||||||
icons: {
|
icons: {
|
||||||
icon: [
|
icon: [
|
||||||
{ url: "/icons/web-app-manifest-192x192.png" },
|
{ url: PWA_ICON_192 },
|
||||||
{ url: "/icons/web-app-manifest-192x192.png", sizes: "192x192", type: "image/png" },
|
{ url: PWA_ICON_192, sizes: "192x192", type: "image/png" },
|
||||||
{ url: "/icons/web-app-manifest-512x512.png", sizes: "512x512", type: "image/png" },
|
{ url: PWA_ICON_512, sizes: "512x512", type: "image/png" },
|
||||||
],
|
],
|
||||||
shortcut: "/icons/web-app-manifest-192x192.png",
|
shortcut: PWA_ICON_192,
|
||||||
apple: "/icons/web-app-manifest-192x192.png",
|
apple: PWA_ICON_192,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,18 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
import { buildPwaManifestIcons } from "@/lib/helpers/pwaIcons";
|
||||||
import { getRestaurant } from "../lib/getRestaurant";
|
import { getRestaurant } from "../lib/getRestaurant";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
export const revalidate = 0;
|
export const revalidate = 0;
|
||||||
|
|
||||||
function buildManifestIcons(name: string, logo?: string | null) {
|
function buildManifestIcons(name: string, logo?: string | null) {
|
||||||
const default192 = "/icons/web-app-manifest-192x192.png";
|
|
||||||
const default512 = "/icons/web-app-manifest-512x512.png";
|
|
||||||
|
|
||||||
if (logo && logo.trim() !== "") {
|
if (logo && logo.trim() !== "") {
|
||||||
const logo192 = `/${name}/api/logo?url=${encodeURIComponent(logo)}&size=192`;
|
const logo192 = `/${name}/api/logo?url=${encodeURIComponent(logo)}&size=192`;
|
||||||
const logo512 = `/${name}/api/logo?url=${encodeURIComponent(logo)}&size=512`;
|
const logo512 = `/${name}/api/logo?url=${encodeURIComponent(logo)}&size=512`;
|
||||||
|
return buildPwaManifestIcons(logo192, logo512);
|
||||||
return [
|
|
||||||
{
|
|
||||||
src: logo192,
|
|
||||||
sizes: "192x192",
|
|
||||||
type: "image/png",
|
|
||||||
purpose: "any",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: logo512,
|
|
||||||
sizes: "512x512",
|
|
||||||
type: "image/png",
|
|
||||||
purpose: "any",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return buildPwaManifestIcons();
|
||||||
{
|
|
||||||
src: default192,
|
|
||||||
sizes: "192x192",
|
|
||||||
type: "image/png",
|
|
||||||
purpose: "any",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: default512,
|
|
||||||
sizes: "512x512",
|
|
||||||
type: "image/png",
|
|
||||||
purpose: "any",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { readFile } from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const VALID_SIZES = [192, 512] as const;
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
_request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ size: string }> },
|
||||||
|
) {
|
||||||
|
const { size: sizeParam } = await params;
|
||||||
|
const size = parseInt(sizeParam, 10);
|
||||||
|
|
||||||
|
if (!VALID_SIZES.includes(size as (typeof VALID_SIZES)[number])) {
|
||||||
|
return new NextResponse('Invalid size', { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const svgPath = path.join(process.cwd(), 'public', 'icon0.svg');
|
||||||
|
const svgBuffer = await readFile(svgPath);
|
||||||
|
const pngBuffer = await sharp(svgBuffer).resize(size, size).png().toBuffer();
|
||||||
|
|
||||||
|
return new NextResponse(new Uint8Array(pngBuffer), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'image/png',
|
||||||
|
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating PWA icon:', error);
|
||||||
|
return new NextResponse('Failed to generate icon', { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -7,11 +7,11 @@ import initTranslations from '@/lib/i18n';
|
|||||||
import TranslationsProvider from "@/components/utils/TranslationsProdiver";
|
import TranslationsProvider from "@/components/utils/TranslationsProdiver";
|
||||||
import ToastContainer from "@/components/Toast";
|
import ToastContainer from "@/components/Toast";
|
||||||
import { ThemeColorSetter } from "@/components/ThemeColorSetter";
|
import { ThemeColorSetter } from "@/components/ThemeColorSetter";
|
||||||
|
import PwaServiceWorker from "@/components/PwaServiceWorker";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
description: 'Webapp dashboard',
|
description: 'Webapp dashboard',
|
||||||
manifest: '/manifest.json',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const viewport: Viewport = {
|
export const viewport: Viewport = {
|
||||||
@@ -54,6 +54,7 @@ export default async function RootLayout({
|
|||||||
locale={locale}
|
locale={locale}
|
||||||
resources={resources}>
|
resources={resources}>
|
||||||
<ThemeColorSetter />
|
<ThemeColorSetter />
|
||||||
|
<PwaServiceWorker />
|
||||||
<div id="root" className="h-svh overflow-hidden">
|
<div id="root" className="h-svh overflow-hidden">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function PwaServiceWorker() {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!('serviceWorker' in navigator)) return;
|
||||||
|
|
||||||
|
navigator.serviceWorker.register('/sw.js', { scope: '/' }).catch(() => {
|
||||||
|
// Registration can fail on unsupported contexts (e.g. in-app browsers)
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -69,7 +69,16 @@ const isIOS = () => {
|
|||||||
|
|
||||||
const isStandalone = () => {
|
const isStandalone = () => {
|
||||||
if (typeof window === 'undefined') return false;
|
if (typeof window === 'undefined') return false;
|
||||||
return ('standalone' in window.navigator) && (window.navigator as { standalone?: boolean }).standalone === true;
|
|
||||||
|
const isIOSStandalone =
|
||||||
|
('standalone' in window.navigator) &&
|
||||||
|
(window.navigator as { standalone?: boolean }).standalone === true;
|
||||||
|
|
||||||
|
const isDisplayModeStandalone =
|
||||||
|
window.matchMedia('(display-mode: standalone)').matches ||
|
||||||
|
window.matchMedia('(display-mode: fullscreen)').matches;
|
||||||
|
|
||||||
|
return isIOSStandalone || isDisplayModeStandalone;
|
||||||
};
|
};
|
||||||
|
|
||||||
function SideMenu({ menuState, toggleMenuState, nightModeState, togglenightModeState }: Props) {
|
function SideMenu({ menuState, toggleMenuState, nightModeState, togglenightModeState }: Props) {
|
||||||
@@ -114,18 +123,23 @@ function SideMenu({ menuState, toggleMenuState, nightModeState, togglenightModeS
|
|||||||
setIsIOSDevice(ios);
|
setIsIOSDevice(ios);
|
||||||
setIsPWAInstalled(standalone);
|
setIsPWAInstalled(standalone);
|
||||||
|
|
||||||
if (!ios) {
|
|
||||||
const handleBeforeInstallPrompt = (e: Event) => {
|
const handleBeforeInstallPrompt = (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setDeferredPrompt(e as BeforeInstallPromptEvent);
|
setDeferredPrompt(e as BeforeInstallPromptEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAppInstalled = () => {
|
||||||
|
setDeferredPrompt(null);
|
||||||
|
setIsPWAInstalled(true);
|
||||||
|
};
|
||||||
|
|
||||||
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||||||
|
window.addEventListener('appinstalled', handleAppInstalled);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||||||
|
window.removeEventListener('appinstalled', handleAppInstalled);
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleInstallPWA = async () => {
|
const handleInstallPWA = async () => {
|
||||||
|
|||||||
@@ -2,10 +2,19 @@
|
|||||||
|
|
||||||
import usePreference from '@/hooks/helpers/usePreference';
|
import usePreference from '@/hooks/helpers/usePreference';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
import { useGetAbout } from '@/app/[name]/(Main)/about/hooks/useAboutData';
|
import { useGetAbout } from '@/app/[name]/(Main)/about/hooks/useAboutData';
|
||||||
import SplashScreen from '@/components/overlays/SplashScreen';
|
import SplashScreen from '@/components/overlays/SplashScreen';
|
||||||
import { hexToOklch, calculateBrightness } from '@/lib/helpers/colorUtils';
|
import { hexToOklch, calculateBrightness } from '@/lib/helpers/colorUtils';
|
||||||
|
|
||||||
|
function getSplashStorageKey(restaurantSlug: string) {
|
||||||
|
return `splash-seen:${restaurantSlug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSplashFingerprint(logo?: string | null, name?: string | null) {
|
||||||
|
return `${logo ?? ''}|${name ?? ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
@@ -24,6 +33,9 @@ function applyPrimaryColor(colorHex: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PreferenceWrapper({ children }: Props) {
|
function PreferenceWrapper({ children }: Props) {
|
||||||
|
const params = useParams();
|
||||||
|
const restaurantSlug = typeof params.name === 'string' ? params.name : '';
|
||||||
|
|
||||||
const { state: nightMode } = usePreference('night-mode', false);
|
const { state: nightMode } = usePreference('night-mode', false);
|
||||||
const { data: aboutData } = useGetAbout();
|
const { data: aboutData } = useGetAbout();
|
||||||
|
|
||||||
@@ -34,7 +46,14 @@ function PreferenceWrapper({ children }: Props) {
|
|||||||
|
|
||||||
const handleSplashDismiss = useCallback(() => {
|
const handleSplashDismiss = useCallback(() => {
|
||||||
setShowSplash(false);
|
setShowSplash(false);
|
||||||
}, []);
|
|
||||||
|
if (!restaurantSlug) return;
|
||||||
|
|
||||||
|
const logo = aboutData?.data?.logo || cachedLogo;
|
||||||
|
const name = aboutData?.data?.name || cachedName;
|
||||||
|
const fingerprint = getSplashFingerprint(logo, name);
|
||||||
|
localStorage.setItem(getSplashStorageKey(restaurantSlug), fingerprint);
|
||||||
|
}, [aboutData?.data?.logo, aboutData?.data?.name, cachedLogo, cachedName, restaurantSlug]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
@@ -49,16 +68,15 @@ function PreferenceWrapper({ children }: Props) {
|
|||||||
if (savedLogo) setCachedLogo(savedLogo);
|
if (savedLogo) setCachedLogo(savedLogo);
|
||||||
if (savedName) setCachedName(savedName);
|
if (savedName) setCachedName(savedName);
|
||||||
|
|
||||||
const navigationEntries = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[];
|
if (!restaurantSlug) return;
|
||||||
const isReload = navigationEntries.length > 0 && navigationEntries[0].type === 'reload';
|
|
||||||
const isFirstLoadInSession = !sessionStorage.getItem('app-loaded') || isReload;
|
|
||||||
|
|
||||||
if (isFirstLoadInSession) {
|
const currentFingerprint = getSplashFingerprint(savedLogo, savedName);
|
||||||
|
const seenFingerprint = localStorage.getItem(getSplashStorageKey(restaurantSlug));
|
||||||
|
|
||||||
|
if (seenFingerprint !== currentFingerprint) {
|
||||||
setShowSplash(true);
|
setShowSplash(true);
|
||||||
}
|
}
|
||||||
|
}, [restaurantSlug]);
|
||||||
sessionStorage.setItem('app-loaded', 'true');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (aboutData?.data?.menuColor) {
|
if (aboutData?.data?.menuColor) {
|
||||||
@@ -76,6 +94,19 @@ function PreferenceWrapper({ children }: Props) {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [aboutData?.data?.menuColor]);
|
}, [aboutData?.data?.menuColor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!restaurantSlug || showSplash) return;
|
||||||
|
|
||||||
|
const logo = aboutData?.data?.logo || cachedLogo;
|
||||||
|
const name = aboutData?.data?.name || cachedName;
|
||||||
|
const fingerprint = getSplashFingerprint(logo, name);
|
||||||
|
const seenFingerprint = localStorage.getItem(getSplashStorageKey(restaurantSlug));
|
||||||
|
|
||||||
|
if (seenFingerprint === '|' && fingerprint !== '|') {
|
||||||
|
localStorage.setItem(getSplashStorageKey(restaurantSlug), fingerprint);
|
||||||
|
}
|
||||||
|
}, [aboutData?.data?.logo, aboutData?.data?.name, cachedLogo, cachedName, restaurantSlug, showSplash]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.setAttribute('data-theme', nightMode ? 'dark' : 'light');
|
document.documentElement.setAttribute('data-theme', nightMode ? 'dark' : 'light');
|
||||||
}, [nightMode]);
|
}, [nightMode]);
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
export const PWA_ICON_192 = '/api/pwa-icon/192';
|
||||||
|
export const PWA_ICON_512 = '/api/pwa-icon/512';
|
||||||
|
|
||||||
|
export function buildPwaManifestIcons(logo192?: string, logo512?: string) {
|
||||||
|
const icon192 = logo192 ?? PWA_ICON_192;
|
||||||
|
const icon512 = logo512 ?? PWA_ICON_512;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
src: icon192,
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: icon512,
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: icon192,
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'maskable',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: icon512,
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'maskable',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user