B2B-88: add starter kit structure and elements

This commit is contained in:
devmc-ee
2025-06-08 16:18:30 +03:00
parent 657a36a298
commit e7b25600cb
1280 changed files with 77893 additions and 5688 deletions

View File

@@ -0,0 +1,118 @@
'use client';
import { createContext, useCallback, useContext, useRef } from 'react';
type EmptyPayload = NonNullable<unknown>;
// Base event types
export interface BaseAppEventTypes {
'user.signedIn': { userId: string };
'user.signedUp': { method: `magiclink` | `password` };
'user.updated': EmptyPayload;
'checkout.started': { planId: string; account?: string };
// Add more base event types here
}
export type ConsumerProvidedEventTypes = EmptyPayload;
// Helper type for extending event types
export type ExtendedAppEventTypes<
T extends ConsumerProvidedEventTypes = ConsumerProvidedEventTypes,
> = BaseAppEventTypes & T;
// Generic type for the entire module
export type AppEventType<T extends ConsumerProvidedEventTypes> =
keyof ExtendedAppEventTypes<T>;
export type AppEvent<
T extends ConsumerProvidedEventTypes = ConsumerProvidedEventTypes,
K extends AppEventType<T> = AppEventType<T>,
> = {
type: K;
payload: ExtendedAppEventTypes<T>[K];
};
export type EventCallback<
T extends ConsumerProvidedEventTypes,
K extends AppEventType<T> = AppEventType<T>,
> = (event: AppEvent<T, K>) => void;
interface InternalAppEventsContextType<
T extends ConsumerProvidedEventTypes = ConsumerProvidedEventTypes,
K extends AppEventType<T> = AppEventType<T>,
> {
emit: (event: AppEvent<never, never>) => void;
on: (eventType: K, callback: EventCallback<T, K>) => void;
off: (eventType: K, callback: EventCallback<T, K>) => void;
}
interface AppEventsContextType<T extends ConsumerProvidedEventTypes> {
emit: <K extends AppEventType<T>>(event: AppEvent<T, K>) => void;
on: <K extends AppEventType<T>>(
eventType: K,
callback: EventCallback<T, K>,
) => void;
off: <K extends AppEventType<T>>(
eventType: K,
callback: EventCallback<T, K>,
) => void;
}
const AppEventsContext = createContext<InternalAppEventsContextType | null>(
null,
);
export function AppEventsProvider<
T extends ConsumerProvidedEventTypes = ConsumerProvidedEventTypes,
K extends AppEventType<T> = AppEventType<T>,
>({ children }: React.PropsWithChildren) {
const listeners = useRef<Record<K, EventCallback<T, K>[]>>(
{} as Record<K, EventCallback<T, K>[]>,
);
const emit = useCallback(
(event: AppEvent<T, K>) => {
const eventListeners = listeners.current[event.type] ?? [];
eventListeners.forEach((callback) => callback(event));
},
[listeners],
);
const on = useCallback((eventType: K, callback: EventCallback<T, K>) => {
listeners.current = {
...listeners.current,
[eventType]: [...(listeners.current[eventType] ?? []), callback],
};
}, []) as AppEventsContextType<T>['on'];
const off = useCallback((eventType: K, callback: EventCallback<T, K>) => {
listeners.current = {
...listeners.current,
[eventType]: (listeners.current[eventType] ?? []).filter(
(cb) => cb !== callback,
),
};
}, []) as AppEventsContextType<T>['off'];
return (
<AppEventsContext.Provider value={{ emit, on, off }}>
{children}
</AppEventsContext.Provider>
);
}
export function useAppEvents<
T extends ConsumerProvidedEventTypes = ConsumerProvidedEventTypes,
>(): AppEventsContextType<T> {
const context = useContext(AppEventsContext);
if (!context) {
throw new Error('useAppEvents must be used within an AppEventsProvider');
}
return context as AppEventsContextType<T>;
}

View File

@@ -0,0 +1 @@
export * from './use-csrf-token';

View File

@@ -0,0 +1,17 @@
/**
* Get the CSRF token from the meta tag.
* @returns The CSRF token.
*/
export function useCsrfToken() {
if (typeof document === 'undefined') {
return '';
}
const meta = document.querySelector('meta[name="csrf-token"]');
if (!meta) {
return '';
}
return meta.getAttribute('content') ?? '';
}

View File

@@ -0,0 +1,9 @@
const Logger = {
info: console.info,
error: console.error,
warn: console.warn,
debug: console.debug,
fatal: console.error,
}
export { Logger };

View File

@@ -0,0 +1,18 @@
import { pino } from 'pino';
/**
* @name Logger
* @description A logger implementation using Pino
*/
const Logger = pino({
browser: {
asObject: true,
},
level: 'debug',
base: {
env: process.env.NODE_ENV,
},
errorKey: 'error',
});
export { Logger };

View File

@@ -0,0 +1,33 @@
import { createRegistry } from '../registry';
import { Logger as LoggerInstance } from './logger';
// Define the type for the logger provider. Currently supporting 'pino'.
type LoggerProvider = 'pino' | 'console';
// Use pino as the default logger provider
const LOGGER = (process.env.LOGGER ?? 'pino') as LoggerProvider;
// Create a registry for logger implementations
const loggerRegistry = createRegistry<LoggerInstance, LoggerProvider>();
// Register the 'pino' logger implementation
loggerRegistry.register('pino', async () => {
const { Logger: PinoLogger } = await import('./impl/pino');
return PinoLogger;
});
// Register the 'console' logger implementation
loggerRegistry.register('console', async () => {
const { Logger: ConsoleLogger } = await import('./impl/console');
return ConsoleLogger;
});
/**
* @name getLogger
* @description Retrieves the logger implementation based on the LOGGER environment variable using the registry API.
*/
export async function getLogger() {
return loggerRegistry.get(LOGGER);
}

View File

@@ -0,0 +1,17 @@
type LogFn = {
<T extends object>(obj: T, msg?: string, ...args: unknown[]): void;
(obj: unknown, msg?: string, ...args: unknown[]): void;
(msg: string, ...args: unknown[]): void;
};
/**
* @name Logger
* @description Logger interface for logging messages
*/
export interface Logger {
info: LogFn;
error: LogFn;
warn: LogFn;
debug: LogFn;
fatal: LogFn;
}

View File

@@ -0,0 +1,106 @@
/**
* Implementation factory type
*/
export type ImplementationFactory<T> = () => T | Promise<T>;
/**
* Public API types with improved get method
*/
export interface Registry<T, Names extends string> {
register: (
name: Names,
factory: ImplementationFactory<T>,
) => Registry<T, Names>;
// Overloaded get method that infers return types based on input.
get: {
<K extends Names>(name: K): Promise<T>;
<K extends [Names, ...Names[]]>(
...names: K
): Promise<{ [P in keyof K]: T }>;
};
setup: (group?: string) => Promise<void>;
addSetup: (
group: string,
callback: () => Promise<void>,
) => Registry<T, Names>;
}
/**
* @name createRegistry
* @description Creates a new registry instance with the provided implementations.
* @returns A new registry instance.
*/
export function createRegistry<T, Names extends string = string>(): Registry<
T,
Names
> {
const implementations = new Map<Names, ImplementationFactory<T>>();
const setupCallbacks = new Map<string, Array<() => Promise<void>>>();
const setupPromises = new Map<string, Promise<void>>();
const registry: Registry<T, Names> = {
register(name, factory) {
implementations.set(name, factory);
return registry;
},
// Updated get method overload that supports tuple inference
get: (async (...names: Names[]) => {
await registry.setup();
if (names.length === 1) {
return await getImplementation(names[0]!);
}
return await Promise.all(names.map((name) => getImplementation(name)));
}) as Registry<T, Names>['get'],
async setup(group?: string) {
if (group) {
if (!setupPromises.has(group)) {
const callbacks = setupCallbacks.get(group) ?? [];
setupPromises.set(
group,
Promise.all(callbacks.map((cb) => cb())).then(() => void 0),
);
}
return setupPromises.get(group);
}
const groups = Array.from(setupCallbacks.keys());
await Promise.all(groups.map((group) => registry.setup(group)));
},
addSetup(group, callback) {
if (!setupCallbacks.has(group)) {
setupCallbacks.set(group, []);
}
setupCallbacks.get(group)!.push(callback);
return registry;
},
};
async function getImplementation(name: Names) {
const factory = implementations.get(name);
if (!factory) {
throw new Error(`Implementation "${name}" not found`);
}
const implementation = await factory();
if (!implementation) {
throw new Error(`Implementation "${name}" is not available`);
}
return implementation;
}
return registry;
}

View File

@@ -0,0 +1,23 @@
/**
* Check if the code is running in a browser environment.
*/
export function isBrowser() {
return typeof window !== 'undefined';
}
/**
* @name formatCurrency
* @description Format the currency based on the currency code
*/
export function formatCurrency(params: {
currencyCode: string;
locale: string;
value: string | number;
}) {
const [lang, region] = params.locale.split('-');
return new Intl.NumberFormat(region ?? lang, {
style: 'currency',
currency: params.currencyCode,
}).format(Number(params.value));
}