From e05a2061c4df5144d5bd8c75141a7890d1a13d4f Mon Sep 17 00:00:00 2001 From: AminGhasempoor Date: Wed, 22 Apr 2026 08:56:11 +0330 Subject: [PATCH 1/2] add pwa config --- .env.example | 3 + next.config.ts | 12 +- package.json | 98 +++++----- .../{manifest.json => manifest.webmanifest} | 6 +- public/offline.html | 10 + public/sw.js | 77 ++++++++ src/app/api/notifications/route.ts | 22 +++ src/app/api/notifications/sse/route.ts | 41 ++++ src/app/api/push-notifications/route.ts | 175 ++++++++++++++++++ src/app/layout.tsx | 14 +- .../PWAPush/PushNotificationManager.tsx | 139 ++++++++++++++ src/providers/ClientAppProvider.tsx | 20 ++ 12 files changed, 551 insertions(+), 66 deletions(-) rename public/{manifest.json => manifest.webmanifest} (84%) create mode 100644 public/offline.html create mode 100644 public/sw.js create mode 100644 src/app/api/notifications/route.ts create mode 100644 src/app/api/notifications/sse/route.ts create mode 100644 src/app/api/push-notifications/route.ts create mode 100644 src/components/PWAPush/PushNotificationManager.tsx 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/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 9b32e57..438764d 100644 --- a/package.json +++ b/package.json @@ -1,48 +1,50 @@ -{ - "name": "witelflix", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "format": "npx prettier . --write" - }, - "dependencies": { - "@types/js-cookie": "^3.0.6", - "axios": "^1.15.0", - "clsx": "^2.1.1", - "date-fns": "^4.1.0", - "date-fns-jalali": "4.1.0-0", - "embla-carousel": "^8.6.0", - "embla-carousel-autoplay": "^8.6.0", - "embla-carousel-react": "^8.6.0", - "framer-motion": "^12.38.0", - "js-cookie": "^3.0.5", - "lucide-react": "^1.8.0", - "next": "16.1.6", - "next-intl": "^4.9.1", - "next-pwa": "^5.6.0", - "next-sitemap": "^4.2.3", - "next-themes": "^0.4.6", - "nextjs-toploader": "^3.9.17", - "prettier": "^3.8.2", - "prettier-plugin-tailwindcss": "^0.7.2", - "radix-ui": "^1.4.3", - "react": "19.2.4", - "react-dom": "19.2.4", - "react-hook-form": "^7.72.1", - "react-hot-toast": "^2.6.0", - "tailwind-merge": "^3.5.0", - "tailwind-scrollbar": "^4.0.2", - "zustand": "^5.0.12" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4.1.18", - "@types/node": "25.0.10", - "@types/react": "19.2.10", - "@types/react-dom": "19.2.3", - "postcss": "8.5.6", - "tailwindcss": "4.1.18", - "typescript": "5.9.3" - } -} +{ + "name": "witelflix", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "format": "npx prettier . --write" + }, + "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", + "date-fns-jalali": "4.1.0-0", + "embla-carousel": "^8.6.0", + "embla-carousel-autoplay": "^8.6.0", + "embla-carousel-react": "^8.6.0", + "framer-motion": "^12.38.0", + "js-cookie": "^3.0.5", + "lucide-react": "^1.8.0", + "next": "16.1.6", + "next-intl": "^4.9.1", + "next-pwa": "^5.6.0", + "next-sitemap": "^4.2.3", + "next-themes": "^0.4.6", + "nextjs-toploader": "^3.9.17", + "prettier": "^3.8.2", + "prettier-plugin-tailwindcss": "^0.7.2", + "radix-ui": "^1.4.3", + "react": "19.2.4", + "react-dom": "19.2.4", + "react-hook-form": "^7.72.1", + "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": { + "@tailwindcss/postcss": "^4.1.18", + "@types/node": "25.0.10", + "@types/react": "19.2.10", + "@types/react-dom": "19.2.3", + "postcss": "8.5.6", + "tailwindcss": "4.1.18", + "typescript": "5.9.3" + } +} 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/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/app/layout.tsx b/src/app/layout.tsx index e06c8d3..3d45b2c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,16 +6,22 @@ import { NextIntlClientProvider } from "next-intl"; export const metadata: Metadata = { title: { - template: "%s | Theater of dreams", - default: "Theater of dreams", + template: "%s | Ru'ya", + default: "Ru'ya", }, - description: "Theater of dreams", + 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.json", + manifest: "/manifest.webmanifest", icons: { icon: "/logo/192px.png", apple: "/logo/512px.png", 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/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); -- 2.49.1 From 88b9f5c387dfd322816d057f9641b97256702af5 Mon Sep 17 00:00:00 2001 From: AminGhasempoor Date: Wed, 22 Apr 2026 08:59:26 +0330 Subject: [PATCH 2/2] build and format --- messages/ar.json | 10 +- messages/en.json | 10 +- messages/fa.json | 10 +- messages/ku.json | 10 +- next-env.d.ts | 2 +- package.json | 100 ++++++++-------- src/app/[locale]/page.tsx | 27 +++-- src/components/ComponentsConteiner.tsx | 54 ++++----- src/components/buttons/PrimaryButton.tsx | 138 +++++++++++------------ src/i18n/direction.ts | 24 ++-- src/i18n/navigation.ts | 8 +- src/i18n/request.ts | 26 ++--- src/i18n/routing.ts | 14 +-- src/i18n/useDirection.ts | 18 +-- src/lib/utils.ts | 12 +- src/proxy.ts | 16 +-- 16 files changed, 239 insertions(+), 240 deletions(-) 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/package.json b/package.json index 8e1a470..1a298fc 100644 --- a/package.json +++ b/package.json @@ -1,50 +1,50 @@ -{ - "name": "cinematales", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "format": "npx prettier . --write" - }, - "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", - "date-fns-jalali": "4.1.0-0", - "embla-carousel": "^8.6.0", - "embla-carousel-autoplay": "^8.6.0", - "embla-carousel-react": "^8.6.0", - "framer-motion": "^12.38.0", - "js-cookie": "^3.0.5", - "lucide-react": "^1.8.0", - "next": "16.1.6", - "next-intl": "^4.9.1", - "next-pwa": "^5.6.0", - "next-sitemap": "^4.2.3", - "next-themes": "^0.4.6", - "nextjs-toploader": "^3.9.17", - "prettier": "^3.8.2", - "prettier-plugin-tailwindcss": "^0.7.2", - "radix-ui": "^1.4.3", - "react": "19.2.4", - "react-dom": "19.2.4", - "react-hook-form": "^7.72.1", - "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": { - "@tailwindcss/postcss": "^4.1.18", - "@types/node": "25.0.10", - "@types/react": "19.2.10", - "@types/react-dom": "19.2.3", - "postcss": "8.5.6", - "tailwindcss": "4.1.18", - "typescript": "5.9.3" - } -} +{ + "name": "ruya", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "format": "npx prettier . --write" + }, + "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", + "date-fns-jalali": "4.1.0-0", + "embla-carousel": "^8.6.0", + "embla-carousel-autoplay": "^8.6.0", + "embla-carousel-react": "^8.6.0", + "framer-motion": "^12.38.0", + "js-cookie": "^3.0.5", + "lucide-react": "^1.8.0", + "next": "16.1.6", + "next-intl": "^4.9.1", + "next-pwa": "^5.6.0", + "next-sitemap": "^4.2.3", + "next-themes": "^0.4.6", + "nextjs-toploader": "^3.9.17", + "prettier": "^3.8.2", + "prettier-plugin-tailwindcss": "^0.7.2", + "radix-ui": "^1.4.3", + "react": "19.2.4", + "react-dom": "19.2.4", + "react-hook-form": "^7.72.1", + "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": { + "@tailwindcss/postcss": "^4.1.18", + "@types/node": "25.0.10", + "@types/react": "19.2.10", + "@types/react-dom": "19.2.3", + "postcss": "8.5.6", + "tailwindcss": "4.1.18", + "typescript": "5.9.3" + } +} 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/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/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/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|.*\\..*).*)", +}; -- 2.49.1