added pwa support

This commit is contained in:
shrt 2025-04-08 14:13:27 +02:00
parent b1823f5294
commit f1ee071e62
17 changed files with 5206 additions and 136 deletions

9
capacitor.config.ts Normal file
View File

@ -0,0 +1,9 @@
import type { CapacitorConfig } from "@capacitor/cli";
const config: CapacitorConfig = {
appId: "bettersplit.shrt.solutions",
appName: "bettersplit",
webDir: "out",
};
export default config;

View File

@ -3,8 +3,16 @@
* for Docker builds.
*/
import "./src/env.js";
import pwa from "next-pwa";
import { env } from "./src/env.js";
/** @type {import("next").NextConfig} */
const config = {};
const withPWA = pwa({
dest: "public",
disable: env.NODE_ENV === "development",
register: true, // register the PWA service worker
skipWaiting: true,
});
export default config;
export default withPWA({
eslint: { ignoreDuringBuilds: true },
});

View File

@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"build": "next build",
"export": "next export",
"check": "biome check .",
"check:unsafe": "biome check --write --unsafe .",
"check:write": "biome check --write .",
@ -15,10 +16,13 @@
"dev": "next dev --turbo",
"preview": "next build && next start",
"start": "next start",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"android": "pnpm build && pnpm export && pnpm dlx cap sync android && pnpm dlx cap open android"
},
"dependencies": {
"@auth/drizzle-adapter": "^1.7.2",
"@capacitor/cli": "^7.2.0",
"@capacitor/core": "^7.2.0",
"@hookform/resolvers": "^5.0.1",
"@paralleldrive/cuid2": "^2.2.2",
"@radix-ui/react-avatar": "^1.1.3",
@ -35,6 +39,7 @@
"@trpc/client": "^11.0.0",
"@trpc/react-query": "^11.0.0",
"@trpc/server": "^11.0.0",
"@types/next-pwa": "^5.6.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@ -42,6 +47,7 @@
"lucide-react": "^0.487.0",
"next": "^15.2.3",
"next-auth": "5.0.0-beta.25",
"next-pwa": "^5.6.0",
"next-themes": "^0.4.6",
"postgres": "^3.4.4",
"react": "^19.0.0",

4946
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

1
public/sw.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@ type AppConfig = {
};
export const appConfig: AppConfig = {
name: "Betterwise",
name: "Bettesplit",
navigator: [
{
name: "Home",

View File

@ -1,11 +1,13 @@
import AddFriendDrawer from "@/app/_components/friend/add-friend-drawer";
import FriendList from "@/app/_components/friend/friend-list";
import Header from "@/components/header";
import { auth } from "@/server/auth";
import { api, HydrateClient } from "@/trpc/server";
import React from "react";
export default async function Page() {
void api.friend.getAll.prefetch();
const session = await auth();
if (session?.user) void api.friend.getAll.prefetch();
return (
<HydrateClient>

View File

@ -4,13 +4,14 @@ import React from "react";
export default function FriendRequestButton({}: {}) {
return (
<div>
<Button
{/* <Button
variant={requestedBy === "me" ? "destructive" : "outline"}
className="ml-auto"
size="sm"
>
{requestedBy === "me" ? "Cancel" : "Accept"}
</Button>
</Button> */}
friendRequestButton
</div>
);
}

View File

@ -0,0 +1,86 @@
// "use client";
// import React, { useEffect, useState } from "react";
// export function usePwaInstallStatus() {
// const [isStandalone, setIsStandalone] = useState(false);
// const [isIOS, setIsIOS] = useState(false);
// const [isAndroid, setIsAndroid] = useState(false);
// useEffect(() => {
// const userAgent =
// navigator.userAgent || navigator.vendor || (window as any).opera;
// // iOS detection
// const iOS = /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream;
// setIsIOS(iOS);
// // Android detection
// const android = /android/i.test(userAgent);
// setIsAndroid(android);
// // PWA "standalone" mode detection (works for both iOS & Android)
// const isInStandaloneMode =
// window.matchMedia("(display-mode: standalone)").matches ||
// (navigator as any).standalone === true; // for iOS Safari
// setIsStandalone(isInStandaloneMode);
// }, []);
// return { isIOS, isAndroid, isStandalone };
// }
// function InstallPrompt() {
// const { isIOS, isAndroid, isStandalone } = usePwaInstallStatus();
// const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
// useEffect(() => {
// const handler = (e: any) => {
// e.preventDefault();
// setDeferredPrompt(e);
// };
// window.addEventListener("beforeinstallprompt", handler);
// return () => {
// window.removeEventListener("beforeinstallprompt", handler);
// };
// }, []);
// const handleInstallClick = () => {
// console.log("install clicked");
// if (deferredPrompt) {
// deferredPrompt.prompt();
// deferredPrompt.userChoice.then((choiceResult: any) => {
// if (choiceResult.outcome === "accepted") {
// console.log("User accepted the A2HS prompt");
// } else {
// console.log("User dismissed the A2HS prompt");
// }
// setDeferredPrompt(null);
// });
// }
// };
// if (isStandalone) {
// return null; // Already running as a PWA, no need to show install prompt
// }
// return (
// <div className="absolute w-full h-20 z-50 bg-red-500 left-0 flex items-center justify-center right-0 bottom-0 text-white">
// {isIOS && <p>Tap "Share" and "Add to Home Screen" to install the app.</p>}
// {isAndroid && <button onClick={handleInstallClick}>Install App</button>}
// </div>
// );
// }
// export default function PWAWrapper({
// children,
// }: {
// children: React.ReactNode;
// }) {
// return (
// <>
// <InstallPrompt />
// {children}
// </>
// );
// }

View File

@ -5,6 +5,7 @@ import { Geist } from "next/font/google";
import { Toaster } from "@/components/ui/sonner";
import { TRPCReactProvider } from "@/trpc/react";
import { ThemeProvider } from "@/components/theme-provider";
// import PWAWrapper from "./_components/install-prompt";
export const metadata: Metadata = {
title: "Create T3 App",
@ -30,6 +31,8 @@ export default function RootLayout({
enableSystem
disableTransitionOnChange
>
{/* <PWAWrapper>
</PWAWrapper> */}
{children}
<Toaster position="top-center" />
</ThemeProvider>

26
src/app/manifest.ts Normal file
View File

@ -0,0 +1,26 @@
import { appConfig } from "@/app.config";
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: appConfig.name,
short_name: "NextPWA",
description: "A Progressive Web App built with Next.js",
start_url: "/",
display: "standalone",
background_color: "#ffffff",
theme_color: "#000000",
icons: [
{
src: "/icon-192x192.png",
sizes: "192x192",
type: "image/png",
},
{
src: "/icon-512x512.png",
sizes: "512x512",
type: "image/png",
},
],
};
}

View File

@ -15,7 +15,7 @@ function NavLink({ href, name, icon }: NavLink) {
asChild
variant={"ghost"}
className={cn(
"p-2 size-12 text-muted-foreground/75 flex flex-col gap-2 ",
"p-2 size-12 text-muted-foreground/75 flex flex-col gap-2 hover:bg-transparent dark:hover:bg-transparent ",
active && "text-foreground "
)}
>

View File

@ -1,10 +1,10 @@
"use client"
"use client";
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
const { theme = "system" } = useTheme();
return (
<Sonner
@ -19,7 +19,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
}
{...props}
/>
)
}
);
};
export { Toaster }
export { Toaster };

View File

View File

@ -38,6 +38,7 @@ declare module "next-auth" {
* @see https://next-auth.js.org/configuration/options
*/
export const authConfig = {
trustHost: true,
providers: [
DiscordProvider,
/**

View File

@ -39,5 +39,5 @@
"**/*.js",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
"exclude": ["node_modules", "public/sw.js", "public/workbox-*.js"]
}