Skip to content

rifatxtra/feature-kit

Repository files navigation

@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("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),
}));

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

💡 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).


Layouts (src/layouts/)

AdminLayoutClient.tsx -- Admin Dashboard Shell

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.

⚙️ How Sidebar Link Customisation Works

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
}
1. Dynamic Icon Handling

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: LayoutDashboard from lucide-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.
2. Automatic Path Highlighting (Active State)

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/new will highlight a menu item pointing to /admin/works), as long as the parent path is not /.
3. Router Decoupled Single-Page Navigation

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.

🚀 Example Integration

Defining Custom Sidebar Links with Next.js App Router:
// 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>
  );
}
Authenticated Security Guard:

Upon mounting, the component makes a secure GET /api/me call.

  • If 401/Unauthorized: It automatically clears the rifatxtra_token from 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.

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. Custom Layout & Sidebar Setup (src/layouts/AppAdminLayout.tsx)

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>
  );
}
💡 Key Features of this Setup:
  • No Page Reloads: By mapping Vite's <Link> component inside routerAdapter.Link, the sidebar navigation acts as a seamless client-side SPA with high-fidelity transitions.
  • Auto-Active Tabs: The layout will automatically inspect location.pathname and set the correct active tab styles. It will even match sub-paths (e.g. /admin/works/create will correctly highlight the /admin/works tab).
  • 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.

3. Route Configuration & Layout Application (src/App.tsx)

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>
  );
}

4. 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

About

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

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors