A collection of reusable React utilities, hooks, and components for Next.js and React projects. Supports both ESM and CJS. Works with the App Router and Pages Router.
npm install @rifatxtra/feature-kitnpm install react react-dom lucide-react| Path | Contents |
|---|---|
@rifatxtra/feature-kit |
Everything (main barrel) |
@rifatxtra/feature-kit/utils |
Utility functions only |
@rifatxtra/feature-kit/components |
UI components only |
@rifatxtra/feature-kit/contexts |
React contexts & hooks only |
@rifatxtra/feature-kit/layouts |
Full-page layout shells only |
Tip: Import from sub-paths for better tree-shaking.
The package ships with a CLI to copy source files directly into your project.
# Initialize (choose JS or TS)
npx feature-kit init
# Add one or more components interactively
npx feature-kit add
# Add specific components by name
npx feature-kit add Modal Toast BasicEditorThe CLI binary. Powered by commander, prompts, and chalk.
| Command | Description |
|---|---|
init |
Creates feature-kit.json with language & paths config |
add [components...] |
Copies component source files into your project |
feature-kit.json example (auto-generated by init):
{
"language": "ts",
"paths": {
"components": "./src/components",
"contexts": "./src/contexts",
"utils": "./src/utils"
}
}src/
├── index.ts ← main barrel (re-exports everything)
├── utils/
│ ├── api.ts ← fetch wrapper with auth
│ ├── clipboard.ts ← clipboard read/write
│ ├── date.ts ← date formatting helpers
│ ├── imageCompressor.ts ← browser-side image compression
│ ├── number.ts ← currency, percent, bytes formatting
│ ├── performance.ts ← debounce & throttle
│ ├── storage.ts ← localStorage wrapper with expiry
│ ├── string.ts ← slug, truncate, capitalize, random
│ ├── toast.ts ← validation error helpers
│ ├── validation.ts ← phone, email, password validators
│ ├── webVitals.ts ← Core Web Vitals monitoring
│ └── index.ts ← utils barrel
├── components/
│ ├── ApexChart.tsx ← dynamic ApexCharts wrapper
│ ├── BasicEditor.tsx ← Tiptap rich text editor
│ ├── LoadingSpinner.tsx ← full-screen loading overlay
│ ├── MainLayout.tsx ← app-level provider wrapper
│ ├── Modal.tsx ← context-driven modal popup
│ ├── Pagination.tsx ← Laravel-compatible paginator
│ ├── PromoTemplates.tsx ← ready-made promotional modals
│ ├── SeoHead.tsx ← SEO meta tags (Pages Router)
│ ├── Toast.tsx ← toast notification container
│ └── index.ts ← components barrel
├── contexts/
│ ├── ModalContext.tsx ← modal state & actions context
│ ├── RouterContext.tsx ← framework-agnostic router adapter
│ ├── ToastContext.tsx ← toast state & actions context
│ └── index.ts ← contexts barrel
├── layouts/
│ ├── AdminLayoutClient.tsx ← full admin dashboard shell
│ ├── UserLayoutClient.tsx ← user portal shell
│ ├── index.ts ← layouts barrel
│ └── components/
│ ├── NotificationsDropdown.tsx ← bell icon dropdown
│ ├── NotificationsPage.tsx ← paginated notifications list
│ ├── NotificationDetailPage.tsx ← single notification view
│ └── ProfilePage.tsx ← profile & password update forms
└── shared/
├── notifications/page.tsx ← Next.js page wrapper
├── notifications/[id]/page.tsx ← Next.js dynamic page wrapper
└── profile/page.tsx ← Next.js page wrapper
The components and layouts in this kit are designed to work with both Next.js and Vite (React Router) out of the box. They use a custom RouterProvider to inject the routing logic.
Here is how you set them up for your setup. Wrap your application with the appropriate provider below.
Create a Client component (e.g., NextRouterProvider.tsx) and wrap your layout.tsx with it:
"use client";
import React from "react";
import { usePathname, useRouter } from "next/navigation";
import Link from "next/link";
import { RouterProvider } from "@rifatxtra/feature-kit/contexts";
export function NextRouterProvider({
children,
}: {
children: React.ReactNode;
}) {
const pathname = usePathname();
const router = useRouter();
return (
<RouterProvider
value={{
pathname: pathname || "",
push: (href) => router.push(href),
replace: (href) => router.replace(href),
Link: ({ href, className, children, onClick }) => (
<Link href={href} className={className} onClick={onClick}>
{children}
</Link>
),
}}
>
{children}
</RouterProvider>
);
}Wrap your app right inside of your <BrowserRouter>:
import React from "react";
import { useLocation, useNavigate, Link } from "react-router-dom";
import { RouterProvider } from "@rifatxtra/feature-kit/contexts";
export function ViteRouterProvider({
children,
}: {
children: React.ReactNode;
}) {
const location = useLocation();
const navigate = useNavigate();
return (
<RouterProvider
value={{
pathname: location.pathname,
push: (href) => navigate(href),
replace: (href) => navigate(href, { replace: true }),
Link: ({ href, className, children, onClick }) => (
<Link to={href} className={className} onClick={onClick}>
{children}
</Link>
),
}}
>
{children}
</RouterProvider>
);
}Auto-discovers your base URL from env vars and attaches the Bearer token from localStorage.
Exports: getApiBaseUrl, api
import { api } from "@rifatxtra/feature-kit/utils";
// GET request
const data = await api("/users");
// POST with body
const result = await api("/posts", {
method: "POST",
body: JSON.stringify({ title: "Hello" }),
});
// File upload (FormData — Content-Type set automatically)
const formData = new FormData();
formData.append("file", file);
await api("/upload", { method: "POST", body: formData });Base URL resolution order:
window.__FEATURE_KIT_API_URL__(runtime override)process.env.NEXT_PUBLIC_API_URL(Next.js)process.env.NEXT_PUBLIC_APP_URL(Next.js fallback)import.meta.env.VITE_API_URL(Vite)import.meta.env.VITE_APP_URL(Vite fallback)- Falls back to
/api(relative)
Auth token key: rifatxtra_token in localStorage.
Exports: copyToClipboard, copyWithToast, readFromClipboard
import {
copyToClipboard,
copyWithToast,
readFromClipboard,
} from "@rifatxtra/feature-kit/utils";
// Copy text
const ok = await copyToClipboard("https://example.com");
// Copy and show a toast callback
await copyWithToast("Hello!", (msg) => alert(msg));
// Read from clipboard (requires browser permission)
const text = await readFromClipboard();Exports: formatDate, formatDateTime, timeAgo
import {
formatDate,
formatDateTime,
timeAgo,
} from "@rifatxtra/feature-kit/utils";
formatDate("2024-01-15"); // "January 15, 2024"
formatDate("2024-01-15", "de-DE"); // "15. Januar 2024"
formatDateTime("2024-01-15T10:30:00"); // "January 15, 2024 at 10:30 AM"
timeAgo("2024-01-10"); // "5 days ago"
timeAgo(new Date()); // "just now"Exports: formatNumber, formatCurrency, formatPercent, formatBytes, compactNumber
import {
formatNumber,
formatCurrency,
formatPercent,
formatBytes,
compactNumber,
} from "@rifatxtra/feature-kit/utils";
formatNumber(1234567); // "1,234,567"
formatNumber(1234.5, "en-US", 2); // "1,234.50"
formatCurrency(99.99); // "$99.99"
formatCurrency(99.99, "euro", "de-DE"); // "99,99 €"
formatCurrency(1500, "taka"); // "৳1,500.00"
formatPercent(0.175, 1); // "17.5%"
formatBytes(1536); // "1.50 KB"
formatBytes(1048576); // "1.00 MB"
compactNumber(12500); // "12.5K"
compactNumber(1200000); // "1.2M"Supported currency names: dollar, euro, pound, yen, yuan, rupee, taka, real, ruble, won, peso, franc, krona, riyal, dirham — or any ISO 4217 code.
Exports: slugify, truncate, capitalize, randomString
import {
slugify,
truncate,
capitalize,
randomString,
} from "@rifatxtra/feature-kit/utils";
slugify("Hello World! 123"); // "hello-world-123"
truncate("A very long sentence...", 20); // "A very long sentence..."
capitalize("hello world"); // "Hello World"
randomString(16); // "aB3xKz9mQpR2nWsL"Exports: validatePhone, validateEmail, validatePassword, passwordRequirements
import {
validatePhone,
validateEmail,
validatePassword,
passwordRequirements,
} from "@rifatxtra/feature-kit/utils";
validatePhone("01712345678"); // true (Bangladeshi format)
validateEmail("user@example.com"); // true
validatePassword("Abcd@1234"); // true (8-15 chars, upper, lower, number, symbol)
// Display password strength rules in UI
passwordRequirements.map((req) => ({
label: req.label, // "8-15 Characters", "Uppercase Letter", etc.
met: req.regex.test(password),
}));Exports (default object): storage.set, storage.get, storage.remove, storage.clear, storage.setWithExpiry, storage.getWithExpiry, storage.has, storage.keys
import storage from "@rifatxtra/feature-kit/utils";
storage.set("user", { name: "Alice", role: "admin" });
const user = storage.get("user", {}); // auto-parsed
storage.set("theme", "dark");
const theme = storage.get("theme", "light");
// Expire in 1 hour (3600 seconds)
storage.setWithExpiry("token", "abc123", 3600);
const token = storage.getWithExpiry("token"); // null if expired
storage.has("user"); // true / false
storage.keys(); // ['user', 'theme', 'token']
storage.remove("theme");
storage.clear();Exports: debounce, throttle
import { debounce, throttle } from "@rifatxtra/feature-kit/utils";
// Wait 300ms after last keystroke before firing
const handleSearch = debounce((query: string) => {
fetch(`/api/search?q=${query}`);
}, 300);
// Scroll handler fires at most once every 100ms
const handleScroll = throttle(() => {
console.log("scrolled");
}, 100);
window.addEventListener("scroll", handleScroll);Exports: compressImage, compressImages, generateResponsiveImages, getImageDimensions, formatFileSize
import {
compressImage,
compressImages,
generateResponsiveImages,
getImageDimensions,
formatFileSize,
} from "@rifatxtra/feature-kit/utils";
// Compress single file (max 1920×1080, 80% quality, output JPEG)
const compressed = await compressImage(file, {
maxWidth: 1920,
maxHeight: 1080,
quality: 0.8,
type: "image/jpeg",
});
// Compress multiple files
const files = await compressImages(fileList, { quality: 0.7 });
// Generate 3 responsive sizes for srcset
const { small, medium, large } = await generateResponsiveImages(file);
// small = max 640px, medium = max 1280px, large = max 1920px
// Get original dimensions
const { width, height } = await getImageDimensions(file);
// Format file size for display
formatFileSize(1536000); // "1.46 MB"Exports: TOAST_TYPES, formatValidationErrors, hasErrors, getError
import {
TOAST_TYPES,
formatValidationErrors,
hasErrors,
getError,
} from "@rifatxtra/feature-kit/utils";
const errors = { email: ["Invalid email"], name: ["Required"] };
formatValidationErrors(errors); // "Invalid email, Required"
hasErrors(errors); // true
hasErrors(errors, "email"); // true
getError(errors, "email"); // "Invalid email"
// Use TOAST_TYPES as constants
TOAST_TYPES.SUCCESS; // "success"
TOAST_TYPES.ERROR; // "error"
TOAST_TYPES.WARNING; // "warning"
TOAST_TYPES.INFO; // "info"Exports: initWebVitals, getWebVitals
Requires: npm install web-vitals
import { initWebVitals, getWebVitals } from "@rifatxtra/feature-kit/utils";
// In your root layout / _app.js
initWebVitals({
logToConsole: true, // dev: logs to console with emoji ratings
sendToServer: true, // prod: POSTs to your analytics endpoint
endpoint: "/api/vitals", // default: '/api/analytics/vitals'
onMetric: (metric) => {
// optional custom callback
console.log(metric.name, metric.value, metric.rating);
},
});
// Snapshot all 5 vitals at once (resolves after all fire or 3s timeout)
const vitals = await getWebVitals();
// vitals.lcp, vitals.fid, vitals.cls, vitals.fcp, vitals.ttfbTracked metrics: LCP (Largest Contentful Paint), FID (First Input Delay), CLS (Cumulative Layout Shift), FCP (First Contentful Paint), TTFB (Time to First Byte).
Wraps your app with ModalProvider, ToastProvider, Modal, and ToastContainer in one shot.
// Next.js App Router -- app/layout.tsx
import { MainLayout } from '@rifatxtra/feature-kit/components';
export default function RootLayout({ children }) {
return (
<html><body>
<MainLayout toastPosition=top-right>{children}</MainLayout>
</body></html>
);
}Props: children (required), toastPosition (default top-right).
Must be inside ModalProvider (included in MainLayout).
import { useModal } from "@rifatxtra/feature-kit/contexts";
function DeleteButton() {
const { openModal, closeModal } = useModal();
return (
<button
onClick={() =>
openModal(
<div>
<h2>Confirm?</h2>
<button onClick={closeModal}>Cancel</button>
<button
onClick={() => {
doDelete();
closeModal();
}}
>
Delete
</button>
</div>,
{ size: "sm", closeOnOverlay: true, showCloseButton: true },
)
}
>
Delete
</button>
);
}openModal size options: sm=480px, md=560px, lg=768px, xl=1024px, full=100vw-32px. Closes on Escape key automatically.
import { useToast } from "@rifatxtra/feature-kit/contexts";
function Form() {
const toast = useToast();
return (
<button
onClick={async () => {
try {
await save();
toast.success("Saved!");
} catch (e) {
toast.error(e.message);
}
}}
>
Save
</button>
);
}Methods: toast.success(msg, duration?), toast.error(), toast.warning(), toast.info(), toast.addToast(msg, type?, duration?), toast.removeToast(id). Position values: top-right, top-left, bottom-right, bottom-left, top-center. Default duration: 5000ms.
import { LoadingSpinner } from '@rifatxtra/feature-kit/components';
{isLoading && <LoadingSpinner />}
{isLoading && <LoadingSpinner color=#10b981 />}Props: color (default #6366f1). Fixed-position, full-screen, semi-transparent overlay.
import { Pagination } from "@rifatxtra/feature-kit/components";
// Laravel paginator shape: { from, to, total, links: [{ url, label, active }] }
<Pagination links={apiResponse} onPageChange={(url) => router.push(url)} />;Renders nothing when total pages <= 1. Shows Showing X to Y of Z results.
import { BasicEditor } from "@rifatxtra/feature-kit/components";
function PostForm() {
const [html, setHtml] = useState("");
return <BasicEditor value={html} onChange={setHtml} />;
}Props: value (HTML string, default "), onChange: (html: string) => void (required).
Toolbar: Bold, Italic, 8-color text palette, H1/H2/H3, Bullet/Ordered lists, Left/Center/Right/Justify align, Insert/Remove links, Insert image by URL, Upload+compress local image, Insert 3x3 table with contextual row/column controls.
Lazy-loads apexcharts only in browser. Requires: npm install apexcharts.
import { ApexChart } from '@rifatxtra/feature-kit/components';
<ApexChart
type=line
height={350}
series={[{ name: 'Revenue', data: [30, 40, 35, 50, 49] }]}
options={{ xaxis: { categories: ['Jan','Feb','Mar','Apr','May'] } }}
/>Props: type (default line), series (default []), options (default {}), height (default 350). Supported types: line, bar, area, pie, donut, radar, etc.
A robust SEO meta tag manager. In Next.js Pages Router, it automatically delegates to next/head. In standard React + Vite, Create React App, or React Router SPAs, it uses a client-side useEffect fallback to dynamically update document title and upsert <meta> head tags.
import { SeoHead } from '@rifatxtra/feature-kit/components';
export default function BlogPost({ post }) {
return (
<>
<SeoHead
title={post.title}
description={post.excerpt}
keywords="react, nextjs, vite, spa"
image={post.cover}
appName="My Blog"
type="article"
/>
<main>{post.content}</main>
</>
);
}Sets: <title>, description, keywords, Open Graph tags (og:title, og:description, og:image, og:type), and Twitter Cards.
Exports: ImagePromo, BannerPromo, CountdownPromo, EmailCapturePromo, GalleryPromo. All require ModalProvider.
import { useModal } from '@rifatxtra/feature-kit/contexts';
import { ImagePromo, BannerPromo, CountdownPromo, EmailCapturePromo, GalleryPromo } from '@rifatxtra/feature-kit/components';
const { openModal } = useModal();
// Full-width image + CTA
openModal(<ImagePromo imageUrl=/sale.jpg title=Sale! description=50% off ctaText=Shop Now ctaLink=/shop />, { size: 'md' });
// Side-by-side image + feature bullets
openModal(<BannerPromo imageUrl=/pro.jpg title=Pro Plan features={['Unlimited', 'Support']} ctaText=Upgrade ctaLink=/pricing />, { size: 'lg' });
// Live countdown timer (auto-expires)
openModal(<CountdownPromo imageUrl=/flash.jpg title=Sale Ends: endTime=2025-12-31T23:59:59 ctaText=Buy Now ctaLink=/deals />, { size: 'md' });
// Email lead capture
openModal(<EmailCapturePromo title=Get 20% Off subtitle=Join newsletter buttonText=Subscribe onSubmit={async (email) => await subscribe(email)} />, { size: 'sm' });
// Image carousel
openModal(<GalleryPromo images={['/img1.jpg', '/img2.jpg']} title=Portfolio ctaText=View All ctaLink=/portfolio />, { size: 'lg' });Uses useSyncExternalStore for surgical re-renders.
import {
ModalProvider,
useModal,
useModalState,
} from "@rifatxtra/feature-kit/contexts";
<ModalProvider>{children}</ModalProvider>;
const { openModal, closeModal } = useModal(); // actions (stable)
const { isOpen, content, options } = useModalState(); // state (used by Modal)import {
ToastProvider,
useToast,
useToastsState,
} from "@rifatxtra/feature-kit/contexts";
<ToastProvider>{children}</ToastProvider>;
const toast = useToast();
toast.success("Done!");
const id = toast.error("Failed!", 3000); // custom 3s duration
toast.removeToast(id); // dismiss programmatically
const { toasts } = useToastsState(); // read list (used by ToastContainer)💡 Zero Configuration Fallback: Wrapping your layouts with a
<RouterProvider>is 100% optional. If omitted, all links and navigation across layouts, dropdowns, and pages will automatically fall back to standard HTML<a>tags and native browser navigation (window.location). Wrapping is only needed to map framework-specific SPA routing (e.g. Next.js, React Router) for client-side transitions.
import { RouterProvider, useRouterAdapter } from "@rifatxtra/feature-kit/contexts";
import type { RouterAdapter } from "@rifatxtra/feature-kit/contexts";
// Option A: Next.js App Router Setup
"use client";
import { useRouter, usePathname } from "next/navigation";
import Link from "next/link";
export default function AppRouterProvider({ children }) {
const router = useRouter();
const pathname = usePathname();
const adapter: RouterAdapter = {
pathname,
push: router.push,
replace: router.replace,
Link,
};
return <RouterProvider value={adapter}>{children}</RouterProvider>;
}
// Option B: React + Vite (React Router v6) Setup
import { useNavigate, useLocation, Link } from "react-router-dom";
export default function ViteRouterProvider({ children }) {
const navigate = useNavigate();
const location = useLocation();
const adapter: RouterAdapter = {
pathname: location.pathname,
push: (href) => navigate(href),
replace: (href) => navigate(href, { replace: true }),
Link: ({ href, className, children, onClick }: any) => (
<Link to={href} className={className} onClick={onClick}>
{children}
</Link>
),
};
return <RouterProvider value={adapter}>{children}</RouterProvider>;
}
// Option C: Next.js Pages Router Setup
import { useRouter } from "next/router";
import Link from "next/link";
export default function PagesRouterProvider({ children }) {
const { pathname, push, replace } = useRouter();
const adapter: RouterAdapter = {
pathname,
push: (h) => push(h),
replace: (h) => replace(h),
Link,
};
return <RouterProvider value={adapter}>{children}</RouterProvider>;
}
// Consume inside any layout component:
const { pathname, push, replace, Link } = useRouterAdapter();RouterAdapter interface: pathname (string), push(href), replace(href), Link (React component).
Note
Do I need to build a new layout from scratch? NO! The entire admin panel structure—collapsible sidebars, desktop/mobile responsive grids, sliding mobile drawers, profile dropdowns, and notification feeds—is 100% pre-built and styled inside the library. You don't have to code any complex layout logic.
You only need to create a simple Layout Wrapper file (e.g. layout.tsx in your Next.js folder). This acts as a routing adapter to bind Next.js's single-page click handlers to the pre-built library shell, and provides a target context ({children}) to render your sub-pages.
A professional admin panel with a collapsible sidebar, integrated notification dropdown, role-based auth protection, profile navigation, and fully dynamic sidebar customisation.
The AdminLayoutClient accepts an optional array of sidebarItems conforming to the SidebarNavItem interface:
export interface SidebarNavItem {
href: string; // The absolute URL path
label: string; // Link text visible in expanded mode
icon: React.ComponentType<any> | React.ReactNode; // The icon component or elements
}You can pass icons to the icon field in two convenient ways:
- Icon Components: Pass the uninstantiated component reference directly from an icon package (e.g.
icon: LayoutDashboardfromlucide-react). The layout will automatically instantiate it, setting proper size properties. - Pre-rendered Elements: Pass an already instantiated React element or custom inline SVG (e.g.
icon: <LayoutDashboard size={25} />or<svg>...</svg>). The layout will render it verbatim.
You don't need to manually calculate active paths. The layout automatically tracks pathname from the router context:
- It highlights a link as active if it is an exact match (
pathname === item.href). - It automatically flags sub-routes as active as well (e.g.
/admin/works/newwill highlight a menu item pointing to/admin/works), as long as the parent path is not/.
The layout maps the custom <Link> element provided inside the RouterProvider context. This means that whether you are in Next.js (next/link) or Vite (react-router-dom), the sidebar clicks will perform instant, single-page transitions without full page reloads. If no router is provided, it falls back to standard HTML <a> tags gracefully.
// app/(admin)/layout.tsx
"use client";
import React from "react";
import { AdminLayoutClient, SidebarNavItem } from "@rifatxtra/feature-kit/layouts";
import { RouterProvider } from "@rifatxtra/feature-kit/contexts";
import { useRouter, usePathname } from "next/navigation";
import Link from "next/link";
import { LayoutDashboard, Sparkles, FileText, Settings } from "lucide-react";
export default function AdminLayout({ children }) {
const router = useRouter();
const pathname = usePathname();
// 1. Construct the customized links array
const adminLinks: SidebarNavItem[] = [
{ href: "/admin/dashboard", label: "Overview", icon: LayoutDashboard },
{ href: "/admin/analytics", label: "Performance", icon: Sparkles },
{ href: "/admin/reports", label: "Monthly Reports", icon: FileText },
{ href: "/admin/settings", label: "Preferences", icon: Settings },
];
return (
// 2. Wrap in RouterProvider to enable single-page transitions
<RouterProvider
value={{ pathname, push: router.push, replace: router.replace, Link }}
>
{/* 3. Pass custom sidebar links directly */}
<AdminLayoutClient sidebarItems={adminLinks}>
{children}
</AdminLayoutClient>
</RouterProvider>
);
}Upon mounting, the component makes a secure GET /api/me call.
- If 401/Unauthorized: It automatically clears the
rifatxtra_tokenfrom local storage and redirects the user to/login. - If Role Mismatch: If an admin is on a user path, or a user is on an admin path, it dynamically redirects them to their corresponding dashboard home.
Identical to AdminLayoutClient but customized for users. Supports the same sidebarItems prop for fully dynamic sidebar navigation links.
// app/(user)/layout.tsx
"use client";
import { UserLayoutClient, SidebarNavItem } from "@rifatxtra/feature-kit/layouts";
import { RouterProvider } from "@rifatxtra/feature-kit/contexts";
import { useRouter, usePathname } from "next/navigation";
import Link from "next/link";
import { LayoutDashboard, Wrench, ShieldAlert } from "lucide-react";
export default function UserLayout({ children }) {
const router = useRouter();
const pathname = usePathname();
const customUserLinks: SidebarNavItem[] = [
{ href: "/user/dashboard", label: "My Hub", icon: LayoutDashboard },
{ href: "/user/tickets", label: "Support Tickets", icon: ShieldAlert },
{ href: "/user/setup", label: "Tools", icon: Wrench },
];
return (
<RouterProvider
value={{ pathname, push: router.push, replace: router.replace, Link }}
>
<UserLayoutClient sidebarItems={customUserLinks}>
{children}
</UserLayoutClient>
</RouterProvider>
);
}Sidebar defaults: Dashboard (/user/dashboard), My Services (/user/my-services). Both layouts expect a /logo.png image file in your public directory.
Used inside both layout headers. Can be used standalone.
import { NotificationsDropdown } from "@rifatxtra/feature-kit/layouts";
<NotificationsDropdown />;API: GET /api/notifications?limit=10, POST /api/notifications/:id/read, POST /api/notifications/read-all. Animated pulse badge for unread count. Auto-routes to /admin/notifications/:id or /user/notifications/:id based on pathname.
import { NotificationsPage } from "@rifatxtra/feature-kit/layouts";
// Place at /admin/notifications or /user/notifications:
export default function Page() {
return <NotificationsPage />;
}API: GET /api/notifications?page=N. Expected: { status: success, data: { data: [...], last_page: 5 } }. Features: mark one read, mark all read, prev/next pagination.
import { NotificationDetailPage } from "@rifatxtra/feature-kit/layouts";
// app/(admin)/admin/notifications/[id]/page.tsx
export default function Page({ params }) {
return <NotificationDetailPage id={params.id} />;
}Props: id (string from URL). API: GET /api/notifications/:id. Renders HTML message. Shows action button if action_url is present. Redirects to list if not found.
import { ProfilePage } from "@rifatxtra/feature-kit/layouts";
// app/(admin)/admin/profile/page.tsx
export default function Page() {
return <ProfilePage />;
}API calls: GET /api/me (fetch name/email), PUT /api/profile/info { name, email }, PUT /api/profile/password { current_password, password, password_confirmation }. Requires ToastProvider for feedback.
Copy-paste Next.js App Router page stubs.
Re-exports NotificationsPage. Copy to your app's notifications route.
Re-exports NotificationDetailPage with params.id. Copy to your app's notification detail route.
Re-exports ProfilePage. Copy to your app's profile route.
| Endpoint | Method | Description |
|---|---|---|
| /api/me | GET | { status, data: { role, name, email } } |
| /api/logout | POST | Invalidates session |
| /api/notifications | GET | Paginated list (?page=N&limit=N) |
| /api/notifications/:id | GET | Single notification |
| /api/notifications/:id/read | POST | Mark as read |
| /api/notifications/read-all | POST | Mark all as read |
| /api/profile/info | PUT | Update name + email |
| /api/profile/password | PUT | Change password |
// app/layout.tsx
import { MainLayout } from '@rifatxtra/feature-kit/components';
export default function RootLayout({ children }) {
return <html><body><MainLayout>{children}</MainLayout></body></html>;
}
// app/(admin)/layout.tsx
'use client';
import { AdminLayoutClient } from '@rifatxtra/feature-kit/layouts';
import { RouterProvider } from '@rifatxtra/feature-kit/contexts';
import { useRouter, usePathname } from 'next/navigation';
import Link from 'next/link';
export default function AdminLayout({ children }) {
const router = useRouter(); const pathname = usePathname();
return (
<RouterProvider value={{ pathname, push: router.push, replace: router.replace, Link }}>
<AdminLayoutClient>{children}</AdminLayoutClient>
</RouterProvider>
);
}
// app/(admin)/admin/dashboard/page.tsx
'use client';
import { useModal, useToast } from '@rifatxtra/feature-kit/contexts';
import { ApexChart, BasicEditor } from '@rifatxtra/feature-kit/components';
import { formatCurrency, timeAgo } from '@rifatxtra/feature-kit/utils';
import storage from '@rifatxtra/feature-kit/utils';
export default function Dashboard() {
const { openModal } = useModal();
const toast = useToast();
return (
<div>
<p>Revenue: {formatCurrency(12500, 'taka')}</p>
<p>Last sync: {timeAgo('2024-01-10')}</p>
<ApexChart type=area series={[{ name: 'Orders', data: [30,40,35,50] }]} options={{}} />
<button onClick={() => toast.success('Synced!')}>Sync</button>
<button onClick={() => openModal(<p>Hello!</p>, { size: 'sm' })}>Modal</button>
</div>
);
}This library is fully framework-agnostic and works beautifully in a standard Single Page Application (SPA) environment with React, Vite, and React Router.
Wrap your application in MainLayout to boot the centralized modal and toast engines.
import React from 'react';
import ReactDOM from 'react-dom/client';
import { MainLayout } from '@rifatxtra/feature-kit/components';
import App from './App';
import './index.css'; // Your Tailwind CSS or Custom CSS
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<MainLayout toastPosition="top-right">
<App />
</MainLayout>
</React.StrictMode>
);Note
Do I need to build a new layout from scratch? NO! The entire admin panel structure—collapsible sidebars, desktop/mobile responsive grids, sliding mobile drawers, profile dropdowns, and notification feeds—is 100% pre-built and styled inside the library. You don't have to code any complex layout logic.
You only need to create a simple Layout Wrapper file (AppAdminLayout.tsx) in your project. This acts as a routing adapter to bind React Router's single-page click handlers to the pre-built library shell, and provides a target context (<Outlet />) to render your sub-routes.
Here is the exact way to set up this lightweight wrapper in your Vite application:
import React from 'react';
import { Outlet, useNavigate, useLocation, Link } from 'react-router-dom';
import { AdminLayoutClient, SidebarNavItem } from '@rifatxtra/feature-kit/layouts';
import { RouterProvider } from '@rifatxtra/feature-kit/contexts';
// Import any icons you want to use from lucide-react
import { LayoutDashboard, Briefcase, Mail, Settings } from 'lucide-react';
export default function AppAdminLayout() {
const navigate = useNavigate();
const location = useLocation();
// 1. Define custom, dynamic navigation links
// The layout will render these in order in the sidebar
const customSidebarLinks: SidebarNavItem[] = [
{ href: '/admin', label: 'Dashboard', icon: LayoutDashboard },
{ href: '/admin/works', label: 'Works Portfolio', icon: Briefcase },
{ href: '/admin/messages', label: 'Inquiries', icon: Mail },
{ href: '/admin/settings', label: 'Preferences', icon: Settings },
];
// 2. Simple router adapter (hooks React Router v6 to the Layout's inner clicks)
const routerAdapter = {
pathname: location.pathname,
push: (href: string) => navigate(href),
replace: (href: string) => navigate(href, { replace: true }),
Link: ({ href, className, children, onClick }: any) => (
<Link to={href} className={className} onClick={onClick}>
{children}
</Link>
),
};
return (
<RouterProvider value={routerAdapter}>
{/* 3. Pass custom sidebar links directly as props */}
<AdminLayoutClient sidebarItems={customSidebarLinks}>
{/* The children of this route (pages) will be injected dynamically inside Outlet */}
<Outlet />
</AdminLayoutClient>
</RouterProvider>
);
}- No Page Reloads: By mapping Vite's
<Link>component insiderouterAdapter.Link, the sidebar navigation acts as a seamless client-side SPA with high-fidelity transitions. - Auto-Active Tabs: The layout will automatically inspect
location.pathnameand set the correct active tab styles. It will even match sub-paths (e.g./admin/works/createwill correctly highlight the/admin/workstab). - Collapsible States & Mobile Support: The sidebar handles toggle events, collapses to a neat icon-only format, and triggers slide-out panels on mobile screen sizes natively.
To actually apply your new AppAdminLayout wrapper inside your application, declare it as the parent <Route> element. Any nested child route will automatically render inside the layout's <Outlet /> container:
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import AppAdminLayout from './layouts/AppAdminLayout';
import Dashboard from './pages/Dashboard';
// Dummy secondary pages
const Works = () => <h1 className="text-xl font-bold">Works Portfolio</h1>;
const Inquiries = () => <h1 className="text-xl font-bold">Client Inquiries</h1>;
const Settings = () => <h1 className="text-xl font-bold">Preferences Setup</h1>;
export default function App() {
return (
<BrowserRouter>
<Routes>
{/* 1. Declare the pre-built layout wrapper as the parent element */}
<Route path="/admin" element={<AppAdminLayout />}>
{/* 2. Map all sub-routes here. They render inside the layout shell! */}
<Route index element={<Dashboard />} />
<Route path="works" element={<Works />} />
<Route path="messages" element={<Inquiries />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
</BrowserRouter>
);
}import React from 'react';
import { useModal, useToast } from '@rifatxtra/feature-kit/contexts';
import { ApexChart } from '@rifatxtra/feature-kit/components';
import { formatCurrency, timeAgo } from '@rifatxtra/feature-kit/utils';
export default function Dashboard() {
const { openModal } = useModal();
const toast = useToast();
const showDemoModal = () => {
openModal(
<div className="p-4">
<h3 className="text-lg font-bold">Vite SPA Action!</h3>
<p className="mt-2 text-sm text-gray-500">
centralesed state modal working seamlessly.
</p>
</div>,
{ size: 'sm' }
);
};
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">Admin Dashboard</h1>
<p className="text-gray-600">Total balance: {formatCurrency(48250.75, 'USD')}</p>
<p className="text-xs text-gray-400">Data compiled {timeAgo(new Date())}</p>
<div className="bg-white p-6 rounded-2xl border border-gray-100 shadow-sm">
<ApexChart
type="area"
series={[{ name: 'Sales', data: [31, 40, 28, 51, 42, 109, 100] }]}
options={{
colors: ['#3b82f6'],
stroke: { curve: 'smooth' }
}}
/>
</div>
<div className="flex gap-3">
<button
onClick={() => toast.success('Vite toast fired!')}
className="px-4 py-2 bg-blue-600 text-white rounded-xl shadow-md hover:bg-blue-700 transition"
>
Fire Toast
</button>
<button
onClick={showDemoModal}
className="px-4 py-2 bg-gray-900 text-white rounded-xl shadow-md hover:bg-gray-800 transition"
>
Open Modal
</button>
</div>
</div>
);
}Multi-entry Rollup build producing ESM + CJS + .d.ts for each sub-path.
| Entry | ESM | CJS | Types |
|---|---|---|---|
| src/index.ts | dist/index.esm.js | dist/index.cjs.js | dist/index.d.ts |
| src/utils/index.ts | dist/utils.esm.js | dist/utils.cjs.js | dist/utils.d.ts |
| src/components/index.ts | dist/components.esm.js | dist/components.cjs.js | dist/components.d.ts |
| src/contexts/index.ts | dist/contexts.esm.js | dist/contexts.cjs.js | dist/contexts.d.ts |
| src/layouts/index.ts | dist/layouts.esm.js | dist/layouts.cjs.js | dist/layouts.d.ts |
npm run build # one-time production build
npm run build:watch # watch mode during developmentExternalized (not bundled): react, react-dom, lucide-react, all @tiptap/*, apexcharts, web-vitals.
{
compilerOptions: {
target: ESNext, module: ESNext,
jsx: react-jsx, declaration: true,
emitDeclarationOnly: true, strict: false,
esModuleInterop: true
},
include: [src/**/*]
}MIT (c) rifatxtra