From d4f1bb5dcd23a2260a02ddbc1218aa6f56b7368b Mon Sep 17 00:00:00 2001 From: Nathan Lamy Date: Tue, 19 Aug 2025 21:59:56 +0200 Subject: [PATCH] feat: add install prompt --- app/components/settings/index.tsx | 3 +- app/components/settings/install-app.tsx | 120 +++++++++++ app/components/settings/notifications.tsx | 247 ++++++++++++++++++++++ app/components/ui/accordion.tsx | 64 ++++++ app/components/ui/switch.tsx | 29 +++ package.json | 2 + pnpm-lock.yaml | 93 ++++++++ 7 files changed, 557 insertions(+), 1 deletion(-) create mode 100644 app/components/settings/install-app.tsx create mode 100644 app/components/settings/notifications.tsx create mode 100644 app/components/ui/accordion.tsx create mode 100644 app/components/ui/switch.tsx diff --git a/app/components/settings/index.tsx b/app/components/settings/index.tsx index 42ca147..c2f1876 100644 --- a/app/components/settings/index.tsx +++ b/app/components/settings/index.tsx @@ -5,6 +5,7 @@ import Preferences from "./preferences"; import BottomNavigation from "~/components/bottom-nav"; import Profile from "./profile"; import type { User } from "~/lib/api"; +import NotificationSettings from "./notifications"; export default function SettingsPage({ user }: { user: User }) { const tabs = [ @@ -24,7 +25,7 @@ export default function SettingsPage({ user }: { user: User }) { value: "notifications", label: "Notifications", icon: , - content:
WIP
, + content: , }, ]; diff --git a/app/components/settings/install-app.tsx b/app/components/settings/install-app.tsx new file mode 100644 index 0000000..3143d6c --- /dev/null +++ b/app/components/settings/install-app.tsx @@ -0,0 +1,120 @@ +import { + ChevronDown, + Chrome, + Info, + MoreVertical, + Share, + Smartphone, +} from "lucide-react"; +import { Alert, AlertDescription, AlertTitle } from "../ui/alert"; +import { Button } from "../ui/button"; +import { CardDescription } from "../ui/card"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@radix-ui/react-accordion"; +import { Separator } from "../ui/separator"; +import { useEffect, useState } from "react"; + +export default function InstallApp() { + const [supportsPWA, setSupportsPWA] = useState(false); + const [promptInstall, setPromptInstall] = useState(null); + + useEffect(() => { + const handler = (e: any) => { + e.preventDefault(); + console.log("we are being triggered :D"); + setSupportsPWA(true); + setPromptInstall(e); + }; + window.addEventListener("beforeinstallprompt", handler); + + return () => window.removeEventListener("transitionend", handler); + }, []); + + const onClick = (evt: React.MouseEvent) => { + evt.preventDefault(); + if (!promptInstall) { + return; + } + promptInstall.prompt(); + }; + + return ( + <> + + + + Informations importantes + + +
    +
  • + • Pour recevoir des notifications, vous devez installer notre + application sur votre appareil. +
  • +
  • + • Vous devrez autoriser les notifications lorsque l'application + vous le demandera. +
  • +
+
+
+ + {/* Install PWA button */} + + + + + + Si le bouton ci-dessus ne fonctionne pas, suivez ces étapes : + + + + + +

Instructions Android (Chrome)

+ +
+ +
    +
  1. Ouvrez ce site avec Google Chrome
  2. +
  3. Appuyez sur les trois points en haut à droite
  4. +
  5. Sélectionnez "Ajouter à l'écran d'accueil"
  6. +
  7. Confirmez pour installer l'application
  8. +
+
+
+ + + +

Instructions iOS (Safari)

+ +
+ +
    +
  1. Ouvrez ce site avec Safari
  2. +
  3. + Appuyez sur l'icône de partage en bas (le carré avec une flèche) +
  4. +
  5. + Faites défiler vers le bas et sélectionnez "Sur l'écran + d'accueil" +
  6. +
  7. Confirmez pour installer l'application
  8. +
+
+
+
+ + ); +} diff --git a/app/components/settings/notifications.tsx b/app/components/settings/notifications.tsx new file mode 100644 index 0000000..e7e84b3 --- /dev/null +++ b/app/components/settings/notifications.tsx @@ -0,0 +1,247 @@ +import { useState } from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { Switch } from "~/components/ui/switch"; +import { Button } from "~/components/ui/button"; +import { Badge } from "~/components/ui/badge"; +import { Separator } from "~/components/ui/separator"; +import { + Bell, + Smartphone, + Monitor, + Trash2, + Send, + AlertTriangle, + Info, +} from "lucide-react"; +import InstallApp from "./install-app"; + +export default function NotificationSettings() { + // TODO: Replace with actual user data + const [pushEnabled, setPushEnabled] = useState(false); + const [events, setEvents] = useState([ + { id: "new-message", label: "New Message", enabled: true }, + { id: "comment-reply", label: "Comment Reply", enabled: true }, + { id: "app-update", label: "App Update", enabled: false }, + { id: "weekly-digest", label: "Weekly Digest", enabled: true }, + { id: "security-alert", label: "Security Alert", enabled: true }, + { id: "friend-request", label: "Friend Request", enabled: false }, + ]); + const [subscriptions] = useState([ + { + id: "1", + name: "iPhone 15 Pro", + type: "mobile", + status: "active", + lastSeen: "2 hours ago", + }, + { + id: "2", + name: "MacBook Pro", + type: "desktop", + status: "active", + lastSeen: "5 minutes ago", + }, + { + id: "3", + name: "Chrome on Windows", + type: "desktop", + status: "revoked", + lastSeen: "3 days ago", + }, + ]); + + const toggleEvent = (eventId: string) => { + setEvents( + events.map((event) => + event.id === eventId ? { ...event, enabled: !event.enabled } : event + ) + ); + }; + + return ( +
+
+

+ Paramètres de notifications +

+

+ Gérez vos préférences de notification et vos appareils abonnés. +

+
+ + {/* Enable Push Notifications Section */} + + +
+ + Activer les notifications +
+ + Autorisez cette application à vous envoyer des notifications push. + +
+ + {/*
+
+

+ Notifications +

+

+ Recevez des notifications même lorsque l’application est fermée. +

+
+ +
*/} + + +
+
+ + {/* Select Notification Events Section */} + + + Select Notification Events + + Choose which events you want to receive notifications for + + + +
+ {events.map((event, index) => ( +
+
+
+

+ {event.label} +

+

+ {event.id === "new-message" && + "Get notified when you receive a new message"} + {event.id === "comment-reply" && + "Get notified when someone replies to your comment"} + {event.id === "app-update" && + "Get notified when a new app version is available"} + {event.id === "weekly-digest" && + "Receive a weekly summary of your activity"} + {event.id === "security-alert" && + "Important security notifications and alerts"} + {event.id === "friend-request" && + "Get notified when someone sends you a friend request"} +

+
+ toggleEvent(event.id)} + aria-label={`Toggle ${event.label} notifications`} + className="flex-shrink-0" + /> +
+ {index < events.length - 1 && ( + + )} +
+ ))} +
+
+
+ + {/* Manage Subscriptions Section */} + + + Manage Subscriptions + + View and manage devices that can receive notifications + + + +
+ {subscriptions.map((subscription, index) => ( +
+
+
+
+ {subscription.type === "mobile" ? ( + + ) : ( + + )} +
+
+
+

+ {subscription.name} +

+ {subscription.status === "revoked" && ( + + + Revoked + + )} + {subscription.status === "active" && ( + + Active + + )} +
+

+ Last seen {subscription.lastSeen} +

+
+
+
+ + +
+
+ {index < subscriptions.length - 1 && ( + + )} +
+ ))} + + {subscriptions.length === 0 && ( +
+ +

+ No subscribed devices found +

+

+ Enable push notifications to add this device +

+
+ )} +
+
+
+
+ ); +} diff --git a/app/components/ui/accordion.tsx b/app/components/ui/accordion.tsx new file mode 100644 index 0000000..8478d48 --- /dev/null +++ b/app/components/ui/accordion.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDownIcon } from "lucide-react" + +import { cn } from "~/lib/utils" + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/app/components/ui/switch.tsx b/app/components/ui/switch.tsx new file mode 100644 index 0000000..6b77246 --- /dev/null +++ b/app/components/ui/switch.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import * as SwitchPrimitive from "@radix-ui/react-switch"; + +import { cn } from "~/lib/utils"; + +function Switch({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { Switch }; diff --git a/package.json b/package.json index 1a335bc..9095e29 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@marsidev/react-turnstile": "^1.1.0", + "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "^1.1.14", @@ -21,6 +22,7 @@ "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.12", "@react-router/node": "^7.5.3", "@react-router/serve": "^7.5.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e50b2bb..dfc5334 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@marsidev/react-turnstile': specifier: ^1.1.0 version: 1.1.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-accordion': + specifier: ^1.2.12 + version: 1.2.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@radix-ui/react-avatar': specifier: ^1.1.10 version: 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -41,6 +44,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-switch': + specifier: ^1.2.6 + version: 1.2.6(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@radix-ui/react-tabs': specifier: ^1.1.12 version: 1.1.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -1645,6 +1651,19 @@ packages: '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-arrow@1.1.7': resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} peerDependencies: @@ -1684,6 +1703,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.7': resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} peerDependencies: @@ -1972,6 +2004,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tabs@1.1.12': resolution: {integrity: sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==} peerDependencies: @@ -8166,6 +8211,23 @@ snapshots: '@radix-ui/primitive@1.1.3': {} + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -8204,6 +8266,22 @@ snapshots: '@types/react': 19.1.8 '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1) @@ -8520,6 +8598,21 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.8)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-tabs@1.1.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/primitive': 1.1.2