chore: import codes
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Application Configuration
|
||||
* Centralized configuration management for the entire package
|
||||
*/
|
||||
|
||||
import { getAvailableModules } from '../../modules/modules.registry.js';
|
||||
|
||||
/**
|
||||
* Get application name from environment variables
|
||||
* @returns {string} Application name
|
||||
*/
|
||||
export function getAppName() {
|
||||
return process.env.ZEN_NAME || 'ZEN';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session cookie name from environment variables
|
||||
* @returns {string} Session cookie name
|
||||
*/
|
||||
export function getSessionCookieName() {
|
||||
return process.env.ZEN_AUTH_SESSION_COOKIE_NAME || 'zen_session';
|
||||
}
|
||||
|
||||
/**
|
||||
* Public site origin for server-side absolute URLs (emails, redirects, PDF assets).
|
||||
* When NODE_ENV is development, NEXT_PUBLIC_URL_DEV is preferred, then NEXT_PUBLIC_URL.
|
||||
* Otherwise NEXT_PUBLIC_URL is used. Trailing slashes are stripped.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getPublicBaseUrl() {
|
||||
const fallback = 'http://localhost:3000';
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const raw = isDev
|
||||
? (process.env.NEXT_PUBLIC_URL_DEV || process.env.NEXT_PUBLIC_URL || fallback)
|
||||
: (process.env.NEXT_PUBLIC_URL || fallback);
|
||||
return String(raw).replace(/\/$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled modules configuration (server-side only)
|
||||
* This function dynamically reads from modules.registry.js and checks environment variables
|
||||
* Use this on the server and pass the result to client components as props
|
||||
*
|
||||
* To enable a module, set the environment variable: ZEN_MODULE_{NAME}=true
|
||||
* Example: ZEN_MODULE_INVOICE=true
|
||||
*
|
||||
* @returns {Object} Object with module names as keys and boolean values
|
||||
*/
|
||||
export function getModulesConfig() {
|
||||
const modules = {};
|
||||
const availableModules = getAvailableModules();
|
||||
|
||||
for (const moduleName of availableModules) {
|
||||
const envVar = `ZEN_MODULE_${moduleName.toUpperCase()}`;
|
||||
modules[moduleName] = process.env[envVar] === 'true';
|
||||
}
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get application configuration
|
||||
* @returns {Object} Application configuration object
|
||||
*/
|
||||
export function getAppConfig() {
|
||||
return {
|
||||
name: getAppName(),
|
||||
sessionCookieName: getSessionCookieName(),
|
||||
timezone: process.env.ZEN_TIMEZONE || 'America/Toronto',
|
||||
dateFormat: process.env.ZEN_DATE_FORMAT || 'YYYY-MM-DD',
|
||||
// Currency configuration (for currency module)
|
||||
defaultCurrency: process.env.ZEN_CURRENCY || 'CAD',
|
||||
currencySymbol: process.env.ZEN_CURRENCY_SYMBOL || '$',
|
||||
// Enabled modules
|
||||
modules: getModulesConfig(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* Date Utilities
|
||||
* All date functions work with UTC dates to avoid timezone issues
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse a date string as UTC date (ignoring timezone)
|
||||
* This ensures dates like "2025-11-15" are always treated as November 15, 2025
|
||||
* regardless of the user's timezone
|
||||
*
|
||||
* @param {string|Date} dateInput - Date string (YYYY-MM-DD) or Date object
|
||||
* @returns {Date} Date object in UTC
|
||||
*/
|
||||
export function parseUTCDate(dateInput) {
|
||||
if (!dateInput) return null;
|
||||
|
||||
// If already a Date object, return it
|
||||
if (dateInput instanceof Date) {
|
||||
return dateInput;
|
||||
}
|
||||
|
||||
// Parse YYYY-MM-DD string as UTC
|
||||
const [year, month, day] = dateInput.split('T')[0].split('-').map(Number);
|
||||
return new Date(Date.UTC(year, month - 1, day));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date for display
|
||||
*
|
||||
* @param {string|Date} dateInput - Date string or Date object
|
||||
* @param {string} locale - Locale string (default: 'en-US')
|
||||
* @param {Object} options - Intl.DateTimeFormat options
|
||||
* @returns {string} Formatted date string
|
||||
*/
|
||||
export function formatDateForDisplay(dateInput, locale = 'en-US', options = {}) {
|
||||
if (!dateInput) return '';
|
||||
|
||||
const date = parseUTCDate(dateInput);
|
||||
if (!date) return '';
|
||||
|
||||
const defaultOptions = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
timeZone: 'UTC',
|
||||
...options
|
||||
};
|
||||
|
||||
return date.toLocaleDateString(locale, defaultOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date for HTML input type="date"
|
||||
* Returns YYYY-MM-DD format
|
||||
*
|
||||
* @param {string|Date} dateInput - Date string or Date object
|
||||
* @returns {string} Date in YYYY-MM-DD format
|
||||
*/
|
||||
export function formatDateForInput(dateInput) {
|
||||
if (!dateInput) return '';
|
||||
|
||||
const date = parseUTCDate(dateInput);
|
||||
if (!date) return '';
|
||||
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getUTCDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date for HTML input type="datetime-local"
|
||||
* Returns YYYY-MM-DDTHH:MM format
|
||||
*
|
||||
* @param {string|Date} dateInput - Date string or Date object
|
||||
* @returns {string} Date in YYYY-MM-DDTHH:MM format
|
||||
*/
|
||||
export function formatDateTimeForInput(dateInput) {
|
||||
if (!dateInput) return '';
|
||||
|
||||
const date = new Date(dateInput);
|
||||
if (isNaN(date.getTime())) return '';
|
||||
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getUTCDate()).padStart(2, '0');
|
||||
const hours = String(date.getUTCHours()).padStart(2, '0');
|
||||
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a datetime string for display (preserves time component)
|
||||
* Use this for datetime fields — formatDateForDisplay strips the time via parseUTCDate.
|
||||
*
|
||||
* @param {string|Date} dateInput - ISO datetime string or Date object
|
||||
* @param {string} locale - Locale string (default: 'fr-FR')
|
||||
* @param {Object} options - Intl.DateTimeFormat options
|
||||
* @returns {string} Formatted datetime string
|
||||
*/
|
||||
export function formatDateTimeForDisplay(dateInput, locale = 'fr-FR', options = {}) {
|
||||
if (!dateInput) return '';
|
||||
|
||||
const date = new Date(dateInput);
|
||||
if (isNaN(date.getTime())) return '';
|
||||
|
||||
const defaultOptions = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZone: 'UTC',
|
||||
...options
|
||||
};
|
||||
|
||||
return date.toLocaleDateString(locale, defaultOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date for short display (e.g., "Nov 15, 2025")
|
||||
*
|
||||
* @param {string|Date} dateInput - Date string or Date object
|
||||
* @param {string} locale - Locale string (default: 'en-US')
|
||||
* @returns {string} Formatted date string
|
||||
*/
|
||||
export function formatDateShort(dateInput, locale = 'en-US') {
|
||||
return formatDateForDisplay(dateInput, locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate days between two dates
|
||||
*
|
||||
* @param {string|Date} date1 - First date
|
||||
* @param {string|Date} date2 - Second date
|
||||
* @returns {number} Number of days between dates (can be negative)
|
||||
*/
|
||||
export function getDaysBetween(date1, date2) {
|
||||
const d1 = parseUTCDate(date1);
|
||||
const d2 = parseUTCDate(date2);
|
||||
|
||||
if (!d1 || !d2) return 0;
|
||||
|
||||
// Reset both dates to midnight UTC to avoid any time component issues
|
||||
d1.setUTCHours(0, 0, 0, 0);
|
||||
d2.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
const diffTime = d2.getTime() - d1.getTime();
|
||||
const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
return diffDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add days to a date
|
||||
*
|
||||
* @param {string|Date} dateInput - Date to add days to
|
||||
* @param {number} days - Number of days to add (can be negative)
|
||||
* @returns {Date} New date with days added
|
||||
*/
|
||||
export function addDays(dateInput, days) {
|
||||
const date = parseUTCDate(dateInput);
|
||||
if (!date) return null;
|
||||
|
||||
const newDate = new Date(date);
|
||||
newDate.setUTCDate(newDate.getUTCDate() + days);
|
||||
|
||||
return newDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract days from a date
|
||||
*
|
||||
* @param {string|Date} dateInput - Date to subtract days from
|
||||
* @param {number} days - Number of days to subtract
|
||||
* @returns {Date} New date with days subtracted
|
||||
*/
|
||||
export function subtractDays(dateInput, days) {
|
||||
return addDays(dateInput, -days);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a date is overdue (before today)
|
||||
*
|
||||
* @param {string|Date} dueDate - Due date to check
|
||||
* @param {string|Date} today - Today's date (default: current date)
|
||||
* @returns {boolean} True if overdue
|
||||
*/
|
||||
export function isOverdue(dueDate, today = null) {
|
||||
const due = parseUTCDate(dueDate);
|
||||
if (!due) return false;
|
||||
|
||||
const todayDate = today ? parseUTCDate(today) : getTodayUTC();
|
||||
|
||||
return due < todayDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get today's date in UTC (at midnight)
|
||||
*
|
||||
* @returns {Date} Today's date in UTC
|
||||
*/
|
||||
export function getTodayUTC() {
|
||||
const now = new Date();
|
||||
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get today's date as YYYY-MM-DD string
|
||||
*
|
||||
* @returns {string} Today's date in YYYY-MM-DD format
|
||||
*/
|
||||
export function getTodayString() {
|
||||
return formatDateForInput(getTodayUTC());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two dates (ignoring time)
|
||||
*
|
||||
* @param {string|Date} date1 - First date
|
||||
* @param {string|Date} date2 - Second date
|
||||
* @returns {number} -1 if date1 < date2, 0 if equal, 1 if date1 > date2
|
||||
*/
|
||||
export function compareDates(date1, date2) {
|
||||
const d1 = parseUTCDate(date1);
|
||||
const d2 = parseUTCDate(date2);
|
||||
|
||||
if (!d1 || !d2) return 0;
|
||||
|
||||
if (d1 < d2) return -1;
|
||||
if (d1 > d2) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* ZEN Initialization
|
||||
* Initialize all ZEN services and modules using dynamic module discovery
|
||||
*/
|
||||
|
||||
import { discoverModules, startModuleCronJobs, stopModuleCronJobs } from '../../core/modules/index.js';
|
||||
|
||||
// Use globalThis to persist initialization flag across module reloads
|
||||
const ZEN_INIT_KEY = Symbol.for('__ZEN_INITIALIZED__');
|
||||
|
||||
/**
|
||||
* Initialize ZEN system
|
||||
* Discovers modules dynamically and starts cron jobs
|
||||
*
|
||||
* Recommended: Use instrumentation.js for automatic initialization
|
||||
* Alternative: Call this function manually in your root layout
|
||||
*
|
||||
* @example
|
||||
* // instrumentation.js (Recommended)
|
||||
* export async function register() {
|
||||
* if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
* const { initializeZen } = await import('@hykocx/zen');
|
||||
* await initializeZen();
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @example
|
||||
* // app/layout.js (Alternative)
|
||||
* import { initializeZen } from '@hykocx/zen';
|
||||
* initializeZen();
|
||||
*
|
||||
* @param {Object} options - Initialization options
|
||||
* @param {boolean} options.skipCron - Skip cron job initialization
|
||||
* @param {boolean} options.skipDb - Skip database initialization
|
||||
* @returns {Promise<Object>} Initialization result
|
||||
*/
|
||||
export async function initializeZen(options = {}) {
|
||||
const { skipCron = false, skipDb = true } = options;
|
||||
|
||||
// Only run on server-side
|
||||
if (typeof window !== 'undefined') {
|
||||
return { skipped: true, reason: 'client-side' };
|
||||
}
|
||||
|
||||
// Prevent multiple initializations using globalThis
|
||||
if (globalThis[ZEN_INIT_KEY]) {
|
||||
console.log('⚠ ZEN: Already initialized, skipping...');
|
||||
return { skipped: true, reason: 'already-initialized' };
|
||||
}
|
||||
|
||||
globalThis[ZEN_INIT_KEY] = true;
|
||||
console.log('🚀 ZEN: Starting initialization...');
|
||||
|
||||
const result = {
|
||||
discovery: null,
|
||||
cron: { started: [], errors: [] }
|
||||
};
|
||||
|
||||
try {
|
||||
// Step 1: Discover and register all enabled modules
|
||||
// This reads from modules.registry.js and loads each module's config files
|
||||
result.discovery = await discoverModules();
|
||||
|
||||
const enabledCount = result.discovery.enabled?.length || 0;
|
||||
const skippedCount = result.discovery.skipped?.length || 0;
|
||||
|
||||
if (enabledCount > 0) {
|
||||
console.log(`✓ ZEN: Discovered ${enabledCount} enabled module(s): ${result.discovery.enabled.join(', ')}`);
|
||||
}
|
||||
if (skippedCount > 0) {
|
||||
console.log(`⚠ ZEN: Skipped ${skippedCount} disabled module(s): ${result.discovery.skipped.join(', ')}`);
|
||||
}
|
||||
|
||||
// Step 2: Start cron jobs for all enabled modules
|
||||
if (!skipCron) {
|
||||
result.cron = await startModuleCronJobs();
|
||||
|
||||
if (result.cron.started.length > 0) {
|
||||
console.log(`✓ ZEN: Started ${result.cron.started.length} cron job(s): ${result.cron.started.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✓ ZEN: Initialization complete');
|
||||
|
||||
} catch (error) {
|
||||
console.error('✗ ZEN: Initialization failed:', error);
|
||||
result.error = error.message;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset initialization flag (useful for testing or manual reinitialization)
|
||||
* @returns {void}
|
||||
*/
|
||||
export function resetZenInitialization() {
|
||||
globalThis[ZEN_INIT_KEY] = false;
|
||||
|
||||
// Stop all cron jobs using the module system
|
||||
try {
|
||||
stopModuleCronJobs();
|
||||
} catch (e) {
|
||||
// Cron system not available
|
||||
}
|
||||
|
||||
console.log('⚠ ZEN: Initialization flag reset');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Metadata Utilities
|
||||
* Functions to generate dynamic metadata for Next.js pages
|
||||
*/
|
||||
|
||||
import { getAppName } from '../appConfig.js';
|
||||
|
||||
/**
|
||||
* Generate page title with app name
|
||||
* @param {string} title - Page title
|
||||
* @param {Object} options - Options
|
||||
* @param {boolean} options.includeAppName - Include app name in title (default: true)
|
||||
* @param {string} options.separator - Separator between title and app name (default: ' - ')
|
||||
* @param {string} options.appName - Custom app name (default: from config)
|
||||
* @returns {string} Formatted title
|
||||
*/
|
||||
export function generateTitle(title, options = {}) {
|
||||
const {
|
||||
includeAppName = true,
|
||||
separator = ' - ',
|
||||
appName = null
|
||||
} = options;
|
||||
|
||||
if (!includeAppName) {
|
||||
return title;
|
||||
}
|
||||
|
||||
const name = appName || getAppName();
|
||||
return `${title}${separator}${name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate metadata object for Next.js
|
||||
* @param {Object} params - Metadata parameters
|
||||
* @param {string} params.title - Page title
|
||||
* @param {string} params.description - Page description
|
||||
* @param {Object} params.openGraph - Open Graph metadata
|
||||
* @param {Object} params.twitter - Twitter metadata
|
||||
* @param {Object} params.other - Other metadata fields
|
||||
* @param {Object} options - Generation options
|
||||
* @param {boolean} options.includeAppName - Include app name in title (default: true)
|
||||
* @param {string} options.appName - Custom app name
|
||||
* @returns {Object} Next.js metadata object
|
||||
*/
|
||||
export function generateMetadata(params = {}, options = {}) {
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
openGraph,
|
||||
twitter,
|
||||
...other
|
||||
} = params;
|
||||
|
||||
const metadata = {};
|
||||
|
||||
// Title
|
||||
if (title) {
|
||||
metadata.title = generateTitle(title, options);
|
||||
}
|
||||
|
||||
// Description
|
||||
if (description) {
|
||||
metadata.description = description;
|
||||
}
|
||||
|
||||
// Open Graph
|
||||
if (openGraph) {
|
||||
metadata.openGraph = {
|
||||
...openGraph,
|
||||
title: openGraph.title ? generateTitle(openGraph.title, options) : metadata.title,
|
||||
description: openGraph.description || description,
|
||||
};
|
||||
}
|
||||
|
||||
// Twitter
|
||||
if (twitter) {
|
||||
metadata.twitter = {
|
||||
...twitter,
|
||||
title: twitter.title ? generateTitle(twitter.title, options) : metadata.title,
|
||||
description: twitter.description || description,
|
||||
};
|
||||
}
|
||||
|
||||
// Other metadata
|
||||
Object.assign(metadata, other);
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate robots metadata
|
||||
* @param {Object} options - Robots options
|
||||
* @param {boolean} options.index - Allow indexing (default: true)
|
||||
* @param {boolean} options.follow - Allow following links (default: true)
|
||||
* @param {boolean} options.noarchive - Prevent archiving
|
||||
* @param {boolean} options.nocache - Prevent caching
|
||||
* @returns {Object} Robots metadata
|
||||
*/
|
||||
export function generateRobots(options = {}) {
|
||||
const {
|
||||
index = true,
|
||||
follow = true,
|
||||
noarchive = false,
|
||||
nocache = false,
|
||||
} = options;
|
||||
|
||||
return {
|
||||
index,
|
||||
follow,
|
||||
...(noarchive && { noarchive: true }),
|
||||
...(nocache && { nocache: true }),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user