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

chore(pcomparator): add pwa manifest #79

Merged
merged 7 commits into from
Nov 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading