feat: implement PWA support with manifest and icons and add mobile-optimized bottom navigation

This commit is contained in:
sazzadulalambd
2026-05-06 16:38:57 +06:00
parent 849c6e56ba
commit 502e576fc1
10 changed files with 3408 additions and 190 deletions

8
.gitignore vendored
View File

@@ -39,3 +39,11 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
# PWA files
**/public/sw.js
**/public/workbox-*.js
**/public/worker-*.js
**/public/sw.js.map
**/public/workbox-*.js.map
**/public/worker-*.js.map

View File

@@ -0,0 +1,136 @@
# Settings Module — Permission Documentation
## Module: Settings
Controls access to all system configuration areas including KYC documents, plans, and company policies.
---
## 1. KYC Documents
**Permission Keys:**
- `settings.kyc_documents_config` — Admin can configure document lists
- `settings.kyc_documents_view` — Staff/Users can view document lists
**Description:**
- **Admin** will set KYC Documents list for:
- Investor
- Merchant
- Swap Station
- Rental Types:
- Rental (Single)
- Rental (2 Person Shared)
- Rent-to-Own
- **Staffs and Users** will show the list of documents based on what Admin configured.
---
## 2. Plan Selection + EV Condition
**Permission Keys:**
- `settings.plan_selection_with_condition_config` — Admin can configure plan details
- `settings.plan_selection_with_condition_view` — Staff/Users can view plan details
**Description:**
- **Admin** will set Rental Types plan selection with EV condition including:
- Rental (Single)
- Rental (2 Person Shared)
- Rent-to-Own
- Configurable fields: Price, Deposit, EV Condition, etc.
- **Staffs and Users** will show the list of Plan Selection + EV Condition with price and deposit etc.
---
## 3. Investment Plan
**Permission Keys:**
- `settings.investment_plan_config` — Admin can configure investment plans
- `settings.investment_plan_view` — Staff/Users can view investment plans
**Description:**
- Admin can create/edit investment plans with pricing, duration, features.
- Staffs and Users can only view the available investment plans.
---
## 4. Swap Station Plan
**Permission Keys:**
- `settings.swap_station_plan_config` — Admin can configure swap station plans
- `settings.swap_station_plan_view` — Staff/Users can view swap station plans
**Description:**
- Admin can create/edit swap station plans with pricing, requirements, features.
- Staffs and Users can only view the available swap station plans.
---
## 5. Rider Request Plan for Merchant
**Permission Keys:**
- `settings.rider_request_plan_for_merchant_config` — Admin can configure merchant rider request plans
- `settings.rider_request_plan_for_merchant_view` — Staff/Users can view merchant rider request plans
**Description:**
- Admin can create/edit rider request plans specifically for merchants.
- Staffs and Users can only view the available rider request plans for merchants.
---
## 6. Company Policy
**Permission Keys:**
- `settings.company_policy_config` — Admin can configure company policies
- `settings.company_policy_view` — Staff/Users can view company policies
**Description:**
- Admin can create/edit company policies for all sections.
- Staffs and Users can only view the company policies.
---
## Summary Table
| Section | Config Permission | View Permission |
|---------|------------------|-----------------|
| KYC Documents | `settings.kyc_documents_config` | `settings.kyc_documents_view` |
| Plan Selection + EV Condition | `settings.plan_selection_with_condition_config` | `settings.plan_selection_with_condition_view` |
| Investment Plan | `settings.investment_plan_config` | `settings.investment_plan_view` |
| Swap Station Plan | `settings.swap_station_plan_config` | `settings.swap_station_plan_view` |
| Rider Request Plan for Merchant | `settings.rider_request_plan_for_merchant_config` | `settings.rider_request_plan_for_merchant_view` |
| Company Policy | `settings.company_policy_config` | `settings.company_policy_view` |
---
## Role Examples
### Admin
- Has **Config** + **View** for all settings sections
### Front Desk Officer
- Has **View** only (can see documents, plans, policies)
- Cannot edit any settings
### Biker / Rider
- Has **View** only for visible settings (plans, policies)
- Cannot access KYC document configuration
### Investor / Merchant / Shop
- Has **View** only for their relevant sections
- Cannot configure any settings
Next.js will automatically detect the changes to your next.config.ts and restart the server, but if it doesn't, just restart your npm run dev command manually. You should now see the message (pwa) PWA support is enabled in your terminal!
WARNING
A quick heads-up about developing with PWA enabled: Because the Service Worker aggressively caches files to make your app work offline, you might occasionally notice that your code changes don't immediately show up in the browser.
If that happens, you can easily bypass the cache by:
Opening Chrome DevTools (F12)
Going to the Application tab -> Service Workers
Clicking "Update on reload" or "Unregister"
Alternatively, keeping DevTools open and checking "Disable cache" in the Network tab usually prevents stale code while developing!

View File

@@ -1,4 +1,12 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import withPWAInit from "@ducanh2912/next-pwa";
const withPWA = withPWAInit({
dest: "public",
disable: false, // Explicitly enabling PWA even in development mode per user request
register: true,
skipWaiting: true,
});
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
images: { images: {
@@ -12,4 +20,4 @@ const nextConfig: NextConfig = {
}, },
}; };
export default nextConfig; export default withPWA(nextConfig);

3323
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,12 +3,13 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev --webpack",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"@ducanh2912/next-pwa": "^10.2.9",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"jspdf": "^4.2.1", "jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.7", "jspdf-autotable": "^5.0.7",

1
public/icon-192x192.svg Normal file
View File

@@ -0,0 +1 @@
<svg width="192" height="192" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="#2563eb"/><text x="50%" y="50%" fill="white" font-size="100" font-family="sans-serif" text-anchor="middle" dominant-baseline="middle">J</text></svg>

After

Width:  |  Height:  |  Size: 252 B

1
public/icon-512x512.svg Normal file
View File

@@ -0,0 +1 @@
<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="#2563eb"/><text x="50%" y="50%" fill="white" font-size="100" font-family="sans-serif" text-anchor="middle" dominant-baseline="middle">J</text></svg>

After

Width:  |  Height:  |  Size: 252 B

21
public/manifest.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "JAIBEN Mobility",
"short_name": "JAIBEN",
"description": "EV Rental Platform",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2563eb",
"icons": [
{
"src": "/icon-192x192.svg",
"sizes": "192x192",
"type": "image/svg+xml"
},
{
"src": "/icon-512x512.svg",
"sizes": "512x512",
"type": "image/svg+xml"
}
]
}

View File

@@ -1,4 +1,4 @@
import type { Metadata } from "next"; import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import "./globals.css"; import "./globals.css";
import Sidebar from "@/components/Sidebar"; import Sidebar from "@/components/Sidebar";
@@ -12,6 +12,20 @@ const inter = Inter({
export const metadata: Metadata = { export const metadata: Metadata = {
title: "JAIBEN Mobility - EV Rental Platform", title: "JAIBEN Mobility - EV Rental Platform",
description: "JAIBEN Mobility Ltd - EV Rental, Rent-to-Own, Share EV, FOCO Investor", description: "JAIBEN Mobility Ltd - EV Rental, Rent-to-Own, Share EV, FOCO Investor",
manifest: "/manifest.json",
appleWebApp: {
capable: true,
statusBarStyle: "default",
title: "JAIBEN",
},
};
export const viewport: Viewport = {
themeColor: "#ffffff",
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
}; };
export default function RootLayout({ export default function RootLayout({
@@ -20,10 +34,10 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en" className={inter.variable}> <html lang="en" className={inter.variable} suppressHydrationWarning>
<body className="min-h-screen bg-slate-50 antialiased"> <body className="min-h-screen bg-slate-50 antialiased" suppressHydrationWarning>
<Sidebar /> <Sidebar />
<main className="lg:ml-64 min-h-screen"> <main className="lg:ml-64 min-h-screen pb-20 lg:pb-0">
{children} {children}
</main> </main>
<Toaster position="top-right" /> <Toaster position="top-right" />

View File

@@ -76,21 +76,28 @@ export default function Sidebar() {
isInvestor ? investorNavItems : isInvestor ? investorNavItems :
isShop ? shopNavItems : bikerNavItems; isShop ? shopNavItems : bikerNavItems;
const toggleMenu = (label: string) => { const bottomNavItems = isAdmin ? [
setExpandedMenu(expandedMenu === label ? null : label); { label: 'Home', href: '/admin', icon: BarChart3 },
}; { label: 'Fleet', href: '/admin/fleet', icon: Bike },
{ label: 'Users', href: '/admin/users', icon: Users },
] : isInvestor ? [
{ label: 'Home', href: '/investor', icon: Wallet },
{ label: 'Portfolio', href: '/investor/portfolio', icon: BarChart3 },
{ label: 'Withdraw', href: '/investor/withdraw', icon: CreditCard },
] : isShop ? [
{ label: 'Home', href: '/shop', icon: Store },
{ label: 'Deliveries', href: '/shop/deliveries', icon: Truck },
{ label: 'Fleet', href: '/shop/fleet', icon: Bike },
] : [
{ label: 'Home', href: '/', icon: Bike },
{ label: 'Rent', href: '/rent', icon: Zap },
{ label: 'EVs', href: '/bikes', icon: Battery },
];
return ( return (
<> <>
<button
onClick={() => setMobileOpen(!mobileOpen)}
className="lg:hidden fixed top-4 left-4 z-50 p-2 bg-white rounded-lg shadow-md"
>
{mobileOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</button>
<aside className={` <aside className={`
fixed left-0 top-0 h-screen w-64 bg-white border-r border-slate-200 shadow-sm z-40 fixed left-0 top-0 h-screen w-64 bg-white border-r border-slate-200 shadow-sm z-50
transform transition-transform duration-300 ease-in-out transform transition-transform duration-300 ease-in-out
${mobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'} ${mobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
`}> `}>
@@ -100,13 +107,21 @@ export default function Sidebar() {
<h1 className="text-xl font-extrabold text-accent">JAIBEN</h1> <h1 className="text-xl font-extrabold text-accent">JAIBEN</h1>
<p className="text-xs text-slate-500">Mobility Ltd</p> <p className="text-xs text-slate-500">Mobility Ltd</p>
</div> </div>
<div className="px-2 py-1 bg-accent-light rounded text-xs font-semibold text-accent"> <div className="flex items-center gap-2">
{isAdmin ? 'Admin' : isInvestor ? 'Investor' : isShop ? 'Shop' : 'Biker'} <div className="px-2 py-1 bg-accent-light rounded text-xs font-semibold text-accent">
{isAdmin ? 'Admin' : isInvestor ? 'Investor' : isShop ? 'Shop' : 'Biker'}
</div>
<button
onClick={() => setMobileOpen(false)}
className="lg:hidden p-1 text-slate-400 hover:text-slate-600 rounded-lg hover:bg-slate-100"
>
<X className="w-5 h-5" />
</button>
</div> </div>
</div> </div>
</div> </div>
<nav className="p-3 space-y-1 overflow-y-auto h-[calc(100vh-140px)]"> <nav className="p-3 space-y-1 overflow-y-auto h-[calc(100vh-140px)] pb-24 lg:pb-3">
{navItems.map((item) => { {navItems.map((item) => {
const isActive = pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href)); const isActive = pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href));
const Icon = item.icon; const Icon = item.icon;
@@ -150,9 +165,37 @@ export default function Sidebar() {
</div> </div>
</aside> </aside>
{/* Bottom Navigation for Mobile */}
<nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-slate-200 flex justify-around items-center h-16 z-30 pb-safe shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]">
{bottomNavItems.map((item) => {
const isActive = pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href));
const Icon = item.icon;
return (
<Link
key={item.href}
href={item.href}
className={`
flex flex-col items-center justify-center w-full h-full gap-1 transition-colors
${isActive ? 'text-accent' : 'text-slate-500 hover:text-slate-900'}
`}
>
<Icon className={`w-5 h-5 ${isActive ? 'text-accent' : ''}`} />
<span className="text-[10px] font-medium">{item.label}</span>
</Link>
);
})}
<button
onClick={() => setMobileOpen(true)}
className="flex flex-col items-center justify-center w-full h-full gap-1 text-slate-500 hover:text-slate-900 transition-colors"
>
<Menu className="w-5 h-5" />
<span className="text-[10px] font-medium">Menu</span>
</button>
</nav>
{mobileOpen && ( {mobileOpen && (
<div <div
className="lg:hidden fixed inset-0 bg-black/30 z-30" className="lg:hidden fixed inset-0 bg-black/40 backdrop-blur-sm z-40 transition-opacity"
onClick={() => setMobileOpen(false)} onClick={() => setMobileOpen(false)}
/> />
)} )}