npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rifatxtra/feature-kit

v1.0.6

Published

A collection of reusable React utilities, hooks, and components for Next.js and React projects.

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.

npm version License: MIT


Installation

npm install @rifatxtra/feature-kit

Peer Dependencies

npm install react react-dom lucide-react

Import 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 BasicEditor

bin/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 wrapper

Setting 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:

  1. window.__FEATURE_KIT_API_URL__ (runtime override)
  2. process.env.NEXT_PUBLIC_API_URL (Next.js)
  3. process.env.NEXT_PUBLIC_APP_URL (Next.js fallback)
  4. import.meta.env.VITE_API_URL (Vite)
  5. import.meta.env.VITE_APP_URL (Vite fallback)
  6. 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.ttfb

Tracked 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 development

Externalized (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