feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,10 @@
goog.provide('fontface.Descriptors');
/**
* @typedef {{
* style: (string|undefined),
* weight: (string|undefined),
* stretch: (string|undefined)
* }}
*/
fontface.Descriptors;

View File

@@ -0,0 +1,355 @@
goog.provide('fontface.Observer');
goog.require('fontface.Ruler');
goog.require('dom');
goog.scope(function () {
var Ruler = fontface.Ruler;
/**
* @constructor
*
* @param {string} family
* @param {fontface.Descriptors=} opt_descriptors
* @param {Window=} opt_context
*/
fontface.Observer = function (family, opt_descriptors, opt_context) {
var descriptors = opt_descriptors || {};
var context = opt_context || window;
/**
* @type {string}
*/
this['family'] = family;
/**
* @type {string}
*/
this['style'] = descriptors.style || 'normal';
/**
* @type {string}
*/
this['weight'] = descriptors.weight || 'normal';
/**
* @type {string}
*/
this['stretch'] = descriptors.stretch || 'normal';
/**
* @type {Window}
*/
this['context'] = context;
};
var Observer = fontface.Observer;
/**
* @type {null|boolean}
*/
Observer.HAS_WEBKIT_FALLBACK_BUG = null;
/**
* @type {null|boolean}
*/
Observer.HAS_SAFARI_10_BUG = null;
/**
* @type {null|boolean}
*/
Observer.SUPPORTS_STRETCH = null;
/**
* @type {null|boolean}
*/
Observer.SUPPORTS_NATIVE_FONT_LOADING = null;
/**
* @type {number}
*/
Observer.DEFAULT_TIMEOUT = 3000;
/**
* @return {string}
*/
Observer.getUserAgent = function () {
return window.navigator.userAgent;
};
/**
* @return {string}
*/
Observer.getNavigatorVendor = function () {
return window.navigator.vendor;
};
/**
* Returns true if this browser is WebKit and it has the fallback bug
* which is present in WebKit 536.11 and earlier.
*
* @return {boolean}
*/
Observer.hasWebKitFallbackBug = function () {
if (Observer.HAS_WEBKIT_FALLBACK_BUG === null) {
var match = /AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(Observer.getUserAgent());
Observer.HAS_WEBKIT_FALLBACK_BUG = !!match &&
(parseInt(match[1], 10) < 536 ||
(parseInt(match[1], 10) === 536 &&
parseInt(match[2], 10) <= 11));
}
return Observer.HAS_WEBKIT_FALLBACK_BUG;
};
/**
* Returns true if the browser has the Safari 10 bugs. The
* native font load API in Safari 10 has two bugs that cause
* the document.fonts.load and FontFace.prototype.load methods
* to return promises that don't reliably get settled.
*
* The bugs are described in more detail here:
* - https://bugs.webkit.org/show_bug.cgi?id=165037
* - https://bugs.webkit.org/show_bug.cgi?id=164902
*
* If the browser is made by Apple, and has native font
* loading support, it is potentially affected. But the API
* was fixed around AppleWebKit version 603, so any newer
* versions that that does not contain the bug.
*
* @return {boolean}
*/
Observer.hasSafari10Bug = function (context) {
if (Observer.HAS_SAFARI_10_BUG === null) {
if (Observer.supportsNativeFontLoading(context) && /Apple/.test(Observer.getNavigatorVendor())) {
var match = /AppleWebKit\/([0-9]+)(?:\.([0-9]+))(?:\.([0-9]+))/.exec(Observer.getUserAgent());
Observer.HAS_SAFARI_10_BUG = !!match && parseInt(match[1], 10) < 603;
} else {
Observer.HAS_SAFARI_10_BUG = false;
}
}
return Observer.HAS_SAFARI_10_BUG;
};
/**
* Returns true if the browser supports the native font loading
* API.
*
* @return {boolean}
*/
Observer.supportsNativeFontLoading = function (context) {
if (Observer.SUPPORTS_NATIVE_FONT_LOADING === null) {
Observer.SUPPORTS_NATIVE_FONT_LOADING = !!context.document['fonts'];
}
return Observer.SUPPORTS_NATIVE_FONT_LOADING;
};
/**
* Returns true if the browser supports font-style in the font
* short-hand syntax.
*
* @return {boolean}
*/
Observer.supportStretch = function () {
if (Observer.SUPPORTS_STRETCH === null) {
var div = dom.createElement('div');
try {
div.style.font = 'condensed 100px sans-serif';
} catch (e) {}
Observer.SUPPORTS_STRETCH = (div.style.font !== '');
}
return Observer.SUPPORTS_STRETCH;
};
/**
* @private
*
* @param {string} family
* @return {string}
*/
Observer.prototype.getStyle = function (family) {
return [this['style'], this['weight'], Observer.supportStretch() ? this['stretch'] : '', '100px', family].join(' ');
};
/**
* Returns the current time in milliseconds
*
* @return {number}
*/
Observer.prototype.getTime = function () {
return new Date().getTime();
};
/**
* @param {string=} text Optional test string to use for detecting if a font is available.
* @param {number=} timeout Optional timeout for giving up on font load detection and rejecting the promise (defaults to 3 seconds).
* @return {Promise.<fontface.Observer>}
*/
Observer.prototype.load = function (text, timeout) {
var that = this;
var testString = text || 'BESbswy';
var timeoutId = 0;
var timeoutValue = timeout || Observer.DEFAULT_TIMEOUT;
var start = that.getTime();
return new Promise(function (resolve, reject) {
if (Observer.supportsNativeFontLoading(that['context']) && !Observer.hasSafari10Bug(that['context'])) {
var loader = new Promise(function (resolve, reject) {
var check = function () {
var now = that.getTime();
if (now - start >= timeoutValue) {
reject(new Error('' + timeoutValue + 'ms timeout exceeded'));
} else {
that['context'].document.fonts.load(that.getStyle('"' + that['family'] + '"'), testString).then(function (fonts) {
if (fonts.length >= 1) {
resolve();
} else {
setTimeout(check, 25);
}
}, reject);
}
};
check();
});
var timer = new Promise(function (resolve, reject) {
timeoutId = setTimeout(
function() { reject(new Error('' + timeoutValue + 'ms timeout exceeded')); },
timeoutValue
);
});
Promise.race([timer, loader]).then(function () {
clearTimeout(timeoutId);
resolve(that);
}, reject);
} else {
dom.waitForBody(function () {
var rulerA = new Ruler(testString);
var rulerB = new Ruler(testString);
var rulerC = new Ruler(testString);
var widthA = -1;
var widthB = -1;
var widthC = -1;
var fallbackWidthA = -1;
var fallbackWidthB = -1;
var fallbackWidthC = -1;
var container = dom.createElement('div');
/**
* @private
*/
function removeContainer() {
if (container.parentNode !== null) {
dom.remove(container.parentNode, container);
}
}
/**
* @private
*
* If metric compatible fonts are detected, one of the widths will be -1. This is
* because a metric compatible font won't trigger a scroll event. We work around
* this by considering a font loaded if at least two of the widths are the same.
* Because we have three widths, this still prevents false positives.
*
* Cases:
* 1) Font loads: both a, b and c are called and have the same value.
* 2) Font fails to load: resize callback is never called and timeout happens.
* 3) WebKit bug: both a, b and c are called and have the same value, but the
* values are equal to one of the last resort fonts, we ignore this and
* continue waiting until we get new values (or a timeout).
*/
function check() {
if ((widthA != -1 && widthB != -1) || (widthA != -1 && widthC != -1) || (widthB != -1 && widthC != -1)) {
if (widthA == widthB || widthA == widthC || widthB == widthC) {
// All values are the same, so the browser has most likely loaded the web font
if (Observer.hasWebKitFallbackBug()) {
// Except if the browser has the WebKit fallback bug, in which case we check to see if all
// values are set to one of the last resort fonts.
if (((widthA == fallbackWidthA && widthB == fallbackWidthA && widthC == fallbackWidthA) ||
(widthA == fallbackWidthB && widthB == fallbackWidthB && widthC == fallbackWidthB) ||
(widthA == fallbackWidthC && widthB == fallbackWidthC && widthC == fallbackWidthC))) {
// The width we got matches some of the known last resort fonts, so let's assume we're dealing with the last resort font.
return;
}
}
removeContainer();
clearTimeout(timeoutId);
resolve(that);
}
}
}
// This ensures the scroll direction is correct.
container.dir = 'ltr';
rulerA.setFont(that.getStyle('sans-serif'));
rulerB.setFont(that.getStyle('serif'));
rulerC.setFont(that.getStyle('monospace'));
dom.append(container, rulerA.getElement());
dom.append(container, rulerB.getElement());
dom.append(container, rulerC.getElement());
dom.append(that['context'].document.body, container);
fallbackWidthA = rulerA.getWidth();
fallbackWidthB = rulerB.getWidth();
fallbackWidthC = rulerC.getWidth();
function checkForTimeout() {
var now = that.getTime();
if (now - start >= timeoutValue) {
removeContainer();
reject(new Error('' + timeoutValue + 'ms timeout exceeded'));
} else {
var hidden = that['context'].document['hidden'];
if (hidden === true || hidden === undefined) {
widthA = rulerA.getWidth();
widthB = rulerB.getWidth();
widthC = rulerC.getWidth();
check();
}
timeoutId = setTimeout(checkForTimeout, 50);
}
}
checkForTimeout();
rulerA.onResize(function (width) {
widthA = width;
check();
});
rulerA.setFont(that.getStyle('"' + that['family'] + '",sans-serif'));
rulerB.onResize(function (width) {
widthB = width;
check();
});
rulerB.setFont(that.getStyle('"' + that['family'] + '",serif'));
rulerC.onResize(function (width) {
widthC = width;
check();
});
rulerC.setFont(that.getStyle('"' + that['family'] + '",monospace'));
});
}
});
};
});

View File

@@ -0,0 +1,130 @@
goog.provide('fontface.Ruler');
goog.require('dom');
goog.scope(function () {
/**
* @constructor
* @param {string} text
*/
fontface.Ruler = function (text) {
var style = 'max-width:none;' +
'display:inline-block;' +
'position:absolute;' +
'height:100%;' +
'width:100%;' +
'overflow:scroll;' +
'font-size:16px;';
this.element = dom.createElement('div');
this.element.setAttribute('aria-hidden', 'true');
dom.append(this.element, dom.createText(text));
this.collapsible = dom.createElement('span');
this.expandable = dom.createElement('span');
this.collapsibleInner = dom.createElement('span');
this.expandableInner = dom.createElement('span');
this.lastOffsetWidth = -1;
dom.style(this.collapsible, style);
dom.style(this.expandable, style);
dom.style(this.expandableInner, style);
dom.style(this.collapsibleInner, 'display:inline-block;width:200%;height:200%;font-size:16px;max-width:none;');
dom.append(this.collapsible, this.collapsibleInner);
dom.append(this.expandable, this.expandableInner);
dom.append(this.element, this.collapsible);
dom.append(this.element, this.expandable);
};
var Ruler = fontface.Ruler;
/**
* @return {Element}
*/
Ruler.prototype.getElement = function () {
return this.element;
};
/**
* @param {string} font
*/
Ruler.prototype.setFont = function (font) {
dom.style(this.element, 'max-width:none;' +
'min-width:20px;' +
'min-height:20px;' +
'display:inline-block;' +
'overflow:hidden;' +
'position:absolute;' +
'width:auto;' +
'margin:0;' +
'padding:0;' +
'top:-999px;' +
'white-space:nowrap;' +
'font-synthesis:none;' +
'font:' + font + ';');
};
/**
* @return {number}
*/
Ruler.prototype.getWidth = function () {
return this.element.offsetWidth;
};
/**
* @param {string} width
*/
Ruler.prototype.setWidth = function (width) {
this.element.style.width = width + 'px';
};
/**
* @private
*
* @return {boolean}
*/
Ruler.prototype.reset = function () {
var offsetWidth = this.getWidth(),
width = offsetWidth + 100;
this.expandableInner.style.width = width + 'px';
this.expandable.scrollLeft = width;
this.collapsible.scrollLeft = this.collapsible.scrollWidth + 100;
if (this.lastOffsetWidth !== offsetWidth) {
this.lastOffsetWidth = offsetWidth;
return true;
} else {
return false;
}
};
/**
* @private
* @param {function(number)} callback
*/
Ruler.prototype.onScroll = function (callback) {
if (this.reset() && this.element.parentNode !== null) {
callback(this.lastOffsetWidth);
}
};
/**
* @param {function(number)} callback
*/
Ruler.prototype.onResize = function (callback) {
var that = this;
function onScroll() {
that.onScroll(callback);
}
dom.addListener(this.collapsible, 'scroll', onScroll);
dom.addListener(this.expandable, 'scroll', onScroll);
this.reset();
};
});