diff --git a/.env.example b/.env.example
index e69de29..e4fd78d 100644
--- a/.env.example
+++ b/.env.example
@@ -0,0 +1,3 @@
+NEXT_PUBLIC_VAPID_PUBLIC_KEY=BBVuTTKBNkLp2WCh0mXH-Biy8A50XlAvCcHU5tEJFfuUIDEdKg4gw_evU08KLAGJzgC8iurciHX8Ac_VclRDrJE
+VAPID_PRIVATE_KEY=7RHcigeLtd48aZReOYVGrQ-KCin1tWMtIwDSqNuM7F8
+VAPID_SUBJECT_EMAIL="mailto:witel@info.com"
\ No newline at end of file
diff --git a/messages/ar.json b/messages/ar.json
index 2e55808..0503f04 100644
--- a/messages/ar.json
+++ b/messages/ar.json
@@ -1,5 +1,5 @@
-{
- "Home": {
- "title": "مرحبا"
- }
-}
+{
+ "Home": {
+ "title": "مرحبا"
+ }
+}
diff --git a/messages/en.json b/messages/en.json
index 6e88cea..849045a 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -1,5 +1,5 @@
-{
- "Home": {
- "title": "hello"
- }
-}
+{
+ "Home": {
+ "title": "hello"
+ }
+}
diff --git a/messages/fa.json b/messages/fa.json
index 9cf9fe4..ef0eebd 100644
--- a/messages/fa.json
+++ b/messages/fa.json
@@ -1,5 +1,5 @@
-{
- "Home": {
- "title": "سلام"
- }
-}
+{
+ "Home": {
+ "title": "سلام"
+ }
+}
diff --git a/messages/ku.json b/messages/ku.json
index 2e55808..0503f04 100644
--- a/messages/ku.json
+++ b/messages/ku.json
@@ -1,5 +1,5 @@
-{
- "Home": {
- "title": "مرحبا"
- }
-}
+{
+ "Home": {
+ "title": "مرحبا"
+ }
+}
diff --git a/next-env.d.ts b/next-env.d.ts
index c4b7818..9edff1c 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/dev/types/routes.d.ts";
+import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/next.config.ts b/next.config.ts
index 9027e79..f496203 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,16 +1,6 @@
-// next.config.ts
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";
-// @ts-ignore – no types for next-pwa
-const withPWA = require("next-pwa")({
- dest: "public",
- disable: process.env.NODE_ENV === "development",
- register: true,
- skipWaiting: true,
- navigateFallback: "/offline.html", // fallback page
-});
-
const withNextIntl = createNextIntlPlugin();
const nextConfig: NextConfig = {
@@ -22,4 +12,4 @@ const nextConfig: NextConfig = {
const configWithIntl = withNextIntl(nextConfig);
-export default withPWA(configWithIntl);
+export default configWithIntl;
diff --git a/package.json b/package.json
index f907ae5..1a298fc 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "cinematales",
+ "name": "ruya",
"private": true,
"scripts": {
"dev": "next dev",
@@ -9,6 +9,7 @@
},
"dependencies": {
"@types/js-cookie": "^3.0.6",
+ "@types/web-push": "^3.6.4",
"axios": "^1.15.0",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
@@ -34,6 +35,7 @@
"react-hot-toast": "^2.6.0",
"tailwind-merge": "^3.5.0",
"tailwind-scrollbar": "^4.0.2",
+ "web-push": "^3.6.7",
"zustand": "^5.0.12"
},
"devDependencies": {
diff --git a/public/manifest.json b/public/manifest.webmanifest
similarity index 84%
rename from public/manifest.json
rename to public/manifest.webmanifest
index 426125a..c512053 100644
--- a/public/manifest.json
+++ b/public/manifest.webmanifest
@@ -1,8 +1,8 @@
{
"id": "/",
- "name": "Theater of dreams",
- "short_name": "Theater of dreams",
- "description": "Theater of dreams",
+ "name": "Ru'ya",
+ "short_name": "Ru'ya",
+ "description": "Ru'ya",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
diff --git a/public/offline.html b/public/offline.html
new file mode 100644
index 0000000..0ac3cbd
--- /dev/null
+++ b/public/offline.html
@@ -0,0 +1,10 @@
+
+
+
+ Offline
+
+
+ You are offline
+ Please check your connection.
+
+
diff --git a/public/sw.js b/public/sw.js
new file mode 100644
index 0000000..7ff9098
--- /dev/null
+++ b/public/sw.js
@@ -0,0 +1,77 @@
+const CACHE_NAME = "141-cache-v2";
+
+const STATIC_ASSETS = ["/", "/manifest.webmanifest", "/logo/192px.png", "/logo/512px.png"];
+
+self.addEventListener("install", (event) => {
+ event.waitUntil(
+ caches.open(CACHE_NAME).then(async (cache) => {
+ await Promise.allSettled(
+ STATIC_ASSETS.map(async (url) => {
+ try {
+ const response = await fetch(url);
+ if (response.ok) {
+ await cache.put(url, response);
+ }
+ } catch (e) {
+ console.warn("❌ Failed to cache:", url);
+ }
+ })
+ );
+ })
+ );
+
+ self.skipWaiting();
+});
+
+const sendDeliveryReportAction = () => {
+ console.log("Web push delivered.");
+};
+
+self.addEventListener("push", function (event) {
+ if (event.data) {
+ try {
+ // Try to parse as JSON first
+ let data;
+ try {
+ data = event.data.json();
+ } catch (jsonError) {
+ // If JSON parsing fails, treat it as plain text
+ const text = event.data.text();
+ data = {
+ title: "Notification",
+ body: text,
+ icon: "/icon.png",
+ };
+ }
+
+ const options = {
+ body: data.body || "Default notification body",
+ icon: data.icon || "/logo/192px.png",
+ badge: "/badge.png",
+ vibrate: [100, 50, 100],
+ data: {
+ dateOfArrival: Date.now(),
+ primaryKey: "2",
+ url: data.url || "/",
+ },
+ };
+ event.waitUntil(
+ self.registration.showNotification(data.title, options).then(() => {
+ sendDeliveryReportAction();
+ })
+ );
+ } catch (error) {
+ console.error("Error processing push notification:", error);
+ }
+ }
+});
+self.addEventListener("notificationclick", function (event) {
+ event.notification.close();
+ event.waitUntil(clients.openWindow(event.notification.data.url));
+});
+
+self.addEventListener("message", (event) => {
+ if (event.data && event.data.type === "SKIP_WAITING") {
+ self.skipWaiting();
+ }
+});
diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx
index 7303fe5..4f55e96 100644
--- a/src/app/[locale]/layout.tsx
+++ b/src/app/[locale]/layout.tsx
@@ -1,55 +1,80 @@
-import { getDirection } from "@/i18n/direction";
-import { routing } from "@/i18n/routing";
-import { ClientAppProvider } from "@/providers/ClientAppProvider";
-import "@/styles/globals.css";
-import type { Metadata } from "next";
-import { hasLocale, NextIntlClientProvider } from "next-intl";
-import { getMessages } from "next-intl/server";
-import { notFound } from "next/navigation";
-
-export const metadata: Metadata = {
- title: "Theater of dreams",
-};
-
-export default async function RootLayout({
- children,
- params,
-}: Readonly<{
- children: React.ReactNode;
- params: Promise<{ locale: string }>;
-}>) {
- const { locale } = await params;
- if (!hasLocale(routing.locales, locale)) {
- notFound();
- }
-
- const messages = await getMessages();
- const dir = getDirection(locale);
-
- return (
-
-
- {/* Prevent theme flicker before hydration */}
-
-
-
-
-
- {children}
-
-
-
- );
-}
+import { getDirection } from "@/i18n/direction";
+import { routing } from "@/i18n/routing";
+import { ClientAppProvider } from "@/providers/ClientAppProvider";
+import "@/styles/globals.css";
+import type { Metadata } from "next";
+import { hasLocale, NextIntlClientProvider } from "next-intl";
+import { getMessages } from "next-intl/server";
+import { notFound } from "next/navigation";
+
+export const metadata: Metadata = {
+ title: {
+ template: "%s | Ru'ya",
+ default: "Ru'ya",
+ },
+ description: "Ru'ya",
+ appleWebApp: {
+ capable: true, // 🔴 REQUIRED for iOS standalone mode
+ statusBarStyle: "black-translucent",
+ title: "Ru'ya",
+ },
+ applicationName: "Ru'ya",
+ authors: {
+ name: "شرکت دانش بنیان ویرا ارتباطات یکتا تلفن (وایتل)",
+ url: "https://witel.ir",
+ },
+
+ manifest: "/manifest.webmanifest",
+ icons: {
+ icon: "/logo/192px.png",
+ apple: "/logo/512px.png",
+ },
+};
+export const viewport = {
+ width: "device-width",
+ initialScale: 1,
+ maximumScale: 1,
+};
+
+export default async function RootLayout({
+ children,
+ params,
+}: Readonly<{
+ children: React.ReactNode;
+ params: Promise<{ locale: string }>;
+}>) {
+ const { locale } = await params;
+ if (!hasLocale(routing.locales, locale)) {
+ notFound();
+ }
+
+ const messages = await getMessages();
+ const dir = getDirection(locale);
+
+ return (
+
+
+ {/* Prevent theme flicker before hydration */}
+
+
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx
index b634e00..3986db6 100644
--- a/src/app/[locale]/page.tsx
+++ b/src/app/[locale]/page.tsx
@@ -1,14 +1,13 @@
-import ComponentsConteiner from "@/components/ComponentsConteiner";
-import { useTranslations } from "next-intl";
-
-export default function Home() {
- const t = useTranslations("Home");
- return (
-
- {t("title")}
- This project was generated completely offline.
-
-
- );
-}
-
+import ComponentsConteiner from "@/components/ComponentsConteiner";
+import { useTranslations } from "next-intl";
+
+export default function Home() {
+ const t = useTranslations("Home");
+ return (
+
+ {t("title")}
+ This project was generated completely offline.
+
+
+ );
+}
diff --git a/src/app/api/notifications/route.ts b/src/app/api/notifications/route.ts
new file mode 100644
index 0000000..91adf6b
--- /dev/null
+++ b/src/app/api/notifications/route.ts
@@ -0,0 +1,22 @@
+import { NextResponse } from "next/server";
+
+let notifications = [
+ {
+ id: 1,
+ title: "خوش آمدید",
+ body: "این یک نوتیفیکیشن تستی است",
+ type: "info",
+ created_at: new Date().toISOString(),
+ },
+ {
+ id: 2,
+ title: "پرداخت موفق",
+ body: "پرداخت شما با موفقیت انجام شد",
+ type: "success",
+ created_at: new Date().toISOString(),
+ },
+];
+
+export async function GET() {
+ return NextResponse.json(notifications);
+}
diff --git a/src/app/api/notifications/sse/route.ts b/src/app/api/notifications/sse/route.ts
new file mode 100644
index 0000000..4394e18
--- /dev/null
+++ b/src/app/api/notifications/sse/route.ts
@@ -0,0 +1,41 @@
+import { NextRequest } from "next/server";
+
+export const dynamic = "force-dynamic";
+
+export async function GET(req: NextRequest) {
+ const encoder = new TextEncoder();
+
+ const stream = new ReadableStream({
+ start(controller) {
+ let counter = 3;
+
+ const interval = setInterval(() => {
+ const data = {
+ id: counter,
+ title: `نوتیفیکیشن جدید ${counter}`,
+ body: "این پیام از SSE اومده",
+ type: ["info", "success", "warning", "error"][counter % 4],
+ created_at: new Date().toISOString(),
+ };
+
+ const payload = `data: ${JSON.stringify(data)}\n\n`;
+
+ controller.enqueue(encoder.encode(payload));
+ counter++;
+ }, 5000);
+
+ req.signal.addEventListener("abort", () => {
+ clearInterval(interval);
+ controller.close();
+ });
+ },
+ });
+
+ return new Response(stream, {
+ headers: {
+ "Content-Type": "text/event-stream",
+ "Cache-Control": "no-cache",
+ Connection: "keep-alive",
+ },
+ });
+}
diff --git a/src/app/api/push-notifications/route.ts b/src/app/api/push-notifications/route.ts
new file mode 100644
index 0000000..d151a9f
--- /dev/null
+++ b/src/app/api/push-notifications/route.ts
@@ -0,0 +1,175 @@
+import { NextRequest, NextResponse } from "next/server";
+import webpush from "web-push";
+
+// Initialize VAPID keys
+webpush.setVapidDetails(
+ process.env.VAPID_SUBJECT_EMAIL!,
+ process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
+ process.env.VAPID_PRIVATE_KEY!
+);
+
+// Define the subscription type that matches what web-push expects
+interface WebPushSubscription {
+ endpoint: string;
+ keys: {
+ p256dh: string;
+ auth: string;
+ };
+}
+
+// Store subscriptions in memory with user IDs
+// In production, replace this with Redis or a database
+const userSubscriptions: Map = new Map();
+
+// Save subscription for a specific user
+async function saveUserSubscription(userId: string, subscription: WebPushSubscription) {
+ userSubscriptions.set(userId, subscription);
+ // In production: save to Redis/Database
+ // await redis.set(`push:${userId}`, JSON.stringify(subscription));
+}
+
+// Get subscription for a specific user
+async function getUserSubscription(userId: string): Promise {
+ return userSubscriptions.get(userId) || null;
+ // In production: get from Redis/Database
+}
+
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+
+ // Handle different actions
+ const { action, userId, subscription, title, message, icon, url } = body;
+
+ // Action 1: Register/update subscription for a user
+ if (action === "register") {
+ if (!userId || !subscription) {
+ return NextResponse.json({ error: "userId and subscription are required" }, { status: 400 });
+ }
+
+ // Validate subscription has required fields
+ if (!subscription.endpoint || !subscription.keys || !subscription.keys.p256dh || !subscription.keys.auth) {
+ return NextResponse.json(
+ { error: "Invalid subscription format - missing endpoint or keys" },
+ { status: 400 }
+ );
+ }
+
+ const webPushSubscription: WebPushSubscription = {
+ endpoint: subscription.endpoint,
+ keys: {
+ p256dh: subscription.keys.p256dh,
+ auth: subscription.keys.auth,
+ },
+ };
+
+ await saveUserSubscription(userId, webPushSubscription);
+
+ return NextResponse.json({
+ success: true,
+ message: `Subscription registered for user ${userId}`,
+ });
+ }
+
+ // Action 2: Send notification to a specific user
+ if (action === "send") {
+ if (!userId) {
+ return NextResponse.json({ error: "userId is required" }, { status: 400 });
+ }
+
+ const userSubscription = await getUserSubscription(userId);
+
+ if (!userSubscription) {
+ return NextResponse.json({ error: `No subscription found for user ${userId}` }, { status: 404 });
+ }
+
+ const payload = JSON.stringify({
+ title: title || "Notification",
+ body: message || "You have a new notification",
+ icon: icon || "/logo/192px.png",
+ badge: "/badge.png",
+ vibrate: [100, 50, 100],
+ data: {
+ url: url || "/",
+ dateOfArrival: Date.now(),
+ },
+ });
+
+ // Send notification using web-push
+ await webpush.sendNotification(userSubscription, payload);
+
+ return NextResponse.json({
+ success: true,
+ message: `Notification sent to user ${userId}`,
+ });
+ }
+
+ // Action 3: Send to multiple users
+ if (action === "send-multiple") {
+ const { userIds, title, message, icon, url } = body;
+
+ if (!userIds || !Array.isArray(userIds)) {
+ return NextResponse.json({ error: "userIds array is required" }, { status: 400 });
+ }
+
+ const results = [];
+
+ for (const uid of userIds) {
+ const userSubscription = await getUserSubscription(uid);
+
+ if (userSubscription) {
+ try {
+ const payload = JSON.stringify({
+ title: title || "Notification",
+ body: message || "You have a new notification",
+ icon: icon || "/logo/192px.png",
+ badge: "/badge.png",
+ data: { url: url || "/" },
+ });
+
+ await webpush.sendNotification(userSubscription, payload);
+ results.push({ userId: uid, status: "success" });
+ } catch (error) {
+ results.push({
+ userId: uid,
+ status: "failed",
+ error: error instanceof Error ? error.message : String(error),
+ });
+ }
+ } else {
+ results.push({ userId: uid, status: "no_subscription" });
+ }
+ }
+
+ return NextResponse.json({
+ success: true,
+ results,
+ });
+ }
+
+ return NextResponse.json({ error: "Invalid action. Use: register, send, or send-multiple" }, { status: 400 });
+ } catch (error) {
+ console.error("Error in push API:", error);
+ return NextResponse.json(
+ { error: "Internal server error", details: error instanceof Error ? error.message : String(error) },
+ { status: 500 }
+ );
+ }
+}
+
+// GET endpoint to check if a user is subscribed
+export async function GET(request: NextRequest) {
+ const { searchParams } = new URL(request.url);
+ const userId = searchParams.get("userId");
+
+ if (!userId) {
+ return NextResponse.json({ error: "userId required" }, { status: 400 });
+ }
+
+ const subscription = await getUserSubscription(userId);
+
+ return NextResponse.json({
+ userId,
+ isSubscribed: !!subscription,
+ });
+}
diff --git a/src/components/ComponentsConteiner.tsx b/src/components/ComponentsConteiner.tsx
index d7f5443..bdda848 100644
--- a/src/components/ComponentsConteiner.tsx
+++ b/src/components/ComponentsConteiner.tsx
@@ -1,27 +1,27 @@
-"use client";
-
-import { useState } from "react";
-import PrimaryButton from "./buttons/PrimaryButton";
-import ThemeToggle from "./UI/ThemeToggle";
-import { Home } from "lucide-react";
-
-export default function ComponentsConteiner() {
- const [isLoading, setIsloading] = useState(false);
- function dummyLoading() {
- setIsloading(true);
- setTimeout(() => {
- setIsloading(false);
- }, 1500);
- }
- return (
-
-
-
-
dummyLoading()} loading={isLoading}>
-
- test icon
-
-
-
- );
-}
+"use client";
+
+import { useState } from "react";
+import PrimaryButton from "./buttons/PrimaryButton";
+import ThemeToggle from "./UI/ThemeToggle";
+import { Home } from "lucide-react";
+
+export default function ComponentsConteiner() {
+ const [isLoading, setIsloading] = useState(false);
+ function dummyLoading() {
+ setIsloading(true);
+ setTimeout(() => {
+ setIsloading(false);
+ }, 1500);
+ }
+ return (
+
+
+
+
dummyLoading()} loading={isLoading}>
+
+ test icon
+
+
+
+ );
+}
diff --git a/src/components/PWAPush/PushNotificationManager.tsx b/src/components/PWAPush/PushNotificationManager.tsx
new file mode 100644
index 0000000..39ead4f
--- /dev/null
+++ b/src/components/PWAPush/PushNotificationManager.tsx
@@ -0,0 +1,139 @@
+"use client";
+
+import { useEffect } from "react";
+
+function urlBase64ToUint8Array(base64String: string) {
+ // Add padding if needed
+ const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
+ const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
+
+ const rawData = window.atob(base64);
+ const outputArray = new Uint8Array(rawData.length);
+
+ for (let i = 0; i < rawData.length; ++i) {
+ outputArray[i] = rawData.charCodeAt(i);
+ }
+ return outputArray;
+}
+
+function getUserId(): string {
+ let userId = localStorage.getItem("push_user_id");
+ if (!userId) {
+ userId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+ localStorage.setItem("push_user_id", userId);
+ }
+ return userId;
+}
+
+function arrayBufferToBase64(buffer: ArrayBuffer | null): string {
+ if (!buffer) return "";
+ const bytes = new Uint8Array(buffer);
+ let binary = "";
+ for (let i = 0; i < bytes.byteLength; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ return btoa(binary);
+}
+
+export default function AutoPushManager() {
+ useEffect(() => {
+ const initPush = async () => {
+ if (!("serviceWorker" in navigator && "PushManager" in window)) {
+ console.log("Push not supported");
+ return;
+ }
+
+ try {
+ // Register service worker
+ const registration = await navigator.serviceWorker.register("/sw.js", {
+ scope: "/",
+ updateViaCache: "none",
+ });
+
+ await navigator.serviceWorker.ready;
+ console.log("Service worker ready");
+
+ // Check existing subscription
+ let subscription = await registration.pushManager.getSubscription();
+
+ if (subscription) {
+ console.log("Already subscribed");
+ // Still register with backend
+ const subscriptionObj = {
+ endpoint: subscription.endpoint,
+ keys: {
+ p256dh: arrayBufferToBase64(subscription.getKey("p256dh")),
+ auth: arrayBufferToBase64(subscription.getKey("auth")),
+ },
+ };
+
+ await fetch("/api/push-notifications", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ action: "register",
+ userId: getUserId(),
+ subscription: subscriptionObj,
+ }),
+ });
+
+ console.log("Push notifications already ready!");
+ return;
+ }
+
+ // Check permission status
+ console.log("Current permission:", Notification.permission);
+
+ if (Notification.permission === "denied") {
+ console.log("Notifications are blocked. Please enable them in browser settings.");
+ return;
+ }
+
+ // Request permission (this shows the browser prompt)
+ console.log("Requesting permission...");
+ const permission = await Notification.requestPermission();
+ console.log("Permission result:", permission);
+
+ if (permission === "granted") {
+ console.log("Creating subscription...");
+ // This might still take a moment, but should complete
+ subscription = await registration.pushManager.subscribe({
+ userVisibleOnly: true,
+ applicationServerKey: urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!),
+ });
+
+ console.log("Subscription created:", subscription);
+
+ const subscriptionObj = {
+ endpoint: subscription.endpoint,
+ keys: {
+ p256dh: arrayBufferToBase64(subscription.getKey("p256dh")),
+ auth: arrayBufferToBase64(subscription.getKey("auth")),
+ },
+ };
+
+ await fetch("/api/push-notifications", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ action: "register",
+ userId: getUserId(),
+ subscription: subscriptionObj,
+ }),
+ });
+
+ console.log("Push notifications ready! User ID:", getUserId());
+ } else {
+ console.log("Notification permission not granted:", permission);
+ }
+ } catch (error) {
+ console.error("Push error:", error);
+ }
+ };
+
+ // Add a small delay to ensure everything is loaded
+ setTimeout(initPush, 1000);
+ }, []);
+
+ return null;
+}
diff --git a/src/components/buttons/PrimaryButton.tsx b/src/components/buttons/PrimaryButton.tsx
index 9dacf3c..aabc30b 100644
--- a/src/components/buttons/PrimaryButton.tsx
+++ b/src/components/buttons/PrimaryButton.tsx
@@ -1,69 +1,69 @@
-"use client";
-
-import { cn } from "@/lib/utils";
-import { AnimatePresence, motion } from "framer-motion";
-import { LoaderPinwheel } from "lucide-react";
-
-type PrimaryButtonProps = React.ButtonHTMLAttributes & {
- loading?: boolean;
- children: React.ReactNode;
-};
-
-export default function PrimaryButton({ children, loading, className, onClick }: PrimaryButtonProps) {
- return (
-
- );
-}
+"use client";
+
+import { cn } from "@/lib/utils";
+import { AnimatePresence, motion } from "framer-motion";
+import { LoaderPinwheel } from "lucide-react";
+
+type PrimaryButtonProps = React.ButtonHTMLAttributes & {
+ loading?: boolean;
+ children: React.ReactNode;
+};
+
+export default function PrimaryButton({ children, loading, className, onClick }: PrimaryButtonProps) {
+ return (
+
+ );
+}
diff --git a/src/i18n/direction.ts b/src/i18n/direction.ts
index 13102be..5ca8d07 100644
--- a/src/i18n/direction.ts
+++ b/src/i18n/direction.ts
@@ -1,12 +1,12 @@
-export type Direction = "ltr" | "rtl";
-
-export const localeDirectionMap: Record = {
- en: "ltr",
- ar: "rtl",
- ku: "rtl",
- fa: "rtl",
-};
-
-export function getDirection(locale: string): Direction {
- return localeDirectionMap[locale] ?? "ltr";
-}
+export type Direction = "ltr" | "rtl";
+
+export const localeDirectionMap: Record = {
+ en: "ltr",
+ ar: "rtl",
+ ku: "rtl",
+ fa: "rtl",
+};
+
+export function getDirection(locale: string): Direction {
+ return localeDirectionMap[locale] ?? "ltr";
+}
diff --git a/src/i18n/navigation.ts b/src/i18n/navigation.ts
index 5024e59..4fff38d 100644
--- a/src/i18n/navigation.ts
+++ b/src/i18n/navigation.ts
@@ -1,4 +1,4 @@
-import { createNavigation } from 'next-intl/navigation'
-import { routing } from './routing'
-
-export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing)
+import { createNavigation } from "next-intl/navigation";
+import { routing } from "./routing";
+
+export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing);
diff --git a/src/i18n/request.ts b/src/i18n/request.ts
index 76cadd2..047c043 100644
--- a/src/i18n/request.ts
+++ b/src/i18n/request.ts
@@ -1,13 +1,13 @@
-import { getRequestConfig } from "next-intl/server";
-import { hasLocale } from "next-intl";
-import { routing } from "./routing";
-
-export default getRequestConfig(async ({ requestLocale }) => {
- const requested = await requestLocale;
- const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale;
-
- return {
- locale,
- messages: (await import(`../../messages/${locale}.json`)).default,
- };
-});
+import { getRequestConfig } from "next-intl/server";
+import { hasLocale } from "next-intl";
+import { routing } from "./routing";
+
+export default getRequestConfig(async ({ requestLocale }) => {
+ const requested = await requestLocale;
+ const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale;
+
+ return {
+ locale,
+ messages: (await import(`../../messages/${locale}.json`)).default,
+ };
+});
diff --git a/src/i18n/routing.ts b/src/i18n/routing.ts
index 066f761..c50b9ea 100644
--- a/src/i18n/routing.ts
+++ b/src/i18n/routing.ts
@@ -1,7 +1,7 @@
-import { defineRouting } from "next-intl/routing";
-
-export const routing = defineRouting({
- locales: ["en", "ar", "ku", "fa"],
- defaultLocale: "en",
- localePrefix: "never",
-});
+import { defineRouting } from "next-intl/routing";
+
+export const routing = defineRouting({
+ locales: ["en", "ar", "ku", "fa"],
+ defaultLocale: "en",
+ localePrefix: "never",
+});
diff --git a/src/i18n/useDirection.ts b/src/i18n/useDirection.ts
index 875fe61..a9417e8 100644
--- a/src/i18n/useDirection.ts
+++ b/src/i18n/useDirection.ts
@@ -1,9 +1,9 @@
-'use client'
-
-import { useLocale } from 'next-intl'
-import { Direction, getDirection } from './direction'
-
-export function useDirection(): Direction {
- const locale = useLocale()
- return getDirection(locale)
-}
+"use client";
+
+import { useLocale } from "next-intl";
+import { Direction, getDirection } from "./direction";
+
+export function useDirection(): Direction {
+ const locale = useLocale();
+ return getDirection(locale);
+}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index fed2fe9..9911128 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,6 +1,6 @@
-import { clsx, type ClassValue } from 'clsx'
-import { twMerge } from 'tailwind-merge'
-
-export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
-}
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/src/providers/ClientAppProvider.tsx b/src/providers/ClientAppProvider.tsx
index a43ed8f..d155e40 100644
--- a/src/providers/ClientAppProvider.tsx
+++ b/src/providers/ClientAppProvider.tsx
@@ -7,6 +7,26 @@ import { useEffect, useState } from "react";
export function ClientAppProvider({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = useState(false);
+ useEffect(() => {
+ if ("serviceWorker" in navigator) {
+ const registerSW = async () => {
+ try {
+ const registration = await navigator.serviceWorker.register("/sw.js", {
+ scope: "/",
+ updateViaCache: "none",
+ });
+ console.log("Service Worker registered successfully:", registration);
+ } catch (error) {
+ console.error("Service Worker registration failed:", error);
+ }
+ };
+
+ // Register on page load and after navigation
+ registerSW();
+ window.addEventListener("load", registerSW);
+ }
+ }, []);
+
// wait for hydration
useEffect(() => {
setMounted(true);
diff --git a/src/proxy.ts b/src/proxy.ts
index 124b978..d7382d8 100644
--- a/src/proxy.ts
+++ b/src/proxy.ts
@@ -1,8 +1,8 @@
-import createMiddleware from 'next-intl/middleware'
-import { routing } from './i18n/routing'
-
-export default createMiddleware(routing)
-
-export const config = {
- matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)',
-}
+import createMiddleware from "next-intl/middleware";
+import { routing } from "./i18n/routing";
+
+export default createMiddleware(routing);
+
+export const config = {
+ matcher: "/((?!api|trpc|_next|_vercel|.*\\..*).*)",
+};