@123usmanhaider321/ui
v1.0.6
Published
Ticketly's shared UI component library — a Vite-built React package designed to be consumed inside **Next.js** applications. Ships the full `AddEventStepper` flow (multi-step event creation/editing), Redux RTK Query APIs, utilities, and a provider for inj
Readme
@123usmanhaider321/ui
Ticketly's shared UI component library — a Vite-built React package designed to be consumed inside Next.js applications. Ships the full AddEventStepper flow (multi-step event creation/editing), Redux RTK Query APIs, utilities, and a provider for injecting auth services and environment config.
Table of Contents
- Installation
- Peer Dependencies
- Next.js Integration
- Components
- Configuration Reference
- Services Interface
- Exported Utilities
- Redux API Hooks
- Development — Playground
Installation
From npm:
npm install @123usmanhaider321/uiLocal file link (monorepo / sibling directory):
"dependencies": {
"@123usmanhaider321/ui": "file:../ticketly-shared-ui"
}Peer Dependencies
These must be installed in the consuming app. They are not bundled.
npm install react react-dom @reduxjs/toolkit react-redux redux-persist framer-motion next| Package | Required version |
|---|---|
| react | ^18.0.0 |
| react-dom | ^18.0.0 |
| @reduxjs/toolkit | ^2.0.0 |
| react-redux | ^9.0.0 |
| redux-persist | ^6.0.0 |
| framer-motion | ^11.0.0 |
| next | ^14.0.0 |
Next.js Integration
1. transpilePackages
The package is published as ESM. Add it to transpilePackages so Next.js compiles it instead of loading it raw, and alias shared singletons to prevent duplicate-React/duplicate-framer-motion errors:
// next.config.js
const path = require('path');
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['@123usmanhaider321/ui'],
webpack: (config) => {
config.resolve.alias = {
...config.resolve.alias,
react: path.resolve('node_modules/react'),
'react-dom': path.resolve('node_modules/react-dom'),
'framer-motion': path.resolve('node_modules/framer-motion'),
next: path.resolve('node_modules/next'),
};
return config;
},
};
module.exports = nextConfig;If you see
TypeError: Cannot read properties of null (reading 'useContext')it means there are two copies of React. The alias above forces both the app and the library to share the same one.
2. Import styles
For Tailwind v3 consumers: Import the pre-built stylesheet once at the root of your app:
// app/layout.tsx (App Router)
import '@123usmanhaider321/ui/dist/styles.css';For Tailwind v4 consumers: Import the pre-built stylesheet inside a cascade layer within your global CSS file (or a scoped CSS file). This prevents the unlayered Tailwind 3 package CSS from overriding your Tailwind 4 utility classes:
/* app/globals.css */
@import "tailwindcss";
@import "@123usmanhaider321/ui/dist/styles.css" layer(components);The CSS file contains all Tailwind utility classes used by the library. It is independent of the consuming app's Tailwind build.
3. Tailwind config
If you want the library's Tailwind classes generated by your app's build (instead of the pre-built CSS), add the compiled dist to your content array:
// tailwind.config.ts
export default {
content: [
'./app/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./node_modules/@123usmanhaider321/ui/dist/**/*.js',
],
};4. Redux store
The library exposes two RTK Query APIs that must be registered in your store. It also reads from a state.event key — this must exist to avoid a runtime crash in EventSubmissionConfirmationModal.
// store/index.ts
'use client';
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { singleEventApi, eventApi } from '@123usmanhaider321/ui';
// Required — EventSubmissionConfirmationModal selects these flags from state.event.
// Replace with your own event slice if you have one.
const eventSlice = createSlice({
name: 'event',
initialState: {
addLoading: false,
addError: null as string | null,
addSuccess: false,
updateLoading: false,
updateError: null as string | null,
updateSuccess: false,
},
reducers: {},
});
export const store = configureStore({
reducer: {
// ...your own reducers
[singleEventApi.reducerPath]: singleEventApi.reducer,
[eventApi.reducerPath]: eventApi.reducer,
event: eventSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ serializableCheck: false })
.concat(singleEventApi.middleware)
.concat(eventApi.middleware),
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;Wrap your app with a "use client" Provider component:
// app/providers.tsx
'use client';
import { Provider } from 'react-redux';
import { store } from '@/store';
export function StoreProvider({ children }: { children: React.ReactNode }) {
return <Provider store={store}>{children}</Provider>;
}5. Provider setup
TicketlyUIProvider injects auth services and environment config into the library. It must sit inside a "use client" component, below the Redux <Provider>.
// app/providers.tsx
'use client';
import { Provider } from 'react-redux';
import { TicketlyUIProvider } from '@123usmanhaider321/ui';
import { store } from '@/store';
import { auth } from '@/lib/firebase'; // your app's Firebase Auth instance
import { onAuthStateChanged } from 'firebase/auth';
export function Providers({ children }: { children: React.ReactNode }) {
const services = {
getAuthToken: () =>
auth.currentUser?.getIdToken() ?? Promise.resolve(null),
onAuthStateChanged: (cb: (user: any) => void) => {
return onAuthStateChanged(auth, cb);
},
getUserId: () => auth.currentUser?.uid ?? null,
isAuthenticated: () => !!auth.currentUser,
};
return (
<Provider store={store}>
<TicketlyUIProvider
services={services}
config={{
BASE_URL: process.env.NEXT_PUBLIC_API_URL!,
FIREBASE_API_KEY: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
FIREBASE_AUTH_DOMAIN: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
FIREBASE_PROJECT_ID: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,
FIREBASE_STORAGE_BUCKET: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET!,
FIREBASE_MESSAGING_SENDER_ID: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID!,
FIREBASE_APP_ID: process.env.NEXT_PUBLIC_FIREBASE_APP_ID!,
GOOGLE_MAPS_API_KEY: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!,
PAYFAST_MERCHANT_ID: process.env.NEXT_PUBLIC_PAYFAST_MERCHANT_ID!,
PAYFAST_CHECKOUT_URL: process.env.NEXT_PUBLIC_PAYFAST_CHECKOUT_URL!,
PAYFAST_API_URL: process.env.NEXT_PUBLIC_PAYFAST_API_URL!,
HEADER_KEY: process.env.NEXT_PUBLIC_HEADER_KEY!,
BODY_KEY: process.env.NEXT_PUBLIC_BODY_KEY!,
VENDOR_API_KEY: process.env.NEXT_PUBLIC_VENDOR_API_KEY!,
VENDOR_API_SECRET: process.env.NEXT_PUBLIC_VENDOR_API_SECRET!,
}}
>
{children}
</TicketlyUIProvider>
</Provider>
);
}Then in your root layout:
// app/layout.tsx
import '@123usmanhaider321/ui/dist/styles.css';
import { Providers } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Sticky layout warning: Do not put
overflow: hiddenoroverflow-x: hiddenon any element that wraps these components. It breaksposition: stickyused by the file upload sidebar inside the stepper. Useoverflow: clipinstead if you need to contain horizontal bleed.
Components
AddEventStepper
The primary export. A fully self-contained multi-step form for creating and editing events. All steps, validation, API calls, and navigation guards are internal.
// app/events/create/page.tsx
'use client';
import { AddEventStepper } from '@123usmanhaider321/ui';
export default function CreateEventPage() {
return (
<AddEventStepper
organizer_id="org-123"
onEventCreated={(eventId) => {
console.log('Event created:', eventId);
}}
/>
);
}Edit mode:
<AddEventStepper
edit
id="existing-event-id"
organizer_id="org-123"
onEventCreated={(id) => router.push(`/events/${id}`)}
/>Restricted modes — open on a specific step group only:
// Jump straight to the questions editor
<AddEventStepper organizer_id="org-123" id="event-id" mode="questions" />
// Jump straight to the social profiles step
<AddEventStepper organizer_id="org-123" id="event-id" mode="social" />Persisting in-progress state across page reloads:
<AddEventStepper
organizer_id="org-123"
SCOPED_ID_KEY="create_event_id"
SCOPED_FORM_KEY="create_event_form"
onEventCreated={(id) => router.push(`/events/${id}`)}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| organizer_id | string | required | The organizer's ID — attached to all event API payloads. |
| edit | boolean | false | Edit mode — loads the event specified by id. |
| id | string | — | Event ID to fetch in edit mode, or to resume an in-progress creation. |
| onEventCreated | (id: string) => void | — | Callback after a successful create or update. |
| isClearing | boolean | false | Renders a global loading overlay while the parent is resetting state. |
| SCOPED_ID_KEY | string | "" | localStorage key for persisting the in-progress event ID between page reloads. |
| SCOPED_FORM_KEY | string | "" | localStorage key for persisting form state between page reloads. |
| isSameQuestionTitleAllowed | boolean | — | Allow duplicate question titles in the custom form builder. |
| closed_loop_id | string | — | Attaches a closed-loop ID to the event payload. |
| initialStep | number | — | Force the stepper to open at a specific step number on mount. |
| mode | "questions" \| "social" \| null | null | Restrict the stepper to only the questions steps or only the social profiles step. |
| test | boolean | false | Dev only. Enables free click-navigation between all steps, bypassing validation and event ID guards. |
Configuration Reference
All keys are passed to TicketlyUIProvider via the config prop and stored in a singleton. Defaults are empty strings except BASE_URL.
| Key | Description |
|---|---|
| BASE_URL | Base URL for all API calls. Default: http://localhost:8080 |
| FIREBASE_API_KEY | Firebase project API key |
| FIREBASE_AUTH_DOMAIN | Firebase auth domain |
| FIREBASE_PROJECT_ID | Firebase project ID |
| FIREBASE_STORAGE_BUCKET | Firebase Storage bucket |
| FIREBASE_MESSAGING_SENDER_ID | Firebase Cloud Messaging sender ID |
| FIREBASE_APP_ID | Firebase app ID |
| FIREBASE_MEASUREMENT_ID | Firebase Analytics measurement ID |
| GOOGLE_MAPS_API_KEY | Google Maps API key (venue autocomplete) |
| PAYFAST_MERCHANT_ID | PayFast merchant ID |
| PAYFAST_CHECKOUT_URL | PayFast checkout callback URL |
| PAYFAST_API_URL | PayFast API endpoint |
| PAYMO_PG_PAYFAST_CHECKOUT_URL | Paymo PG PayFast callback URL |
| HEADER_KEY | Request encryption header key |
| BODY_KEY | Request encryption body key |
| TICKETING_PORTAL_HEADER_KEY | Ticketing portal encryption header key |
| TICKETING_PORTAL_BODY_KEY | Ticketing portal encryption body key |
| VENDOR_API_KEY | Vendor API key |
| VENDOR_API_SECRET | Vendor API secret |
| INSTAGRAM_URL | Instagram URL used in social profile links |
Services Interface
interface TicketlyUIServices {
// Returns a valid Firebase ID token for authenticated API requests
getAuthToken: () => Promise<string | null>;
// Subscribe to auth state changes; returns an unsubscribe function
onAuthStateChanged: (callback: (user: any) => void) => () => void;
// Returns the current user's UID, or null if unauthenticated
getUserId: () => string | null;
// Returns whether the current user is authenticated
isAuthenticated: () => boolean;
}Exported Utilities
import {
cn, // clsx + tailwind-merge
formatDateForApi, // formats a Date to the API-expected string
formatDate, // human-readable date string
formatDateTime, // human-readable date + time string
isSameDay, // compares two dates by calendar day
isDefaultDateRange, // checks if a range is still the default placeholder
toDate, // parses multiple input types to a Date
extractDateAndTime, // splits a datetime string → { date, time }
convertDateFormat, // converts between date format conventions
getDateRange, // builds a { start, end } range object
getCurrentTierAndMember, // resolves the active ticket tier and member info
encrypt, // AES-encrypt a string using HEADER_KEY / BODY_KEY
decrypt, // AES-decrypt a string
fetchWrapper, // typed fetch wrapper used internally by all API calls
} from '@123usmanhaider321/ui';Redux API Hooks
Re-exported from the library for use alongside the stepper in your own components:
import {
useAddEventMutation,
useUpdateEventMutation,
useGetEventDetailsByIdQuery,
useUpdateEventSocialProfilesMutation,
useSubmitEventForApprovalMutation,
} from '@123usmanhaider321/ui';Development — Playground
A standalone Vite playground lets you develop and test components without a Next.js app.
npm run playground
# → http://127.0.0.1:5173The playground mocks next/navigation, next/image, next/link, and next/dynamic via Vite aliases in playground/vite.config.ts — no Next.js install required.
Edit playground/App.tsx to swap in different components and configs.
npm run build # build the library → dist/ (ESM + type declarations)
npm run build:css # compile Tailwind → dist/styles.css