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 */}
+
+
+ Installer l'application
+
+
+
+
+
+ Si le bouton ci-dessus ne fonctionne pas, suivez ces étapes :
+
+
+
+
+
+ Instructions Android (Chrome)
+
+
+
+
+ Ouvrez ce site avec Google Chrome
+ Appuyez sur les trois points en haut à droite
+ Sélectionnez "Ajouter à l'écran d'accueil"
+ Confirmez pour installer l'application
+
+
+
+
+
+
+ Instructions iOS (Safari)
+
+
+
+
+ Ouvrez ce site avec Safari
+
+ Appuyez sur l'icône de partage en bas (le carré avec une flèche)
+
+
+ Faites défiler vers le bas et sélectionnez "Sur l'écran
+ d'accueil"
+
+ Confirmez pour installer l'application
+
+
+
+
+ >
+ );
+}
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}
+
+
+
+
+
+
+ Test
+
+
+
+ Delete
+
+
+
+ {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