Every day, we're told we need more. More libraries, more frameworks, more complex build tools just to get started. We're sold a mountain of dependencies—Webpack, Babel, Vite, Node.js—before we've even written a single line of our app's logic.
But what if that's a story we don't have to buy into?
What if I told you that your favorite code editor and a single HTML file are all you need to build a truly complex, high-performance application? I'm not talking about a simple counter. I'm talking about this fully-featured Todo application with:
- authentication,
- client-side routing,
- granular,
- user-specific persistent storage.
This entire application, all 1,500+ lines of it, lives in one HTML file. There is no npm install, no package.json, no bundler. It just runs.
This is the story of how a different kind of framework—JurisJS—makes this possible by getting out of your way and letting you build.
You can test the demo here : TodoApp
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Juris Todo App - Routeur, Auth & Stockage</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--orange: #ff6b35;
--orange-light: #ff8c5a;
--orange-dark: #e55a2b;
--orange-pale: #fff4f1;
--primary: #ff6b35;
--primary-dark: #e55a2b;
--primary-light: #ff8c5a;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--gray-50: #fafafa;
--gray-100: #f5f5f5;
--gray-200: #e5e5e5;
--gray-300: #d4d4d4;
--gray-400: #a3a3a3;
--gray-500: #6b7280;
--gray-600: #525252;
--gray-700: #404040;
--gray-800: #262626;
--gray-900: #171717;
--white: #ffffff;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--border-radius: 0.5rem;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Inter', sans-serif;
line-height: 1.6;
color: var(--gray-900);
background: var(--white);
font-size: 16px;
}
/* Layout Components */
.app-container {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: var(--white);
border-bottom: 1px solid var(--gray-200);
padding: 1rem 0;
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: 700;
color: var(--orange);
text-decoration: none;
}
.nav {
display: flex;
gap: 1rem;
align-items: center;
}
.nav-link {
color: var(--gray-600);
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: var(--border-radius);
}
.nav-link:hover {
background: var(--gray-100);
color: var(--orange);
}
.nav-link.active {
background: var(--orange);
color: var(--white);
}
.main-content {
flex: 1;
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
width: 100%;
}
/* Form Components */
.form-container {
max-width: 400px;
margin: 2rem auto;
background: var(--white);
padding: 2rem;
border-radius: var(--border-radius);
box-shadow: var(--shadow-lg);
}
.form-title {
font-size: 1.5rem;
font-weight: 600;
text-align: center;
margin-bottom: 2rem;
color: var(--gray-900);
}
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--gray-700);
}
.form-input {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--gray-300);
border-radius: var(--border-radius);
font-size: 1rem;
}
.form-input:focus {
outline: none;
border-color: var(--orange);
box-shadow: 0 0 0 3px rgba(255, 107, 53, 0.1);
}
.form-input.error {
border-color: var(--danger);
}
.form-error {
color: var(--danger);
font-size: 0.875rem;
margin-top: 0.25rem;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: var(--border-radius);
font-size: 1rem;
font-weight: 500;
cursor: pointer;
text-decoration: none;
display: inline-block;
text-align: center;
touch-action: manipulation;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.btn-primary {
background: var(--orange);
color: var(--white);
}
.btn-primary:hover {
background: var(--orange-dark);
}
.btn-secondary {
background: var(--gray-200);
color: var(--gray-800);
}
.btn-secondary:hover {
background: var(--gray-300);
}
.btn-danger {
background: var(--danger);
color: var(--white);
}
.btn-success {
background: var(--success);
color: var(--white);
}
.btn-full {
width: 100%;
}
/* Todo Components */
.todo-dashboard {
display: grid;
gap: 2rem;
}
.todo-lists {
background: var(--white);
border-radius: var(--border-radius);
padding: 1.5rem;
box-shadow: var(--shadow);
}
.section-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--gray-900);
}
.list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border: 1px solid var(--gray-200);
border-radius: var(--border-radius);
margin-bottom: 0.5rem;
cursor: pointer;
}
.list-item:hover {
border-color: var(--orange);
background: var(--gray-50);
}
.list-item.active {
border-color: var(--orange);
background: var(--orange-pale);
}
.list-info {
flex: 1;
}
.list-name {
font-weight: 500;
color: var(--gray-900);
}
.list-count {
color: var(--gray-500);
font-size: 0.875rem;
}
.list-actions {
display: flex;
gap: 0.5rem;
}
.btn-small {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.todo-form {
background: var(--white);
border-radius: var(--border-radius);
padding: 1.5rem;
box-shadow: var(--shadow);
margin-bottom: 2rem;
}
.todo-input {
display: flex;
gap: 0.5rem;
}
.todo-input input {
flex: 1;
}
.todo-list {
background: var(--white);
border-radius: var(--border-radius);
padding: 1.5rem;
box-shadow: var(--shadow);
}
.todo-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 0;
border-bottom: 1px solid var(--gray-100);
}
.todo-item:last-child {
border-bottom: none;
}
.todo-checkbox {
width: 1.25rem;
height: 1.25rem;
cursor: pointer;
}
.todo-text {
flex: 1;
color: var(--gray-700);
}
.todo-text.completed {
text-decoration: line-through;
color: var(--gray-400);
}
.todo-actions {
display: flex;
gap: 0.5rem;
}
.filters {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.filter-btn {
padding: 0.5rem 1rem;
border: 1px solid var(--gray-300);
background: var(--white);
color: var(--gray-600);
border-radius: var(--border-radius);
cursor: pointer;
}
.filter-btn.active {
background: var(--orange);
color: var(--white);
border-color: var(--orange);
}
/* Responsive Design */
@media (min-width: 768px) {
.todo-dashboard {
grid-template-columns: 450px 1fr;
}
.main-content {
padding: 3rem 2rem;
}
}
/* Loading & Empty States */
.loading {
text-align: center;
padding: 2rem;
color: var(--gray-500);
}
.empty-state {
text-align: center;
padding: 3rem;
color: var(--gray-500);
}
.empty-state h3 {
margin-bottom: 1rem;
color: var(--gray-700);
}
/* Utility Classes */
.text-center {
text-align: center;
}
.text-sm {
font-size: 0.875rem;
}
.text-danger {
color: var(--danger);
}
.text-success {
color: var(--success);
}
.mt-1 {
margin-top: 0.25rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.hidden {
display: none;
}
/* Toast notifications */
.toast {
position: fixed;
top: 1rem;
right: 1rem;
background: var(--white);
border: 1px solid var(--gray-200);
border-radius: var(--border-radius);
padding: 1rem;
box-shadow: var(--shadow-lg);
z-index: 1000;
}
.toast.success {
border-left: 4px solid var(--success);
}
.toast.error {
border-left: 4px solid var(--danger);
}
</style>
</head>
<body>
<div id="app"></div>
<script src="https://unpkg.com/juris"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<script>
// NOTE: The internal logic and console logs of the managers are kept in English
// for easier debugging by developers, as is common practice. Only user-facing
// text in UI components and alerts is translated.
const StatePersistenceManager = (props, context) => {
const { getState, setState, subscribe } = context;
// Enhanced configuration with timing controls
const config = {
domains: props.domains || [],
excludeDomains: props.excludeDomains || ['temp', 'cache', 'session', 'geolocation', 'persistence'],
keyPrefix: props.keyPrefix || 'app_state_',
debounceMs: props.debounceMs || 1000,
debug: props.debug || false,
autoDetectNewDomains: props.autoDetectNewDomains || true,
watchIntervalMs: props.watchIntervalMs || 5000,
// Timing controls for layout shift prevention
aggressiveRestore: props.aggressiveRestore !== false, // Default: true
restoreDelay: props.restoreDelay || 0,
priorityDomains: props.priorityDomains || [], // Restore these first
earlyRestoreTimeout: props.earlyRestoreTimeout || 50,
// Granular domain timing
domainRestoreConfig: props.domainRestoreConfig || {},
// Save timing controls
immediateSave: props.immediateSave || [], // Save changes immediately
criticalSave: props.criticalSave || [], // Save with shorter debounce
criticalDebounceMs: props.criticalDebounceMs || 200,
// User-specific domains (requires authentication)
userSpecificDomains: props.userSpecificDomains || [],
userIdPath: props.userIdPath || 'auth.user.id', // Path to user ID in state
requireAuth: props.requireAuth || false // Whether user-specific domains require auth
};
// Internal state
let saveTimers = new Map();
let isRestoring = false;
let domainSubscriptions = new Map();
let domainWatcher = null;
let restoreQueue = [];
let isProcessingRestoreQueue = false;
return {
hooks: {
onRegister: () => {
console.log('💾 StatePersistenceManager initializing with timing controls...');
// Initialize persistence state - DIRECT INJECTION
if (context.juris && context.juris.stateManager && context.juris.stateManager.state) {
context.juris.stateManager.state.persistence = {
isEnabled: true,
lastSave: null,
lastRestore: null,
errors: [],
stats: {
domainsTracked: 0,
totalSaves: 0,
totalRestores: 0,
priorityRestores: 0,
delayedRestores: 0
}
};
} else {
// Fallback if direct access not available
setState('persistence.isEnabled', true);
setState('persistence.lastSave', null);
setState('persistence.lastRestore', null);
setState('persistence.errors', []);
setState('persistence.stats', {
domainsTracked: 0,
totalSaves: 0,
totalRestores: 0,
priorityRestores: 0,
delayedRestores: 0
});
}
// Restore state with timing controls
if (config.aggressiveRestore) {
restoreAllDomainsWithTiming();
} else {
setTimeout(() => restoreAllDomainsWithTiming(), config.restoreDelay);
}
// Setup monitoring after early restore
setTimeout(() => {
setupDomainMonitoring();
if (config.autoDetectNewDomains) {
setupDomainWatcher();
}
}, Math.max(config.earlyRestoreTimeout, 100));
// Setup cross-tab sync
window.addEventListener('storage', handleStorageEvent);
window.addEventListener('beforeunload', saveAllTrackedDomains);
console.log('✅ StatePersistenceManager ready with timing controls');
},
onUnregister: () => {
console.log('💾 StatePersistenceManager cleanup');
// Save all before cleanup
saveAllTrackedDomains();
// Clear all timers
saveTimers.forEach(timer => clearTimeout(timer));
saveTimers.clear();
if (domainWatcher) {
clearInterval(domainWatcher);
domainWatcher = null;
}
// Unsubscribe from all domains
domainSubscriptions.forEach(unsubscribe => unsubscribe());
domainSubscriptions.clear();
// Remove storage listener
window.removeEventListener('storage', handleStorageEvent);
window.removeEventListener('beforeunload', saveAllTrackedDomains);
console.log('✅ StatePersistenceManager cleaned up');
}
},
api: {
saveDomain: (domain, immediate = false) => saveDomain(domain, immediate),
saveAllDomains: () => saveAllTrackedDomains(),
restoreDomain: (domain) => restoreDomain(domain),
restoreAllDomains: () => restoreAllDomainsWithTiming(),
addDomain: (domain) => addDomainTracking(domain),
removeDomain: (domain) => removeDomainTracking(domain),
clearDomain: (domain) => clearDomainStorage(domain),
clearAllStorage: () => clearAllStorage(),
getStorageStats: () => getStorageStats(),
getTrackedDomains: () => Array.from(domainSubscriptions.keys()),
exportState: () => exportState(),
importState: (data) => importState(data),
refreshDomainDetection: () => setupDomainMonitoring(),
forceImmediateSave: (domain) => saveDomain(domain, true),
restorePriorityDomains: () => restorePriorityDomains(),
getRestoreQueue: () => [...restoreQueue],
updateTimingConfig: (newConfig) => Object.assign(config, newConfig),
getConfig: () => ({ ...config }) // Get current configuration
}
};
// Enhanced restore with timing controls
function restoreAllDomainsWithTiming() {
log('📂 Starting timed restore sequence...');
// Get all stored domains by scanning localStorage
const storedDomains = [];
const keyPrefix = config.keyPrefix;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(keyPrefix)) {
// Extract domain from key
let remainder = key.substring(keyPrefix.length);
// Split by underscore and take first part as domain
let domain = remainder.split('_')[0];
// Only include if it's a valid domain and not already included
if (domain && !storedDomains.includes(domain)) {
storedDomains.push(domain);
}
}
}
log(`📂 Found stored domains: [${storedDomains.join(', ')}]`);
if (storedDomains.length === 0) {
log('📂 No stored domains found');
// DIRECT INJECTION for lastRestore
if (context.juris && context.juris.stateManager && context.juris.stateManager.state && context.juris.stateManager.state.persistence) {
context.juris.stateManager.state.persistence.lastRestore = Date.now();
}
return { restored: [], failed: [] };
}
// Sort domains by priority and timing configuration
const sortedDomains = storedDomains.sort((a, b) => {
const configA = config.domainRestoreConfig[a] || { priority: 999, delay: 0 };
const configB = config.domainRestoreConfig[b] || { priority: 999, delay: 0 };
if (configA.priority !== configB.priority) {
return configA.priority - configB.priority;
}
return configA.delay - configB.delay;
});
log(`📂 Restore order: [${sortedDomains.join(', ')}]`);
// Process domains with their configured timing
let cumulativeDelay = 0;
const results = { restored: [], failed: [], priority: [], delayed: [] };
sortedDomains.forEach((domain, index) => {
const domainConfig = config.domainRestoreConfig[domain] || { priority: 999, delay: 0, aggressive: false };
if (domainConfig.aggressive || config.priorityDomains.includes(domain)) {
// Immediate restore for aggressive/priority domains
if (restoreDomain(domain)) {
results.restored.push(domain);
results.priority.push(domain);
// DIRECT INJECTION for stats
if (context.juris && context.juris.stateManager && context.juris.stateManager.state && context.juris.stateManager.state.persistence) {
context.juris.stateManager.state.persistence.stats.priorityRestores =
(context.juris.stateManager.state.persistence.stats.priorityRestores || 0) + 1;
}
} else {
results.failed.push(domain);
}
log(`⚡ Priority restore completed for ${domain} immediately`);
} else {
// Delayed restore for non-critical domains
const restoreDelay = cumulativeDelay + domainConfig.delay;
setTimeout(() => {
if (restoreDomain(domain)) {
results.restored.push(domain);
results.delayed.push(domain);
// DIRECT INJECTION for stats
if (context.juris && context.juris.stateManager && context.juris.stateManager.state && context.juris.stateManager.state.persistence) {
context.juris.stateManager.state.persistence.stats.delayedRestores =
(context.juris.stateManager.state.persistence.stats.delayedRestores || 0) + 1;
}
} else {
results.failed.push(domain);
}
}, restoreDelay);
log(`⏳ Delayed restore scheduled for ${domain} at ${restoreDelay}ms`);
cumulativeDelay += domainConfig.delay;
}
});
// Update final restore timestamp
const finalDelay = Math.max(cumulativeDelay, config.earlyRestoreTimeout);
setTimeout(() => {
// DIRECT INJECTION for lastRestore
if (context.juris && context.juris.stateManager && context.juris.stateManager.state && context.juris.stateManager.state.persistence) {
context.juris.stateManager.state.persistence.lastRestore = Date.now();
}
log(`📂 Restore sequence completed. Priority: ${results.priority.length}, Delayed: ${results.delayed.length}, Failed: ${results.failed.length}`);
}, finalDelay);
return results;
}
function restorePriorityDomains() {
log('⚡ Restoring priority domains only...');
const priorityResults = { restored: [], failed: [] };
config.priorityDomains.forEach(domain => {
if (restoreDomain(domain)) {
priorityResults.restored.push(domain);
} else {
priorityResults.failed.push(domain);
}
});
log(`⚡ Priority restore completed: [${priorityResults.restored.join(', ')}]`);
return priorityResults;
}
function setupDomainMonitoring() {
log('🔍 Setting up domain monitoring...');
const allState = context.juris.stateManager.state;
const availableDomains = Object.keys(allState);
log(`🔍 Available domains in state:`, availableDomains);
// Clear existing subscriptions
domainSubscriptions.forEach(unsubscribe => unsubscribe());
domainSubscriptions.clear();
// Determine which domains to track
let domainsToTrack = [];
if (config.domains.length > 0) {
domainsToTrack = config.domains.filter(domain => {
const exists = availableDomains.includes(domain);
if (!exists) {
log(`⚠️ Configured domain '${domain}' not found in state`);
}
return exists && !config.excludeDomains.includes(domain);
});
} else {
domainsToTrack = availableDomains.filter(domain =>
!config.excludeDomains.includes(domain)
);
}
log(`📊 Domains to track: [${domainsToTrack.join(', ')}]`);
// Track each domain
domainsToTrack.forEach(domain => {
addDomainTracking(domain);
});
// Update stats - DIRECT INJECTION
if (context.juris && context.juris.stateManager && context.juris.stateManager.state && context.juris.stateManager.state.persistence) {
context.juris.stateManager.state.persistence.stats.domainsTracked = domainSubscriptions.size;
}
log(`✅ Now tracking ${domainSubscriptions.size} domains: [${Array.from(domainSubscriptions.keys()).join(', ')}]`);
}
function setupDomainWatcher() {
domainWatcher = setInterval(() => {
const currentDomains = Object.keys(context.juris.stateManager.state);
const trackedDomains = Array.from(domainSubscriptions.keys());
const newDomains = currentDomains.filter(domain =>
!trackedDomains.includes(domain) &&
!config.excludeDomains.includes(domain) &&
(config.domains.length === 0 || config.domains.includes(domain))
);
if (newDomains.length > 0) {
log(`🆕 Detected new domains: [${newDomains.join(', ')}]`);
newDomains.forEach(domain => addDomainTracking(domain));
// DIRECT INJECTION for stats
if (context.juris && context.juris.stateManager && context.juris.stateManager.state && context.juris.stateManager.state.persistence) {
context.juris.stateManager.state.persistence.stats.domainsTracked = domainSubscriptions.size;
}
}
}, config.watchIntervalMs);
log(`👀 Domain watcher started (checking every ${config.watchIntervalMs}ms)`);
}
function addDomainTracking(domain) {
if (domainSubscriptions.has(domain)) {
log(`⚠️ Domain ${domain} already being tracked`);
return false;
}
try {
const testValue = getState(domain);
log(`🔍 Testing domain '${domain}': ${testValue !== undefined ? 'exists' : 'undefined'}`);
// Use internal subscription
const unsubscribe = context.juris.stateManager.subscribeInternal(domain, () => {
if (!isRestoring) {
const currentValue = getState(domain);
log(`🔄 State change detected in domain: ${domain}`, { currentValue });
debouncedSave(domain, currentValue);
}
});
domainSubscriptions.set(domain, unsubscribe);
log(`➕ Added tracking for domain: ${domain}`);
return true;
} catch (error) {
logError(`Failed to add tracking for domain ${domain}:`, error);
return false;
}
}
function removeDomainTracking(domain) {
const unsubscribe = domainSubscriptions.get(domain);
if (unsubscribe) {
unsubscribe();
domainSubscriptions.delete(domain);
log(`➖ Removed tracking for domain: ${domain}`);
return true;
}
log(`⚠️ Domain ${domain} was not being tracked`);
return false;
}
function debouncedSave(domain, value) {
// Clear existing timer
if (saveTimers.has(domain)) {
clearTimeout(saveTimers.get(domain));
}
// Determine save timing based on domain configuration
let saveDelay = config.debounceMs;
if (config.immediateSave.includes(domain)) {
// Immediate save for critical domains
saveDomain(domain, true);
return;
} else if (config.criticalSave.includes(domain)) {
// Faster save for critical domains
saveDelay = config.criticalDebounceMs;
}
const timer = setTimeout(() => {
saveDomain(domain, false);
saveTimers.delete(domain);
}, saveDelay);
saveTimers.set(domain, timer);
const saveType = config.criticalSave.includes(domain) ? 'CRITICAL' : 'NORMAL';
log(`⏰ ${saveType} save scheduled for domain: ${domain} in ${saveDelay}ms`);
}
function saveDomain(domain, immediate = false) {
try {
const value = getState(domain);
if (value === undefined || value === null) {
log(`⚠️ Skipping save for undefined domain: ${domain}`);
return false;
}
const dataPackage = {
value: value,
timestamp: Date.now(),
domain: domain,
immediate: immediate
};
// Check if domain requires user-specific storage
const isUserSpecific = config.userSpecificDomains.includes(domain);
const currentUserId = getUserId();
if (isUserSpecific && config.requireAuth && !currentUserId) {
log(`⚠️ Skipping save for user-specific domain '${domain}' - no authenticated user`);
return false;
}
// Build storage key
let storageKey = config.keyPrefix + domain;
if (isUserSpecific && currentUserId) {
storageKey = `${storageKey}_${currentUserId}`;
}
localStorage.setItem(storageKey, JSON.stringify(dataPackage));
// Update statistics - DIRECT INJECTION
if (context.juris && context.juris.stateManager && context.juris.stateManager.state && context.juris.stateManager.state.persistence) {
const stats = context.juris.stateManager.state.persistence.stats;
stats.totalSaves = (stats.totalSaves || 0) + 1;
context.juris.stateManager.state.persistence.lastSave = {
domain,
timestamp: Date.now(),
size: JSON.stringify(dataPackage).length,
immediate
};
}
const saveType = immediate ? 'IMMEDIATE' : 'DEBOUNCED';
log(`💾 ${saveType} saved domain: ${domain} (${JSON.stringify(dataPackage).length} bytes) key: ${storageKey}`);
return true;
} catch (error) {
logError(`Failed to save domain ${domain}:`, error);
return false;
}
}
function restoreDomain(domain) {
try {
const isUserSpecific = config.userSpecificDomains.includes(domain);
const currentUserId = getUserId();
// Build storage key
let storageKey = config.keyPrefix + domain;
if (isUserSpecific && currentUserId) {
storageKey = `${storageKey}_${currentUserId}`;
}
const stored = localStorage.getItem(storageKey);
if (!stored) {
log(`📂 No stored data for domain: ${domain} (key: ${storageKey})`);
return false;
}
const data = JSON.parse(stored);
// DIRECT INJECTION - Restore directly to state without triggering subscriptions
isRestoring = true;
// Direct injection into the state manager's internal state
if (context.juris && context.juris.stateManager && context.juris.stateManager.state) {
context.juris.stateManager.state[domain] = data.value;
log(`📂 DIRECT INJECT: ${domain} directly injected into state`);
} else {
// Fallback to setState if direct access not available
setState(domain, data.value);
log(`📂 FALLBACK: ${domain} restored via setState`);
}
isRestoring = false;
// Update statistics - DIRECT INJECTION
if (context.juris && context.juris.stateManager && context.juris.stateManager.state && context.juris.stateManager.state.persistence) {
const stats = context.juris.stateManager.state.persistence.stats;
stats.totalRestores = (stats.totalRestores || 0) + 1;
context.juris.stateManager.state.persistence.lastRestore = {
domain,
timestamp: Date.now(),
dataTimestamp: data.timestamp
};
}
log(`📂 Restored domain: ${domain} (saved ${new Date(data.timestamp).toLocaleString()})`);
return true;
} catch (error) {
logError(`Failed to restore domain ${domain}:`, error);
return false;
}
}
function saveAllTrackedDomains() {
log('💾 Saving all tracked domains...');
const savedDomains = [];
const failedDomains = [];
domainSubscriptions.forEach((unsubscribe, domain) => {
if (saveDomain(domain, true)) {
savedDomains.push(domain);
} else {
failedDomains.push(domain);
}
});
log(`💾 Saved ${savedDomains.length} domains: [${savedDomains.join(', ')}]`);
if (failedDomains.length > 0) {
logError(`❌ Failed to save ${failedDomains.length} domains: [${failedDomains.join(', ')}]`);
}
return { saved: savedDomains, failed: failedDomains };
}
function handleStorageEvent(event) {
if (!event.key || !event.key.startsWith(config.keyPrefix)) {
return;
}
// Extract domain from key
let remainder = event.key.substring(config.keyPrefix.length);
let domain = remainder.split('_')[0];
if (domainSubscriptions.has(domain)) {
log(`🔄 Storage changed externally for domain: ${domain}`);
if (event.newValue) {
// Parse and directly inject the new value
try {
const data = JSON.parse(event.newValue);
isRestoring = true;
// DIRECT INJECTION for cross-tab sync
if (context.juris && context.juris.stateManager && context.juris.stateManager.state) {
context.juris.stateManager.state[domain] = data.value;
log(`🔄 DIRECT INJECT: ${domain} synced from external tab`);
} else {
setState(domain, data.value);
log(`🔄 FALLBACK: ${domain} synced via setState`);
}
isRestoring = false;
} catch (error) {
logError(`Failed to parse external change for ${domain}:`, error);
}
} else {
// Value was deleted externally
isRestoring = true;
if (context.juris && context.juris.stateManager && context.juris.stateManager.state) {
delete context.juris.stateManager.state[domain];
log(`🔄 DIRECT DELETE: ${domain} removed from state`);
} else {
setState(domain, undefined);
log(`🔄 FALLBACK DELETE: ${domain} removed via setState`);
}
isRestoring = false;
}
}
}
function clearDomainStorage(domain) {
try {
const isUserSpecific = config.userSpecificDomains.includes(domain);
const currentUserId = getUserId();
let storageKey = config.keyPrefix + domain;
if (isUserSpecific && currentUserId) {
storageKey = `${storageKey}_${currentUserId}`;
}
localStorage.removeItem(storageKey);
log(`🗑️ Cleared storage for domain: ${domain} (key: ${storageKey})`);
return true;
} catch (error) {
logError(`Failed to clear storage for domain ${domain}:`, error);
return false;
}
}
function clearAllStorage() {
try {
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(config.keyPrefix)) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => localStorage.removeItem(key));
log(`🗑️ Cleared ${keysToRemove.length} storage entries`);
return true;
} catch (error) {
logError('Failed to clear all storage:', error);
return false;
}
}
function getStorageStats() {
let totalSize = 0;
let entryCount = 0;
const domains = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(config.keyPrefix)) {
const value = localStorage.getItem(key);
totalSize += key.length + (value ? value.length : 0);
entryCount++;
// Extract domain from key
let remainder = key.substring(config.keyPrefix.length);
let domain = remainder.split('_')[0];
domains.push(domain);
}
}
return {
totalSize,
entryCount,
domains: [...new Set(domains)], // Remove duplicates
trackedDomains: Array.from(domainSubscriptions.keys()),
config: {
aggressiveRestore: config.aggressiveRestore,
priorityDomains: config.priorityDomains,
immediateSave: config.immediateSave,
criticalSave: config.criticalSave,
keyPrefix: config.keyPrefix,
userSpecificDomains: config.userSpecificDomains
}
};
}
function exportState() {
const exportData = {
timestamp: Date.now(),
domains: {}
};
domainSubscriptions.forEach((unsubscribe, domain) => {
exportData.domains[domain] = getState(domain);
});
return exportData;
}
function importState(data) {
try {
if (!data.domains) {
throw new Error('Invalid import data format');
}
isRestoring = true;
Object.entries(data.domains).forEach(([domain, value]) => {
// DIRECT INJECTION for import as well
if (context.juris && context.juris.stateManager && context.juris.stateManager.state) {
context.juris.stateManager.state[domain] = value;
log(`📥 DIRECT INJECT: ${domain} imported directly into state`);
} else {
setState(domain, value);
log(`📥 FALLBACK: ${domain} imported via setState`);
}
saveDomain(domain, true);
});
isRestoring = false;
log(`📥 Imported ${Object.keys(data.domains).length} domains`);
return true;
} catch (error) {
isRestoring = false;
logError('Import failed:', error);
return false;
}
}
// Helper function to get user ID from configurable path
function getUserId() {
try {
const pathParts = config.userIdPath.split('.');
let value = context.juris.stateManager.state;
for (const part of pathParts) {
if (value && typeof value === 'object') {
value = value[part];
} else {
return null;
}
}
return value;
} catch (error) {
log(`⚠️ Failed to get user ID from path: ${config.userIdPath}`, error);
return null;
}
}
function log(message, ...args) {
if (config.debug) {
console.log(`💾 [StatePersistence] ${message}`, ...args);
}
}
function logError(message, error = null) {
console.error(`💾 [StatePersistence] ${message}`, error);
// DIRECT INJECTION for errors
if (context.juris && context.juris.stateManager && context.juris.stateManager.state && context.juris.stateManager.state.persistence) {
const errors = context.juris.stateManager.state.persistence.errors;
errors.push({
message,
error: error ? error.message : null,
timestamp: Date.now()
});
if (errors.length > 10) {
errors.splice(0, errors.length - 10);
}
}
}
};
// URL State Sync with Route Guards
const UrlStateSync = (props, context) => {
const { getState, setState } = context;
const routeGuards = {
'/': ['authenticated'],
'/lists': ['authenticated'],
'/profile': ['authenticated'],
'/settings': ['authenticated']
};
return {
hooks: {
onRegister: () => {
console.log('🧭 UrlStateSync initializing...');
handleUrlChange();
window.addEventListener('hashchange', handleUrlChange);
window.addEventListener('popstate', handleUrlChange);
}
}
};
async function handleUrlChange() {
const hash = window.location.hash.substring(1) || '/';
const segments = parseSegments(hash);
// Check route guards
const guardResult = await checkRouteAccess(hash);
if (guardResult.allowed) {
setState('url.path', hash);
setState('url.segments', segments);
console.log('🧭 URL updated:', hash);
} else {
console.log('🚫 Route access denied, redirecting to:', guardResult.redirect);
window.location.hash = guardResult.redirect;
}
}
async function checkRouteAccess(path) {
const guards = getRouteGuards(path);
const isAuthenticated = getState('auth.isLoggedIn', false);
if (guards.includes('authenticated') && !isAuthenticated) {
return { allowed: false, redirect: '/login' };
}
return { allowed: true };
}
function getRouteGuards(path) {
if (routeGuards[path]) return routeGuards[path];
const segments = path.split('/').filter(Boolean);
for (let i = segments.length - 1; i > 0; i--) {
const parentPath = '/' + segments.slice(0, i).join('/');
if (routeGuards[parentPath]) return routeGuards[parentPath];
}
return [];
}
function parseSegments(path) {
const parts = path.split('/').filter(Boolean);
return {
full: path,
parts: parts,
base: parts[0] || '',
sub: parts[1] || '',
id: parts[2] || ''
};
}
};
// Authentication Manager
const AuthManager = (props, context) => {
const { getState, setState } = context;
return {
hooks: {
onRegister: () => {
console.log('🔐 AuthManager initializing...');
checkExistingAuth();
}
},
api: {
login: async (email, password) => {
setState('auth.loading', true);
setState('auth.error', null);
try {
await new Promise(resolve => setTimeout(resolve, 1000));
const users = getState('storage.users', []);
const user = users.find(u => u.email === email && u.password === hashPassword(password));
if (!user) {
throw new Error('Email ou mot de passe invalide');
}
const token = generateToken();
setState('auth.user', user);
setState('auth.token', token);
setState('auth.isLoggedIn', true);
// Store in localStorage
localStorage.setItem('todo_auth_token', token);
localStorage.setItem('todo_auth_user', JSON.stringify(user));
console.log('✅ Login successful');
// Trigger user-specific data restore after login
setTimeout(() => {
context.headless.StatePersistenceManager.restoreAllDomains();
}, 100);
return { success: true };
} catch (error) {
setState('auth.error', error.message);
console.error('❌ Login failed:', error.message);
return { success: false, error: error.message };
} finally {
setState('auth.loading', false);
}
},
register: async (email, password, name) => {
setState('auth.loading', true);
setState('auth.error', null);
try {
await new Promise(resolve => setTimeout(resolve, 1000));
const users = getState('storage.users', []);
if (users.find(u => u.email === email)) {
throw new Error('Cet email existe déjà');
}
const newUser = {
id: Date.now().toString(),
email,
password: hashPassword(password),
name,
createdAt: new Date().toISOString()
};
const updatedUsers = [...users, newUser];
setState('storage.users', updatedUsers);
console.log('✅ Registration successful');
return { success: true };
} catch (error) {
setState('auth.error', error.message);
console.error('❌ Registration failed:', error.message);
return { success: false, error: error.message };
} finally {
setState('auth.loading', false);
}
},
logout: () => {
// Save current state before logout
context.headless.StatePersistenceManager.saveAllDomains();
setState('auth.user', null);
setState('auth.token', null);
setState('auth.isLoggedIn', false);
// Clear user-specific state
setState('ui.selectedListId', null);
setState('todos.lists', []);
setState('todos.items', {});
setState('todos.filter', 'all');
localStorage.removeItem('todo_auth_token');
localStorage.removeItem('todo_auth_user');
window.location.hash = '/login';
console.log('👋 Logged out');
}
}
};
function checkExistingAuth() {
const token = localStorage.getItem('todo_auth_token');
const userStr = localStorage.getItem('todo_auth_user');
if (token && userStr) {
try {
const user = JSON.parse(userStr);
setState('auth.user', user);
setState('auth.token', token);
setState('auth.isLoggedIn', true);
console.log('🔄 Restored authentication');
// Trigger user-specific data restore after auth restore
setTimeout(() => {
context.headless.StatePersistenceManager.restoreAllDomains();
}, 100);
} catch (error) {
console.error('❌ Failed to restore auth:', error);
localStorage.removeItem('todo_auth_token');
localStorage.removeItem('todo_auth_user');
}
}
}
function hashPassword(password) {
return CryptoJS.SHA256(password).toString();
}
function generateToken() {
return CryptoJS.lib.WordArray.random(32).toString();
}
};
// Enhanced Todo Manager
const TodoManager = (props, context) => {
const { getState, setState, subscribe } = context;
return {
hooks: {
onRegister: () => {
console.log('📝 TodoManager initializing...');
loadUserTodos();
// Subscribe to auth changes
subscribe('auth.user', (user) => {
if (user) {
loadUserTodos();
} else {
setState('todos', { lists: [], items: {}, filters: {} });
setState('ui.selectedListId', null);
}
});
}
},
api: {
createList: (name) => {
const user = getState('auth.user');
if (!user) return;
const newList = {
id: Date.now().toString(),
name,
userId: user.id,
createdAt: new Date().toISOString()
};
const lists = getState('todos.lists', []);
setState('todos.lists', [...lists, newList]);
// Auto-select the new list
setState('ui.selectedListId', newList.id);
saveTodos();
console.log('✅ List created:', name);
},
deleteList: (listId) => {
const lists = getState('todos.lists', []);
const items = getState('todos.items', {});
// Remove list
const updatedLists = lists.filter(list => list.id !== listId);
setState('todos.lists', updatedLists);
// Remove all items in this list
const updatedItems = { ...items };
delete updatedItems[listId];
setState('todos.items', updatedItems);
// Clear selection if this list was selected
const selectedId = getState('ui.selectedListId');
if (selectedId === listId) {
// Auto-select the first remaining list or null
const newSelectedId = updatedLists.length > 0 ? updatedLists[0].id : null;
setState('ui.selectedListId', newSelectedId);
}
saveTodos();
console.log('🗑️ List deleted:', listId);
},
createTodo: (listId, text) => {
const newTodo = {
id: Date.now().toString(),
text,
completed: false,
createdAt: new Date().toISOString()
};
const items = getState('todos.items', {});
const listItems = items[listId] || [];
setState(`todos.items.${listId}`, [...listItems, newTodo]);
saveTodos();
console.log('✅ Todo created:', text);
},
toggleTodo: (listId, todoId) => {
const items = getState(`todos.items.${listId}`, []);
const updatedItems = items.map(item =>
item.id === todoId ? { ...item, completed: !item.completed } : item
);
setState(`todos.items.${listId}`, updatedItems);
saveTodos();
},
deleteTodo: (listId, todoId) => {
const items = getState(`todos.items.${listId}`, []);
const updatedItems = items.filter(item => item.id !== todoId);
setState(`todos.items.${listId}`, updatedItems);
saveTodos();
},
setFilter: (filter) => {
setState('todos.filter', filter);
},
selectList: (listId) => {
setState('ui.selectedListId', listId);
console.log('📋 Selected list:', listId);
}
}
};
function loadUserTodos() {
const user = getState('auth.user');
if (!user) return;
const stored = localStorage.getItem(`todo_data_${user.id}`);
if (stored) {
try {
const data = JSON.parse(stored);
setState('todos.lists', data.lists || []);
setState('todos.items', data.items || {});
console.log('📚 Todos loaded for user:', user.email);
} catch (error) {
console.error('❌ Failed to load todos:', error);
}
}
}
function saveTodos() {
const user = getState('auth.user');
if (!user) return;
const data = {
lists: getState('todos.lists', []),
items: getState('todos.items', {}),
lastSaved: new Date().toISOString()
};
localStorage.setItem(`todo_data_${user.id}`, JSON.stringify(data));
console.log('💾 Todos saved');
}
};
// ==================== UI COMPONENTS (with French translations) ====================
// App Layout
const AppLayout = (props, context) => {
const { getState } = context;
return {
render: () => ({
div: {
className: 'app-container',
children: () => {
const isLoggedIn = getState('auth.isLoggedIn', false);
const currentPath = getState('url.path', '/');
if (!isLoggedIn && !['/login', '/register'].includes(currentPath)) {
return [{ AuthLayout: {} }];
}
if (['/login', '/register'].includes(currentPath)) {
return [{ AuthLayout: {} }];
}
return [
{ AppHeader: {} },
{ MainContent: {} }
];
}
}
})
};
};
// App Header
const AppHeader = (props, context) => {
const { getState } = context;
return {
render: () => ({
header: {
className: 'header',
children: [{
div: {
className: 'header-content',
children: [
{
a: {
className: 'logo',
href: '#/',
text: '📝 Todo App ',
onclick: (e) => {
e.preventDefault();
window.location.hash = '/';
}
}
},
{
nav: {
className: 'nav',
children: [
{
a: {
className: () => {
const path = getState('url.path', '/');
return path === '/' ? 'nav-link active' : 'nav-link';
},
href: '#/',
text: 'Tableau de bord',
onclick: (e) => {
e.preventDefault();
window.location.hash = '/';
}
}
},
{
a: {
className: () => {
const path = getState('url.path', '/');
return path === '/profile' ? 'nav-link active' : 'nav-link';
},
href: '#/profile',
text: 'Profil',
onclick: (e) => {
e.preventDefault();
window.location.hash = '/profile';
}
}
},
{
button: {
className: 'btn btn-secondary btn-small',
text: 'Déconnexion',
onclick: () => context.headless.AuthManager.logout()
}
}
]
}
}
]
}
}]
}
})
};
};
// Auth Layout
const AuthLayout = (props, context) => {
const { getState } = context;
return {
render: () => ({
div: {
className: 'app-container',
children: [{
main: {
className: 'main-content',
children: () => {
const path = getState('url.path', '/');
if (path === '/register') {
return [{ RegisterForm: {} }];
}
return [{ LoginForm: {} }];
}
}
}]
}
})
};
};
// Login Form
const LoginForm = (props, context) => {
const { getState, setState, headless } = context;
return {
render: () => ({
div: {
className: 'form-container fade-in',
children: [
{
h2: {
className: 'form-title',
text: 'Bon retour !'
}
},
{
form: {
onsubmit: async (e) => {
e.preventDefault();
const email = getState('auth.form.email', '');
const password = getState('auth.form.password', '');
const result = await headless.AuthManager.login(email, password);
if (result.success) {
window.location.hash = '/';
}
},
children: [
{
div: {
className: 'form-group',
children: [
{
label: {
className: 'form-label',
text: 'Email'
}
},
{
input: {
type: 'email',
className: 'form-input',
placeholder: 'Saisissez votre email',
value: () => getState('auth.form.email', ''),
oninput: (e) => setState('auth.form.email', e.target.value),
required: true
}
}
]
}
},
{
div: {
className: 'form-group',
children: [
{
label: {
className: 'form-label',
text: 'Mot de passe'
}
},
{
input: {
type: 'password',
className: 'form-input',
placeholder: 'Saisissez votre mot de passe',
value: () => getState('auth.form.password', ''),
oninput: (e) => setState('auth.form.password', e.target.value),
required: true
}
}
]
}
},
{
div: {
className: () => getState('auth.error') ? 'form-error' : 'hidden',
text: () => getState('auth.error', '')
}
},
{
button: {
type: 'submit',
className: 'btn btn-primary btn-full',
disabled: () => getState('auth.loading', false),
text: () => getState('auth.loading') ? 'Connexion...' : 'Se connecter'
}
},
{
div: {
className: 'text-center mt-2',
children: [
{
span: {
text: "Pas encore de compte ? "
}
},
{
a: {
href: '#/register',
text: 'Inscrivez-vous',
onclick: (e) => {
e.preventDefault();
window.location.hash = '/register';
}
}
}
]
}
}
]
}
}
]
}
})
};
};
// Register Form
const RegisterForm = (props, context) => {
const { getState, setState, headless } = context;
return {
render: () => ({
div: {
className: 'form-container fade-in',
children: [
{
h2: {
className: 'form-title',
text: 'Créer un compte'
}
},
{
form: {
onsubmit: async (e) => {
e.preventDefault();
const name = getState('auth.form.name', '');
const email = getState('auth.form.email', '');
const password = getState('auth.form.password', '');
const result = await headless.AuthManager.register(email, password, name);
if (result.success) {
setState('auth.form', {});
window.location.hash = '/login';
}
},
children: [
{
div: {
className: 'form-group',
children: [
{
label: {
className: 'form-label',
text: 'Nom complet'
}
},
{
input: {
type: 'text',
className: 'form-input',
placeholder: 'Saisissez votre nom complet',
value: () => getState('auth.form.name', ''),
oninput: (e) => setState('auth.form.name', e.target.value),
required: true
}
}
]
}
},
{
div: {
className: 'form-group',
children: [
{
label: {
className: 'form-label',
text: 'Email'
}
},
{
input: {
type: 'email',
className: 'form-input',
placeholder: 'Saisissez votre email',
value: () => getState('auth.form.email', ''),
oninput: (e) => setState('auth.form.email', e.target.value),
required: true
}
}
]
}
},
{
div: {
className: 'form-group',
children: [
{
label: {
className: 'form-label',
text: 'Mot de passe'
}
},
{
input: {
type: 'password',
className: 'form-input',
placeholder: 'Créez un mot de passe',
value: () => getState('auth.form.password', ''),
oninput: (e) => setState('auth.form.password', e.target.value),
required: true
}
}
]
}
},
{
div: {
className: () => getState('auth.error') ? 'form-error' : 'hidden',
text: () => getState('auth.error', '')
}
},
{
button: {
type: 'submit',
className: 'btn btn-primary btn-full',
disabled: () => getState('auth.loading', false),
text: () => getState('auth.loading') ? 'Création du compte...' : 'Créer un compte'
}
},
{
div: {
className: 'text-center mt-2',
children: [
{
span: {
text: "Vous avez déjà un compte ? "
}
},
{
a: {
href: '#/login',
text: 'Connectez-vous',
onclick: (e) => {
e.preventDefault();
window.location.hash = '/login';
}
}
}
]
}
}
]
}
}
]
}
})
};
};
// Main Content
const MainContent = (props, context) => {
const { getState } = context;
return {
render: () => ({
main: {
className: 'main-content',
children: () => {
const segments = getState('url.segments', { base: '' });
switch (segments.base) {
case '':
return [{ TodoDashboard: {} }];
case 'lists':
if (segments.sub) {
return [{ TodoListDetail: { listId: segments.sub } }];
}
return [{ TodoDashboard: {} }];
case 'profile':
return [{ UserProfile: {} }];
case 'settings':
return [{ AppSettings: {} }];
default:
return [{ NotFound: {} }];
}
}
}
})
};
};
// Todo Dashboard
const TodoDashboard = (props, context) => {
const { getState, setState, headless } = context;
return {
render: () => ({
div: {
className: 'todo-dashboard fade-in',
children: [
{
div: {
className: 'todo-lists',
children: [
{
div: {
className: 'section-title',
text: 'Vos listes de tâches'
}
},
{
div: {
className: 'todo-form',
children: [{
form: {
onsubmit: (e) => {
e.preventDefault();
const name = getState('ui.newListName', '').trim();
if (name) {
headless.TodoManager.createList(name);
setState('ui.newListName', '');
}
},
children: [{
div: {
className: 'todo-input',
children: [
{
input: {
type: 'text',
style: { flex: '0.75' },
className: 'form-input',
placeholder: 'Saisir le nom de la liste',
value: () => getState('ui.newListName', ''),
oninput: (e) => setState('ui.newListName', e.target.value)
}
},
{
button: {
style: { flex: 0.25 },
type: 'submit',
className: 'btn btn-primary',
text: 'Ajouter'
}
}
]
}
}]
}
}]
}
},
{
div: {
children: () => {
const lists = getState('todos.lists', []);
if (lists.length === 0) {
return [{
div: {
className: 'empty-state',
children: [
{
h3: {
text: 'Aucune liste pour le moment'
}
},
{
p: {
text: 'Créez votre première liste de tâches pour commencer !'
}
}
]
}
}];
}
return lists.map(list => ({
TodoListItem: { list, key: list.id }
}));
}
}
}
]
}
},
{
div: {
className: 'todo-list',
children: () => {
const selectedListId = getState('ui.selectedListId');
const lists = getState('todos.lists', []);
const selectedList = lists.find(l => l.id === selectedListId);
if (!selectedList) {
return [{
div: {
className: 'empty-state',
children: [
{
h3: {
text: 'Sélectionnez une liste'
}
},
{
p: {
text: 'Choisissez une liste dans la barre latérale pour voir et gérer vos tâches.'
}
}
]
}
}];
}
return [{ TodoListDetail: { listId: selectedListId } }];
}
}
}
]
}
})
};
};
// Todo List Item
const TodoListItem = (props, context) => {
const { getState, setState, headless } = context;
const { list } = props;
return {
render: () => ({
div: {
className: () => {
const selectedId = getState('ui.selectedListId');
return selectedId === list.id ? 'list-item active' : 'list-item';
},
onclick: () => headless.TodoManager.selectList(list.id),
children: [
{
div: {
className: 'list-info',
children: [
{
div: {
className: 'list-name',
text: list.name
}
},
{
div: {
className: 'list-count',
text: () => {
const items = getState(`todos.items.${list.id}`, []);
const completed = items.filter(item => item.completed).length;
return `${completed}/${items.length} terminées`;
}
}
}
]
}
},
{
div: {
className: 'list-actions',
children: [{
button: {
className: 'btn btn-danger btn-small',
text: '🗑️',
onclick: (e) => {
e.stopPropagation();
if (confirm(`Supprimer "${list.name}" ?`)) {
headless.TodoManager.deleteList(list.id);
}
}
}
}]
}
}
]
}
})
};
};
// Todo List Detail
const TodoListDetail = (props, context) => {
const { getState, setState, headless } = context;
const { listId } = props;
return {
render: () => ({
div: {
className: 'fade-in',
children: [
{
div: {
className: 'section-title',
text: () => {
const lists = getState('todos.lists', []);
const list = lists.find(l => l.id === listId);
return list ? list.name : 'Liste de tâches';
}
}
},
{
div: {
className: 'todo-form',
children: [{
form: {
onsubmit: (e) => {
e.preventDefault();
const text = getState('ui.newTodoText', '').trim();
if (text) {
headless.TodoManager.createTodo(listId, text);
setState('ui.newTodoText', '');
}
},
children: [{
div: {
className: 'todo-input',
children: [
{
input: {
type: 'text',
className: 'form-input',
placeholder: 'Ajouter une nouvelle tâche...',
value: () => getState('ui.newTodoText', ''),
oninput: (e) => setState('ui.newTodoText', e.target.value)
}
},
{
button: {
type: 'submit',
className: 'btn btn-primary',
text: 'Ajouter'
}
}
]
}
}]
}
}]
}
},
{
div: {
className: 'filters',
children: [
{
button: {
className: () => {
const filter = getState('todos.filter', 'all');
return filter === 'all' ? 'filter-btn active' : 'filter-btn';
},
text: 'Toutes',
onclick: () => headless.TodoManager.setFilter('all')
}
},
{
button: {
className: () => {
const filter = getState('todos.filter', 'all');
return filter === 'active' ? 'filter-btn active' : 'filter-btn';
},
text: 'Actives',
onclick: () => headless.TodoManager.setFilter('active')
}
},
{
button: {
className: () => {
const filter = getState('todos.filter', 'all');
return filter === 'completed' ? 'filter-btn active' : 'filter-btn';
},
text: 'Terminées',
onclick: () => headless.TodoManager.setFilter('completed')
}
}
]
}
},
{
div: {
children: () => {
const items = getState(`todos.items.${listId}`, []);
const filter = getState('todos.filter', 'all');
let filteredItems = items;
if (filter === 'active') {
filteredItems = items.filter(item => !item.completed);
} else if (filter === 'completed') {
filteredItems = items.filter(item => item.completed);
}
if (filteredItems.length === 0) {
return [{
div: {
className: 'empty-state',
children: [
{
h3: {
text: () => {
const filter = getState('todos.filter', 'all');
if (filter === 'active') return 'Aucune tâche active';
if (filter === 'completed') return 'Aucune tâche terminée';
return 'Aucune tâche pour le moment';
}
}
},
{
p: {
text: 'Ajoutez votre première tâche ci-dessus !'
}
}
]
}
}];
}
return filteredItems.map(item => ({
TodoItem: { listId, item, key: item.id }
}));
}
}
}
]
}
})
};
};
// Todo Item
const TodoItem = (props, context) => {
const { headless } = context;
const { listId, item } = props;
return {
render: () => ({
div: {
className: 'todo-item',
children: [
{
input: {
type: 'checkbox',
className: 'todo-checkbox',
checked: item.completed,
onchange: () => headless.TodoManager.toggleTodo(listId, item.id)
}
},
{
span: {
className: item.completed ? 'todo-text completed' : 'todo-text',
text: item.text
}
},
{
div: {
className: 'todo-actions',
children: [{
button: {
className: 'btn btn-danger btn-small',
text: '🗑️',
onclick: () => {
if (confirm('Supprimer cette tâche ?')) {
headless.TodoManager.deleteTodo(listId, item.id);
}
}
}
}]
}
}
]
}
})
};
};
// User Profile
const UserProfile = (props, context) => {
const { getState } = context;
return {
render: () => ({
div: {
className: 'fade-in',
children: [{
div: {
className: 'form-container',
children: [
{
h2: {
className: 'form-title',
text: 'Profil'
}
},
{
div: {
className: 'form-group',
children: [
{
label: {
className: 'form-label',
text: 'Nom'
}
},
{
div: {
className: 'form-input',
style: {
background: 'var(--gray-100)',
border: 'none',
color: 'var(--gray-700)'
},
text: () => getState('auth.user.name', 'Non disponible')
}
}
]
}
},
{
div: {
className: 'form-group',
children: [
{
label: {
className: 'form-label',
text: 'Membre depuis'
}
},
{
div: {
className: 'form-input',
style: {
background: 'var(--gray-100)',
border: 'none',
color: 'var(--gray-700)'
},
text: () => {
const createdAt = getState('auth.user.createdAt');
return createdAt ? new Date(createdAt).toLocaleDateString() : 'Non disponible';
}
}
}
]
}
},
{
div: {
className: 'form-group',
children: [
{
label: {
className: 'form-label',
text: 'Statistiques'
}
},
{
div: {
style: {
background: 'var(--gray-50)',
padding: '1rem',
borderRadius: 'var(--border-radius)',
border: '1px solid var(--gray-200)'
},
children: [
{
div: {
style: { marginBottom: '0.5rem' },
children: [
{
span: {
style: { fontWeight: '500' },
text: 'Total des listes : '
}
},
{
span: {
text: () => getState('todos.lists', []).length.toString()
}
}
]
}
},
{
div: {
style: { marginBottom: '0.5rem' },
children: [
{
span: {
style: { fontWeight: '500' },
text: 'Total des tâches : '
}
},
{
span: {
text: () => {
const items = getState('todos.items', {});
const total = Object.values(items).reduce((sum, list) => sum + list.length, 0);
return total.toString();
}
}
}
]
}
},
{
div: {
children: [
{
span: {
style: { fontWeight: '500' },
text: 'Terminées : '
}
},
{
span: {
text: () => {
const items = getState('todos.items', {});
const completed = Object.values(items)
.flat()
.filter(item => item.completed).length;
return completed.toString();
}
}
}
]
}
}
]
}
}
]
}
}
]
}
}]
}
})
};
};
// App Settings
const AppSettings = (props, context) => {
const { getState, setState, headless } = context;
return {
render: () => ({
div: {
className: 'fade-in',
children: [{
div: {
className: 'form-container',
children: [
{
h2: {
className: 'form-title',
text: 'Paramètres'
}
},
{
div: {
className: 'form-group',
children: [
{
label: {
className: 'form-label',
text: 'Filtre par défaut'
}
},
{
select: {
className: 'form-input',
value: () => getState('settings.defaultFilter', 'all'),
onchange: (e) => setState('settings.defaultFilter', e.target.value),
children: [
{
option: {
value: 'all',
text: 'Toutes les tâches'
}
},
{
option: {
value: 'active',
text: 'Tâches actives'
}
},
{
option: {
value: 'completed',
text: 'Tâches terminées'
}
}
]
}
}
]
}
},
{
div: {
className: 'form-group',
children: [
{
label: {
className: 'form-label',
children: [
{
input: {
type: 'checkbox',
checked: () => getState('settings.autoSave', true),
onchange: (e) => setState('settings.autoSave', e.target.checked),
style: { marginRight: '0.5rem' }
}
},
{
span: {
text: 'Sauvegarde automatique'
}
}
]
}
}
]
}
},
{
div: {
className: 'form-group',
children: [
{
label: {
className: 'form-label',
text: 'Statistiques de Persistance'
}
},
{
div: {
style: {
background: 'var(--gray-50)',
padding: '1rem',
borderRadius: 'var(--border-radius)',
border: '1px solid var(--gray-200)'
},
children: () => {
const stats = getState('persistence.stats', {});
return [
{
div: {
style: { marginBottom: '0.5rem' },
children: [
{
span: {
style: { fontWeight: '500' },
text: 'Domaines Suivis : '
}
},
{
span: {
text: (stats.domainsTracked || 0).toString()
}
}
]
}
},
{
div: {
style: { marginBottom: '0.5rem' },
children: [
{
span: {
style: { fontWeight: '500' },
text: 'Total Sauvegardes : '
}
},
{
span: {
text: (stats.totalSaves || 0).toString()
}
}
]
}
},
{
div: {
children: [
{
span: {
style: { fontWeight: '500' },
text: 'Total Restaurations : '
}
},
{
span: {
text: (stats.totalRestores || 0).toString()
}
}
]
}
}
];
}
}
}
]
}
},
{
div: {
className: 'form-group',
children: [
{
button: {
className: 'btn btn-secondary btn-full',
style: { marginBottom: '0.5rem' },
text: 'Exporter toutes les données',
onclick: () => {
try {
const exportData = headless.StatePersistenceManager.exportState();
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `todo-app-sauvegarde-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
alert('Données exportées avec succès !');
} catch (error) {
alert("L'exportation a échoué : " + error.message);
}
}
}
}
]
}
},
{
div: {
className: 'form-group',
children: [
{
button: {
className: 'btn btn-danger btn-full',
text: 'Effacer toutes les données',
onclick: () => {
if (confirm('Ceci supprimera toutes vos listes et données. Cette action est irréversible. Êtes-vous sûr(e) ?')) {
const user = getState('auth.user');
if (user) {
// Clear old todo data
localStorage.removeItem(`todo_data_${user.id}`);
// Clear new persistence data
headless.StatePersistenceManager.clearAllStorage();
// Reset state
setState('todos.lists', []);
setState('todos.items', {});
setState('ui.selectedListId', null);
alert('Toutes les données ont été effacées.');
}
}
}
}
}
]
}
}
]
}
}]
}
})
};
};
// Not Found Page
const NotFound = (props, context) => {
return {
render: () => ({
div: {
className: 'empty-state fade-in',
children: [
{
h3: {
text: '404 - Page non trouvée'
}
},
{
p: {
text: "La page que vous cherchez n'existe pas."
}
},
{
a: {
href: '#/',
className: 'btn btn-primary',
text: 'Aller au Tableau de bord',
onclick: (e) => {
e.preventDefault();
window.location.hash = '/';
}
}
}
]
}
})
};
};
// ==================== APPLICATION INITIALIZATION ====================
const juris = new Juris({
components: {
AppLayout,
AppHeader,
AuthLayout,
LoginForm,
RegisterForm,
MainContent,
TodoDashboard,
TodoListItem,
TodoListDetail,
TodoItem,
UserProfile,
AppSettings,
NotFound
},
headlessComponents: {
AuthManager: { fn: AuthManager, options: { autoInit: true } },
UrlStateSync: { fn: UrlStateSync, options: { autoInit: true } },
TodoManager: { fn: TodoManager, options: { autoInit: true } },
StatePersistenceManager: {
fn: StatePersistenceManager,
options: {
autoInit: true,
debug: true, // Enable debug logging
domains: ['ui', 'todos', 'settings', 'storage'], // Specify domains to track
priorityDomains: ['ui'], // UI gets priority restore for selected list
immediateSave: ['ui'], // Save UI changes immediately (like selected list)
criticalSave: ['todos'], // Save todos with faster debounce
aggressiveRestore: true, // Restore immediately on startup
keyPrefix: 'todo_app_state_',
// User-specific domains will have user ID appended automatically
domainRestoreConfig: {
ui: { priority: 1, delay: 0, aggressive: true }, // Restore selected list first
todos: { priority: 2, delay: 0, aggressive: true },
settings: { priority: 3, delay: 100, aggressive: false },
storage: { priority: 4, delay: 200, aggressive: false }
}
}
}
},
layout: {
div: {
children: [{ AppLayout: {} }]
}
},
states: {
url: {
path: '/',
segments: { full: '/', parts: [], base: '', sub: '', id: '' }
},
auth: {
user: null,
token: null,
isLoggedIn: false,
loading: false,
error: null,
form: {}
},
todos: {
lists: [],
items: {},
filter: 'all'
},
ui: {
selectedListId: '', // This will now be persisted!
newListName: '',
newTodoText: ''
},
storage: {
users: [],
settings: {}
},
settings: {
defaultFilter: 'all',
autoSave: true
}
}
});
// Start the application
juris.render('#app');
// Auto-navigate to login if no hash
if (!window.location.hash) {
// Don't force login redirect, let auth check handle it
}
// Global access for debugging
window.juris = juris;
console.log('🚀 Todo App est prête !');
console.log('📱 Routes disponibles :');
console.log(' - #/login (public)');
console.log(' - #/register (public)');
console.log(' - #/ (protégé - tableau de bord)');
console.log(' - #/profile (protégé)');
console.log(' - #/settings (protégé)');
console.log('💾 Les données persistent dans localStorage');
console.log('🔐 Authentification avec gardes de route');
console.log('🧭 Routage basé sur l\'état');
</script>
</body>
</html>
Top comments (0)