Skip to content

Commit

Permalink
chore(pcomparator): add pwa manifest (#79)
Browse files Browse the repository at this point in the history
* chore(pcomparator): add pwa manifest

* test transparent status bar

* set background color

* add dark theme manifest

* add react-use-pwa to customize install prompt

* add custom pwa prompt for ios

* add missing translations
  • Loading branch information
Clement-Muth authored Nov 17, 2024
1 parent fcb2f2b commit bd30e90
Show file tree
Hide file tree
Showing 19 changed files with 361 additions and 18 deletions.
Binary file not shown.
Binary file not shown.
3 changes: 2 additions & 1 deletion pcomparator/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const nextConfig = (): NextConfig => {

const withPWA = withPWAInit({
dest: "public",
disable: false
});

module.exports = nextConfig();
module.exports = withPWA(nextConfig());
4 changes: 3 additions & 1 deletion pcomparator/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pcomparator",
"version": "1.0.0",
"version": "4.8.0",
"private": true,
"scripts": {
"dev": "next dev --turbo",
Expand All @@ -23,6 +23,7 @@
"dependencies": {
"@asteasolutions/zod-to-openapi": "^7.2.0",
"@auth/prisma-adapter": "^2.7.2",
"@dotmind/react-use-pwa": "^1.0.4",
"@imbios/next-pwa": "^1.1.1",
"@lingui/core": "^4.11.0",
"@lingui/react": "^4.11.0",
Expand All @@ -49,6 +50,7 @@
"kysely": "^0.27.4",
"lodash": "^4.17.21",
"lucide-react": "^0.453.0",
"moment": "^2.30.1",
"next": "15.0.0",
"next-auth": "beta",
"next-themes": "^0.3.0",
Expand Down
2 changes: 1 addition & 1 deletion pcomparator/public/sw.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pcomparator/src/app/[locale]/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

div[data-overlay-container="true"] {
@apply flex flex-col flex-1;
@apply min-h-dvh w-full bg-gradient-to-br dark:from-[#1f121b] dark:via-[#0c1820] dark:via-80% dark:to-[#081917] from-indigo-50 via-white to-primary-200;
@apply min-h-dvh w-full bg-gradient-to-b dark:from-[#1f121b] dark:via-[#0c1820] dark:via-80% dark:to-[#081917] from-indigo-50 via-white to-primary-200;
}

.PhoneInput {
Expand Down
6 changes: 5 additions & 1 deletion pcomparator/src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { pcomparatorMetadata } from "~/core/metadata";
import { type NextPageProps, withLinguiLayout } from "~/core/withLinguiLayout";
import "react-toastify/dist/ReactToastify.css";
import "./globals.css";
import { InstallPWA } from "~/core/pwa/Install";

const inter = Inter({ subsets: ["latin"] });

Expand All @@ -19,7 +20,10 @@ const RootLayout = ({ children, locale }: NextPageProps) => {
<html lang={locale} suppressHydrationWarning>
<body className={inter.className}>
<ApplicationKernel locale={locale}>
<ApplicationLayout>{children}</ApplicationLayout>
<ApplicationLayout>
<InstallPWA />
{children}
</ApplicationLayout>
</ApplicationKernel>
</body>
</html>
Expand Down
24 changes: 17 additions & 7 deletions pcomparator/src/app/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,39 @@ import type { MetadataRoute } from "next";

export default (): MetadataRoute.Manifest => {
return {
name: "PComparator",
short_name: "PComparator",
description: "PComparator is the price comparator for foods, cosmetic and more",
name: "Daizl - Compare Prices Easily",
short_name: "Deazl",
description:
"Daizl is a web app that helps you compare prices for food, cosmetics, and more to find the best deals near you.",
start_url: "/",
display: "standalone",
background_color: "#000",
theme_color: "#000",
background_color: "#eef2ff",
theme_color: "#eef2ff",
orientation: "portrait",
dir: "ltr",
lang: "en",
id: "/",
screenshots: [
{
src: "/static/logo.png",
sizes: "512x512",
type: "image/png"
}
},
{ form_factor: "wide", src: "/static/logo.png", sizes: "512x512", type: "image/png" }
],
icons: [
{
src: "/static/logo.png",
sizes: "512x512",
type: "image/png"
}
]
],
related_applications: [
{
platform: "webapp",
url: "https://daizl.fr/manifest.webmanifest"
}
],
prefer_related_applications: true
};
};
2 changes: 1 addition & 1 deletion pcomparator/src/app/robots.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ User-Agent: *
Allow: /
Disallow: /private/

Sitemap: https://pcomparator.vercel.app/sitemap.xml
Sitemap: https://deazl.fr/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const SearchBarcode = ({ onNewProduct, onNoPrices }: SearchBarcodeProps)
onPress={onOpen}
radius="full"
variant="faded"
className="p-7 w-18 h-18 -mt-8 border-none shadow-medium"
className="p-7 w-18 h-18 -mt-8 border-none shadow-[0_5px_10px_1px_rgba(0,0,0,.2)]"
isIconOnly
/>
</>
Expand Down
2 changes: 1 addition & 1 deletion pcomparator/src/components/Tabbar/Tabbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface TabbarProps {
}

export const Tabbar = ({ mainButton }: TabbarProps) => (
<div className="flex justify-evenly py-4 border-t rounded-t-3xl border-t-transparent shadow-medium">
<div className="flex justify-evenly py-4 border-t rounded-t-3xl border-t-transparent shadow-medium bg-white dark:bg-black">
<Button as={Link} href="" startContent={<User />} variant="light" radius="full" isIconOnly />
<Button as={Link} href="" startContent={<Utensils />} variant="light" radius="full" isIconOnly />
{mainButton}
Expand Down
164 changes: 164 additions & 0 deletions pcomparator/src/core/pwa/Install.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
"use client";

import { Trans } from "@lingui/macro";
import {
Button,
Card,
CardBody,
Checkbox,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
useDisclosure
} from "@nextui-org/react";
import { ExternalLink, Info } from "lucide-react";
import dynamic from "next/dynamic";
import { useEffect } from "react";
import useIosInstallPrompt from "~/core/pwa/useIos";
import useWebInstallPrompt from "~/core/pwa/useWebInstall";

const InstallPrompt = () => {
const [iosInstallPrompt, handleIOSInstallDeclined] = useIosInstallPrompt();
const [webInstallPrompt, handleWebInstallDeclined, handleWebInstallAccepted] = useWebInstallPrompt();
const { isOpen, onClose, onOpen, onOpenChange } = useDisclosure();

useEffect(() => {
if (!iosInstallPrompt && !webInstallPrompt) return;
setTimeout(onOpen, 3000);
}, [webInstallPrompt, iosInstallPrompt]);

if (!iosInstallPrompt && !webInstallPrompt) return null;

return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent>
{iosInstallPrompt ? (
<>
<ModalHeader>
<Trans>Install Our App for a Better Experience!</Trans>
</ModalHeader>
<ModalBody>
<p>
<Trans>Get faster access, work offline, and enjoy a smoother experience.</Trans>
</p>
<Card className="mb-4">
<CardBody className="flex !flex-row gap-6 bg-yellow-200/[0.2] dark:bg-yellow-200/[0.06]">
<Info color="#e3c84b" size="22px" />
<div className="flex-1">
<span className="text-small">
<b>App can not be automatically installed on Ios</b>
</span>
<span className="text-small flex items-center gap-x-1">
Tap
<ExternalLink />
then &quot;Add to Home Screen&quot;
</span>
</div>
</CardBody>
</Card>
<ul>
<li>
<Checkbox isSelected readOnly>
Instant access with one tap
</Checkbox>
</li>
<li>
<Checkbox isSelected readOnly>
No need for app store downloads
</Checkbox>
</li>
<li>
<Checkbox isSelected readOnly>
Works offline and loads faster
</Checkbox>
</li>
<li>
<Checkbox isSelected readOnly>
Stay updated with notifications
</Checkbox>
</li>
</ul>
</ModalBody>
<ModalFooter>
<Button
onPress={() => {
handleIOSInstallDeclined();
onClose();
}}
>
<Trans>Maybe later</Trans>
</Button>
<Button
onPress={() => {
handleIOSInstallDeclined();
onClose();
}}
color="primary"
>
<span>Have installed it</span>
</Button>
</ModalFooter>
</>
) : webInstallPrompt ? (
<>
<ModalHeader>
<Trans>Install Our App for a Better Experience!</Trans>
</ModalHeader>
<ModalBody>
<p>
<Trans>Get faster access, work offline, and enjoy a smoother experience.</Trans>
</p>
<ul>
<li>
<Checkbox isSelected readOnly>
Instant access with one tap
</Checkbox>
</li>
<li>
<Checkbox isSelected readOnly>
No need for app store downloads
</Checkbox>
</li>
<li>
<Checkbox isSelected readOnly>
Works offline and loads faster
</Checkbox>
</li>
<li>
<Checkbox isSelected readOnly>
Stay updated with notifications
</Checkbox>
</li>
</ul>
</ModalBody>
<ModalFooter>
<Button
onPress={() => {
handleWebInstallDeclined();
onClose();
}}
>
<Trans>Maybe later</Trans>
</Button>
<Button
onPress={() => {
handleWebInstallAccepted();
onClose();
}}
color="primary"
>
<span>Install our app 👋</span>
</Button>
</ModalFooter>
</>
) : null}
</ModalContent>
</Modal>
);
};

export const InstallPWA = dynamic(() => Promise.resolve(InstallPrompt), {
ssr: false
});
23 changes: 23 additions & 0 deletions pcomparator/src/core/pwa/useIos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import useShouldShowPrompt from "~/core/pwa/useShouldShow";

const iosInstallPromptedAt = "iosInstallPromptedAt";

const isIOS = (): boolean => {
// @ts-ignore
if (navigator.standalone) {
//user has already installed the app
return false;
}
const ua = window.navigator.userAgent;
const isIPad = !!ua.match(/iPad/i);
const isIPhone = !!ua.match(/iPhone/i);
return isIPad || isIPhone;
};

const useIosInstallPrompt = (): [boolean, () => void] => {
const [userShouldBePromptedToInstall, handleUserSeeingInstallPrompt] =
useShouldShowPrompt(iosInstallPromptedAt);

return [isIOS() && userShouldBePromptedToInstall, handleUserSeeingInstallPrompt];
};
export default useIosInstallPrompt;
35 changes: 35 additions & 0 deletions pcomparator/src/core/pwa/useShouldShow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import moment from "moment";
import { useState } from "react";

const getInstallPromptLastSeenAt = (promptName: string): string => localStorage.getItem(promptName)!;

const setInstallPromptSeenToday = (promptName: string): void => {
const today = moment().toISOString();
localStorage.setItem(promptName, today);
};

function getUserShouldBePromptedToInstall(
promptName: string,
daysToWaitBeforePromptingAgain: number
): boolean {
const lastPrompt = moment(getInstallPromptLastSeenAt(promptName));
const daysSinceLastPrompt = moment().diff(lastPrompt, "days");
return Number.isNaN(daysSinceLastPrompt) || daysSinceLastPrompt > daysToWaitBeforePromptingAgain;
}

const useShouldShowPrompt = (
promptName: string,
daysToWaitBeforePromptingAgain = 30
): [boolean, () => void] => {
const [userShouldBePromptedToInstall, setUserShouldBePromptedToInstall] = useState(
getUserShouldBePromptedToInstall(promptName, daysToWaitBeforePromptingAgain)
);

const handleUserSeeingInstallPrompt = () => {
setUserShouldBePromptedToInstall(false);
setInstallPromptSeenToday(promptName);
};

return [userShouldBePromptedToInstall, handleUserSeeingInstallPrompt];
};
export default useShouldShowPrompt;
Loading

0 comments on commit bd30e90

Please sign in to comment.