@rifatxtra/feature-kit
v1.0.6
Published
A collection of reusable React utilities, hooks, and components for Next.js and React projects.
Maintainers
Readme
@rifatxtra/feature-kit
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.
Installation
npm install @rifatxtra/feature-kitPeer Dependencies
npm install react react-dom lucide-reactImport Paths
| 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.
CLI Tool
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 BasicEditorbin/feature-kit.js
The 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"
}
}Project Structure
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 wrapperSetting up Framework-Agnostic Routing
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.
1. In Next.js (App Router)
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>
);
}2. In Vite (React Router v6+)
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>
);
}Utilities (src/utils/)
api.ts — Fetch Wrapper
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.
clipboard.ts — Clipboard Utilities
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();date.ts — Date Formatting
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"number.ts — Number & Currency Formatting
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.
string.ts — String Utilities
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"validation.ts — Form Validation
Exports: validatePhone, validateEmail, validatePassword, passwordRequirements
import {
validatePhone,
validateEmail,
validatePassword,
passwordRequirements,
} from "@rifatxtra/feature-kit/utils";
validatePhone("01712345678"); // true (Bangladeshi format)
validateEmail("[email protected]"); // 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),
}));storage.ts — LocalStorage Wrapper
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();performance.ts — Debounce & Throttle
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);imageCompressor.ts — Browser Image Compression
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"toast.ts — Validation Error Helpers
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"webVitals.ts — Core Web Vitals Monitoring
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).
Components (src/components/)
MainLayout.tsx -- App-Level Provider Wrapper
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).
Modal.tsx -- Context-Driven Modal
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.
Toast.tsx -- Toast Notification Container
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.
LoadingSpinner.tsx -- Full-Screen Loading Overlay
import { LoadingSpinner } from '@rifatxtra/feature-kit/components';
{isLoading && <LoadingSpinner />}
{isLoading && <LoadingSpinner color=#10b981 />}Props: color (default #6366f1). Fixed-position, full-screen, semi-transparent overlay.
Pagination.tsx -- Laravel-Compatible Paginator
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.
BasicEditor.tsx -- Rich Text Editor (Tiptap)
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.
ApexChart.tsx -- ApexCharts Wrapper
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.
SeoHead.tsx -- SEO Meta Tags (Fully Framework-Agnostic)
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.
PromoTemplates.tsx -- Promotional Modal Templates
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' });Contexts (src/contexts/)
ModalContext.tsx
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)ToastContext.tsx
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)RouterContext.tsx -- Framework-Agnostic Router Adapter
import {
RouterProvider,
useRouterAdapter,
} from "@rifatxtra/feature-kit/contexts";
import type { RouterAdapter } from "@rifatxtra/feature-kit/contexts";
// Next.js App Router:
("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>;
}
// Next.js Pages Router:
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).
Layouts (src/layouts/)
AdminLayoutClient.tsx -- Admin Dashboard Shell
Full admin UI: collapsible sidebar (icon-only to labeled), mobile overlay, auth+role guard, notifications bell, profile dropdown.
Passing Dynamic Sidebar Navigation Links
By default, the sidebar renders a pre-defined set of routes. You can pass fully customized routes, labels, and icons dynamically using the sidebarItems prop:
// app/(admin)/layout.tsx
"use client";
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, Settings, FileText, Sparkles } from "lucide-react";
export default function AdminLayout({ children }) {
const router = useRouter();
const pathname = usePathname();
// Define your custom sidebar links
const customSidebarLinks: 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 (
<RouterProvider
value={{ pathname, push: router.push, replace: router.replace, Link }}
>
<AdminLayoutClient sidebarItems={customSidebarLinks}>
{children}
</AdminLayoutClient>
</RouterProvider>
);
}Auth: On mount calls GET /api/me. Unauthorized -> clears rifatxtra_token, redirects /login. Role mismatch -> redirects to correct dashboard.
UserLayoutClient.tsx -- User Portal Shell
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.
NotificationsDropdown.tsx -- Bell Icon Dropdown
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.
NotificationsPage.tsx -- Paginated Notifications List
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.
NotificationDetailPage.tsx -- Single Notification View
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.
ProfilePage.tsx -- Profile and Password Forms
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.
Shared Pages (src/shared/)
Copy-paste Next.js App Router page stubs.
src/shared/notifications/page.tsx
Re-exports NotificationsPage. Copy to your app's notifications route.
src/shared/notifications/[id]/page.tsx
Re-exports NotificationDetailPage with params.id. Copy to your app's notification detail route.
src/shared/profile/page.tsx
Re-exports ProfilePage. Copy to your app's profile route.
Required Backend Endpoints
| 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 |
Full Setup Example (Next.js App Router)
// 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>
);
}Full Setup Example (React + Vite + React Router v6)
This library is fully framework-agnostic and works beautifully in a standard Single Page Application (SPA) environment with React, Vite, and React Router.
1. Root Setup (src/main.tsx)
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>
);2. Layout Adapter (src/layouts/AppAdminLayout.tsx)
Inject React Router v6 hooks into the framework-agnostic RouterProvider.
import React from 'react';
import { Outlet, useNavigate, useLocation, Link } from 'react-router-dom';
import { AdminLayoutClient } from '@rifatxtra/feature-kit/layouts';
import { RouterProvider } from '@rifatxtra/feature-kit/contexts';
export default function AppAdminLayout() {
const navigate = useNavigate();
const location = useLocation();
// Adapt React Router v6 to the layout adapter
const adapter = {
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={adapter}>
<AdminLayoutClient>
{/* Render child sub-routes here */}
<Outlet />
</AdminLayoutClient>
</RouterProvider>
);
}3. Feature Page Setup (src/pages/Dashboard.tsx)
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>
);
}Build System
rollup.config.js
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.
tsconfig.json
{
compilerOptions: {
target: ESNext, module: ESNext,
jsx: react-jsx, declaration: true,
emitDeclarationOnly: true, strict: false,
esModuleInterop: true
},
include: [src/**/*]
}License
MIT (c) rifatxtra
