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,2 @@
<manifest>
</manifest>

View File

@@ -0,0 +1,105 @@
package expo.modules.imageloader
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import expo.modules.core.interfaces.InternalModule
import expo.modules.interfaces.imageloader.ImageLoaderInterface
import java.util.concurrent.ExecutionException
import java.util.concurrent.Future
class ImageLoaderModule(val context: Context) : InternalModule, ImageLoaderInterface {
override fun getExportedInterfaces(): List<Class<*>> {
return listOf(ImageLoaderInterface::class.java)
}
override fun loadImageForDisplayFromURL(url: String): Future<Bitmap> {
val future = SimpleSettableFuture<Bitmap>()
loadImageForDisplayFromURL(
url,
object : ImageLoaderInterface.ResultListener {
override fun onSuccess(bitmap: Bitmap) = future.set(bitmap)
override fun onFailure(cause: Throwable?) =
future.setException(ExecutionException(cause))
}
)
return future
}
override fun loadImageForDisplayFromURL(
url: String,
resultListener: ImageLoaderInterface.ResultListener
) {
Glide.with(context)
.asBitmap()
.load(url)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
resultListener.onSuccess(resource)
}
override fun onLoadCleared(placeholder: Drawable?) {
// no op
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
resultListener.onFailure(Exception("Loading bitmap failed"))
}
})
}
override fun loadImageForManipulationFromURL(url: String): Future<Bitmap> {
val future = SimpleSettableFuture<Bitmap>()
loadImageForManipulationFromURL(
url,
object : ImageLoaderInterface.ResultListener {
override fun onSuccess(bitmap: Bitmap) = future.set(bitmap)
override fun onFailure(cause: Throwable?) = future.setException(ExecutionException(cause))
}
)
return future
}
override fun loadImageForManipulationFromURL(
url: String,
resultListener: ImageLoaderInterface.ResultListener
) {
val normalizedUrl = normalizeAssetsUrl(url)
Glide.with(context)
.asBitmap()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.load(normalizedUrl)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
resultListener.onSuccess(resource)
}
override fun onLoadCleared(placeholder: Drawable?) {
// no op
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
resultListener.onFailure(Exception("Loading bitmap failed"))
}
})
}
private fun normalizeAssetsUrl(url: String): String {
var actualUrl = url
if (url.startsWith("asset:///")) {
actualUrl = "file:///android_asset/" + url.split("/").last()
}
return actualUrl
}
}

View File

@@ -0,0 +1,9 @@
package expo.modules.imageloader
import android.content.Context
import expo.modules.core.BasePackage
import expo.modules.core.interfaces.InternalModule
class ImageLoaderPackage : BasePackage() {
override fun createInternalModules(context: Context): List<InternalModule> = listOf(ImageLoaderModule(context))
}

View File

@@ -0,0 +1,120 @@
// Copied from React Native
package expo.modules.imageloader;
import androidx.annotation.Nullable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* A super simple Future-like class that can safely notify another Thread when a value is ready.
* Does not support canceling.
*/
public class SimpleSettableFuture<T> implements Future<T> {
private final CountDownLatch mReadyLatch = new CountDownLatch(1);
private @Nullable
T mResult;
private @Nullable
Exception mException;
/**
* Sets the result. If another thread has called {@link #get}, they will immediately receive the
* value. set or setException must only be called once.
*/
public void set(@Nullable T result) {
checkNotSet();
mResult = result;
mReadyLatch.countDown();
}
/**
* Sets the exception. If another thread has called {@link #get}, they will immediately receive
* the exception. set or setException must only be called once.
*/
public void setException(Exception exception) {
checkNotSet();
mException = exception;
mReadyLatch.countDown();
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
throw new UnsupportedOperationException();
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return mReadyLatch.getCount() == 0;
}
@Override
public @Nullable
T get() throws InterruptedException, ExecutionException {
mReadyLatch.await();
if (mException != null) {
throw new ExecutionException(mException);
}
return mResult;
}
/**
* Wait up to the timeout time for another Thread to set a value on this future. If a value has
* already been set, this method will return immediately.
*
* <p>NB: For simplicity, we catch and wrap InterruptedException. Do NOT use this class if you are
* in the 1% of cases where you actually want to handle that.
*/
@Override
public @Nullable
T get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
if (!mReadyLatch.await(timeout, unit)) {
throw new TimeoutException("Timed out waiting for result");
}
if (mException != null) {
throw new ExecutionException(mException);
}
return mResult;
}
/**
* Convenience wrapper for {@link #get()} that re-throws get()'s Exceptions as RuntimeExceptions.
*/
public @Nullable
T getOrThrow() {
try {
return get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
/**
* Convenience wrapper for {@link #get(long, TimeUnit)} that re-throws get()'s Exceptions as
* RuntimeExceptions.
*/
public @Nullable
T getOrThrow(long timeout, TimeUnit unit) {
try {
return get(timeout, unit);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new RuntimeException(e);
}
}
private void checkNotSet() {
if (mReadyLatch.getCount() == 0) {
throw new RuntimeException("Result has already been set!");
}
}
}