@kodeme-io/next-core-patterns
v0.8.4
Published
Reusable business patterns for Next.js applications
Downloads
12
Maintainers
Readme
@kodeme-io/next-core-patterns
Reusable business patterns for Next.js applications - offline-first, type-safe, and framework-agnostic.
Installation
npm install @kodeme-io/next-core-patterns
# or
pnpm add @kodeme-io/next-core-patternsPatterns
OfflineSyncQueue
Generic offline-first sync queue with automatic retry and persistence.
import { OfflineSyncQueue } from '@kodeme-io/next-core-patterns'
// Define your sync handler
const syncHandler = async (item) => {
await fetch(`/api/${item.type}`, {
method: 'POST',
body: JSON.stringify(item.data)
})
}
// Create queue instance
const queue = new OfflineSyncQueue({
storageKey: 'my-sync-queue',
syncHandler,
maxRetries: 3
})
// Add items to queue
queue.enqueue({
type: 'photo',
data: { url: '...', metadata: {} }
})
// Subscribe to changes
queue.subscribe((items) => {
console.log('Queue updated:', items.length, 'pending')
})PricingCalculator
Offline-first pricing engine with tax calculations.
import { PricingCalculator } from '@kodeme-io/next-core-patterns'
const calculator = new PricingCalculator()
// Load price data
await calculator.loadPriceData({
priceList: [
{ productId: 1, price: 100 },
{ productId: 2, price: 200 }
],
taxes: [
{ id: 1, name: 'VAT', amount: 10, type: 'percent' }
]
})
// Calculate line item
const line = calculator.calculateLine({
productId: 1,
quantity: 5,
taxIds: [1]
})
// Result: { subtotal: 500, tax: 50, total: 550 }
// Calculate order
const order = calculator.calculateOrder([
{ productId: 1, quantity: 5, taxIds: [1] },
{ productId: 2, quantity: 2, taxIds: [1] }
])
// Result: { subtotal: 900, tax: 90, total: 990 }ScheduleUtils
Date and schedule calculation utilities - 25+ functions for common business date operations.
import {
getCurrentWeekNumber,
isOddWeek,
addBusinessDays,
getDateRange,
formatRelativeDate,
getBusinessDaysBetween,
getWeekStart,
getMonthStart,
getQuarterStart
} from '@kodeme-io/next-core-patterns'
// ISO week calculations
const week = getCurrentWeekNumber() // 42
const isOdd = isOddWeek(week) // true
// Business days (skip weekends)
const deliveryDate = addBusinessDays(new Date(), 5)
const workDays = getBusinessDaysBetween(startDate, endDate)
// Date ranges
const nextWeek = getDateRange(new Date(), 7)
const weekStart = getWeekStart(new Date())
const monthStart = getMonthStart()
// Relative dates
const display = formatRelativeDate(date) // "Today", "Tomorrow", or day name
// Period calculations
const q1Start = getQuarterStart()
const yearEnd = getYearEnd()Available Functions:
- Week:
getCurrentWeekNumber,isOddWeek,isCurrentWeekOdd,getWeekStart,getWeekEnd - Business Days:
addBusinessDays,getBusinessDaysBetween,isBusinessDay,isWeekend - Date Ranges:
getDateRange,getDaysBetween,isSameDay - Formatting:
formatRelativeDate - Next Occurrence:
getNextDayOfWeek - Period Boundaries:
getMonthStart,getMonthEnd,getQuarterStart,getQuarterEnd,getYearStart,getYearEnd
GeofenceUtils
Location-based validation and GPS distance calculations using the Haversine formula.
import {
calculateDistance,
validateGeofence,
formatDistance,
hasValidGPSCoordinates,
getCenterPoint,
getBoundingBox
} from '@kodeme-io/next-core-patterns'
// Calculate distance between two GPS coordinates
const distance = calculateDistance(
-6.2088, 106.8456, // Jakarta
-6.9175, 107.6191 // Bandung
)
// Returns: ~120000 (120km in meters)
// Validate geofence (e.g., check-in at customer location)
const result = validateGeofence(
userLat, userLon, // User's current location
customerLat, customerLon, // Customer location
100 // 100m radius
)
if (result.isWithinGeofence) {
console.log(`✅ Within range (${result.distance}m away)`)
} else {
console.log(`❌ Too far: ${formatDistance(result.distance)}`)
}
// Validate GPS coordinates
if (hasValidGPSCoordinates(lat, lon)) {
// Coordinates are valid
}
// Get center point of multiple locations
const center = getCenterPoint([
{ lat: -6.2088, lon: 106.8456 },
{ lat: -6.9175, lon: 107.6191 }
])
// Get bounding box for map viewport
const bounds = getBoundingBox(locations)
map.fitBounds([[bounds.minLat, bounds.minLon], [bounds.maxLat, bounds.maxLon]])Available Functions:
calculateDistance- Haversine distance calculationvalidateGeofence- Check if within radiusformatDistance- Format meters/kilometershasValidGPSCoordinates- Validate lat/longetCenterPoint- Center of multiple pointsgetBoundingBox- Min/max lat/lon boundsisWithinBounds- Check if point in bounds
MediaUtils
Image compression and storage management for PWA applications.
import {
compressPhoto,
checkStorageQuota,
fileToBase64,
base64ToBlob,
batchCompressPhotos
} from '@kodeme-io/next-core-patterns'
// Compress photo from camera
const result = await compressPhoto(photoDataUrl, {
maxSizeKB: 500, // Target 500KB
maxWidth: 1920, // Max 1920px width
maxHeight: 1080, // Max 1080px height
format: 'image/jpeg'
})
console.log(`Compressed: ${result.originalSizeKB}KB → ${result.compressedSizeKB}KB`)
console.log(`Compression ratio: ${(result.compressionRatio * 100).toFixed(1)}%`)
// Check storage quota
const quota = await checkStorageQuota()
if (quota.percentUsed > 90) {
alert(`Storage almost full: ${quota.usedMB.toFixed(0)}MB / ${quota.availableMB.toFixed(0)}MB`)
}
// Convert file to base64
const file = event.target.files[0]
const dataUrl = await fileToBase64(file)
const compressed = await compressPhoto(dataUrl)
// Convert back to blob for upload
const blob = base64ToBlob(compressed.dataUrl)
const formData = new FormData()
formData.append('photo', blob, 'photo.jpg')
// Batch compress multiple photos
const results = await batchCompressPhotos(
photoArray,
{ maxSizeKB: 500 },
(current, total) => console.log(`${current}/${total}`)
)Available Functions:
compressPhoto- Compress image to size limitgetBase64SizeKB- Calculate base64 sizecheckStorageQuota- Check browser storagefileToBase64- Convert File to base64base64ToBlob- Convert base64 to BlobdownloadImage- Trigger downloadbatchCompressPhotos- Compress multiple
Formatters
Number, currency, phone, and text formatting utilities with Indonesian locale support.
import {
formatCurrency,
formatRupiah,
formatNumber,
formatCompactNumber,
formatPhoneIndonesia,
formatKTP,
formatPercentage,
formatFileSize,
pluralize
} from '@kodeme-io/next-core-patterns'
// Currency formatting
formatRupiah(1500000) // "Rp1.500.000"
formatRupiah(1500000, true) // "Rp1.500.000,00"
formatCurrency(1500, { currency: 'USD', locale: 'en-US' }) // "$1,500.00"
// Number formatting
formatNumber(1500000) // "1.500.000" (Indonesian)
formatCompactNumber(1500000) // "1.5M"
formatPercentage(0.156) // "15.6%"
formatFileSize(1536000) // "1.5 MB"
// Phone & ID formatting
formatPhoneIndonesia('081234567890') // "0812-3456-7890"
formatPhoneIndonesia('081234567890', true) // "+62 812-3456-7890"
formatKTP('3216012345678901') // "3216-012345-678901"
// Text formatting
pluralize(5, 'item') // "5 items"
truncateText('Long text...', 10) // "Long te..."Available Functions (13):
- Currency:
formatCurrency,formatRupiah,formatNumber,formatCompactNumber - Phone/ID:
formatPhoneIndonesia,formatKTP - Numbers:
formatPercentage,formatFileSize,formatCoordinates - Text:
truncateText,formatDateIndonesia,formatDateRange,pluralize
Validators
Form validation and data integrity checks with helpful error messages.
import {
isValidEmail,
isValidPhoneIndonesia,
validatePassword,
validateRequired,
validateRange,
isNumeric
} from '@kodeme-io/next-core-patterns'
// Email & phone validation
isValidEmail('[email protected]') // true
isValidPhoneIndonesia('081234567890') // true
// Password validation
const result = validatePassword('weak')
// { isValid: false, error: 'Password must be at least 8 characters' }
validatePassword('Password123')
// { isValid: true }
// Field validation
validateRequired('', 'Email')
// { isValid: false, error: 'Email is required' }
validateRange(15, 1, 10)
// { isValid: false, error: 'Value must be between 1 and 10' }
// Simple checks
isNumeric('12345') // true
isAlphanumeric('abc123') // trueAvailable Functions (18):
- Email/Phone/ID:
isValidEmail,isValidPhoneIndonesia,isValidKTP,isValidPostalCode - URL/Card:
isValidURL,isValidCreditCard - Password:
validatePassword - Range/Length:
validateRange,validateMinLength,validateMaxLength - Required/Match:
validateRequired,validateMatch - String Type:
isNumeric,isAlphanumeric - Date:
validateFutureDate,validateDateRange - File:
validateFileSize,validateFileType
String Utilities
String manipulation and transformation functions.
import {
slugify,
titleCase,
toCamelCase,
toSnakeCase,
getInitials,
maskString,
randomString
} from '@kodeme-io/next-core-patterns'
// Case conversion
slugify('Hello World!') // "hello-world"
titleCase('hello world') // "Hello World"
toCamelCase('hello world') // "helloWorld"
toSnakeCase('helloWorld') // "hello_world"
toKebabCase('helloWorld') // "hello-world"
// Name utilities
getInitials('John Doe') // "JD"
maskString('1234567890123456', 4, 4) // "1234********3456"
// Text manipulation
randomString(8) // "a7B3xY9k"
countWords('hello world') // 2
contains('Hello World', 'world') // true (case insensitive)
// HTML safety
escapeHtml('<script>alert("xss")</script>')
// "<script>alert("xss")</script>"Available Functions (20):
- Case Conversion:
slugify,titleCase,sentenceCase,camelToTitle,snakeToTitle - Case Transforms:
toCamelCase,toSnakeCase,toKebabCase,capitalize - Text Manipulation:
removeExtraSpaces,getInitials,maskString,reverse - Text Analysis:
countWords,contains,stripHtml,escapeHtml - Generation:
randomString,pad,wordWrap
React Query Helpers
Utilities for @tanstack/react-query - standardized patterns for data fetching.
import {
createQueryKeys,
StaleTime,
createPagination,
useDebouncedQueryKey
} from '@kodeme-io/next-core-patterns'
// Create query key factory
const customerKeys = createQueryKeys('customers')
useQuery({
queryKey: customerKeys.list({ active: true }),
queryFn: () => fetchCustomers({ active: true }),
staleTime: StaleTime.MEDIUM // 5 minutes
})
useQuery({
queryKey: customerKeys.detail(123),
queryFn: () => fetchCustomer(123),
staleTime: StaleTime.LONG // 10 minutes
})
// Pagination
const pagination = createPagination(20)
const { data } = useQuery({
queryKey: ['items', page],
queryFn: () => fetchItems(pagination.getOffset(page), pagination.pageSize)
})
// Debounced search
const [search, setSearch] = useState('')
const debouncedKey = useDebouncedQueryKey(['search'], search, 500)
const { data } = useQuery({
queryKey: debouncedKey,
queryFn: () => searchItems(search)
})Available Functions (10):
- Keys:
createQueryKeys- Type-safe query key factory - Cache:
StaleTime,RefetchInterval- Standard timing presets - Updates:
createOptimisticUpdate- Optimistic update handlers - Pagination:
createPagination,createInfiniteQueryHelpers - Search:
useDebouncedQueryKey- Debounced keys for search - Errors:
getQueryErrorMessage,isQueryLoading- Error handling
Odoo Helpers
Utilities for working with Odoo ERP data structures and APIs.
import {
extractMany2oneId,
extractMany2oneName,
toOdooDatetime,
toOdooDate,
buildDomain,
formatOdooMonetary
} from '@kodeme-io/next-core-patterns'
// Many2one fields
const userId = extractMany2oneId(partner.user_id) // [5, "John"] → 5
const userName = extractMany2oneName(partner.user_id) // [5, "John"] → "John"
// Date/datetime conversion
toOdooDatetime(new Date()) // "2024-12-25 10:30:00"
toOdooDate(new Date()) // "2024-12-25"
// Domain builder (type-safe)
const domain = buildDomain()
.equals('active', true)
.ilike('name', 'john')
.greaterThan('age', 18)
.build()
// [['active', '=', true], ['name', 'ilike', 'john'], ['age', '>', 18]]
// OR conditions
const domain = buildDomain()
.or()
.equals('state', 'draft')
.equals('state', 'pending')
.build()
// ['|', ['state', '=', 'draft'], ['state', '=', 'pending']]
// Format monetary
formatOdooMonetary(1500000) // "Rp 1.500.000"Available Functions (12):
- Many2one:
extractMany2oneId,extractMany2oneName - Dates:
toOdooDatetime,toOdooDate,fromOdooDatetime,fromOdooDate - Domains:
buildDomain- Type-safe domain builder - Formatting:
formatOdooMonetary,parseOdooSelection - Records:
isOdooRecordActive,buildOdooContext,normalizeOdooRecord
Array Utilities
Common array operations for filtering, grouping, sorting, and analysis.
import {
groupBy,
unique,
sortBy,
chunk,
sum,
average,
partition
} from '@kodeme-io/next-core-patterns'
// Group by key
const customers = [
{ id: 1, city: 'Jakarta', total: 100 },
{ id: 2, city: 'Jakarta', total: 200 },
{ id: 3, city: 'Bandung', total: 150 }
]
const byCity = groupBy(customers, 'city')
// Map { 'Jakarta' => [{...}, {...}], 'Bandung' => [{...}] }
// Remove duplicates
unique([1, 2, 2, 3, 3, 3]) // [1, 2, 3]
unique(items, 'id') // Remove by id
// Sort
sortBy(customers, 'total', 'desc') // Highest first
// Chunk for pagination
chunk([1, 2, 3, 4, 5], 2) // [[1, 2], [3, 4], [5]]
// Math operations
sum(orders, 'total') // Sum all totals
average(scores, 'value') // Average score
min(products, 'price') // Lowest price
max(products, 'price') // Highest price
// Partition by condition
const [active, inactive] = partition(users, (u) => u.active)Available Functions (23):
- Grouping:
groupBy,unique,filterBy,findBy - Sorting:
sortBy,shuffle - Chunking:
chunk,take,takeLast,partition - Math:
sum,average,min,max - Manipulation:
flatten,flattenDeep,compact - Generation:
range - Checks:
isEmpty
Features
- ✅ Offline-First: All patterns work offline with automatic sync when online
- ✅ Type-Safe: Full TypeScript support
- ✅ Framework-Agnostic: Works with any backend or API
- ✅ Persistent: Uses localStorage for data persistence
- ✅ Automatic Retry: Failed operations retry automatically
- ✅ Zero Dependencies: Pure TypeScript implementation
- ✅ GPS & Geofencing: Haversine distance calculations
- ✅ Image Compression: Canvas-based photo compression
- ✅ Storage Management: Quota checking and monitoring
Use Cases
- Field Workers: Offline sync, geofencing, photo compression
- E-commerce: Pricing calculations, order totals
- Logistics: Delivery schedules, route validation
- Sales: Visit schedules, customer check-ins
- PWA Apps: Storage management, media optimization
License
MIT © ABC Food
