first commit
|
|
@ -0,0 +1,2 @@
|
|||
BUNDLE_PATH: "vendor/bundle"
|
||||
BUNDLE_FORCE_RUBY_PLATFORM: 1
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: '@react-native',
|
||||
};
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# Xcode
|
||||
#
|
||||
build/
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
**/.xcode.env.local
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
.cxx/
|
||||
*.keystore
|
||||
!debug.keystore
|
||||
.kotlin/
|
||||
|
||||
# node.js
|
||||
#
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
edasmart_backend/.env
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/
|
||||
|
||||
**/fastlane/report.xml
|
||||
**/fastlane/Preview.html
|
||||
**/fastlane/screenshots
|
||||
**/fastlane/test_output
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
# Ruby / CocoaPods
|
||||
**/Pods/
|
||||
/vendor/bundle/
|
||||
|
||||
# Temporary files created by Metro to check the health of the file watcher
|
||||
.metro-health-check*
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# Yarn
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
arrowParens: 'avoid',
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
{
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import { enableScreens } from 'react-native-screens';
|
||||
import AppStack from './src/Navigators/Stack';
|
||||
import { loadToken, cachedToken } from './src/api/api';
|
||||
|
||||
enableScreens();
|
||||
|
||||
const App = () => {
|
||||
// null = masih loading, string = initial route sudah ditentukan
|
||||
const [initialRoute, setInitialRoute] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const bootstrap = async () => {
|
||||
await loadToken();
|
||||
// Kalau token ada di storage → langsung ke Dashboard, tidak perlu login ulang
|
||||
setInitialRoute(cachedToken ? 'DashboardNavigation' : 'LoginNavigation');
|
||||
};
|
||||
bootstrap();
|
||||
}, []);
|
||||
|
||||
// Tunggu cek token selesai sebelum render navigator
|
||||
if (initialRoute === null) return null;
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<SafeAreaProvider>
|
||||
<NavigationContainer>
|
||||
<AppStack initialRoute={initialRoute} />
|
||||
</NavigationContainer>
|
||||
</SafeAreaProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
||||
ruby ">= 2.6.10"
|
||||
|
||||
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
|
||||
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
|
||||
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
|
||||
gem 'xcodeproj', '< 1.26.0'
|
||||
gem 'concurrent-ruby', '< 1.3.4'
|
||||
|
||||
# Ruby 3.4.0 has removed some libraries from the standard library.
|
||||
gem 'bigdecimal'
|
||||
gem 'logger'
|
||||
gem 'benchmark'
|
||||
gem 'mutex_m'
|
||||
gem 'nkf'
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli).
|
||||
|
||||
# Getting Started
|
||||
|
||||
> **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding.
|
||||
|
||||
## Step 1: Start Metro
|
||||
|
||||
First, you will need to run **Metro**, the JavaScript build tool for React Native.
|
||||
|
||||
To start the Metro dev server, run the following command from the root of your React Native project:
|
||||
|
||||
```sh
|
||||
# Using npm
|
||||
npm start
|
||||
|
||||
# OR using Yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Step 2: Build and run your app
|
||||
|
||||
With Metro running, open a new terminal window/pane from the root of your React Native project, and use one of the following commands to build and run your Android or iOS app:
|
||||
|
||||
### Android
|
||||
|
||||
```sh
|
||||
# Using npm
|
||||
npm run android
|
||||
|
||||
# OR using Yarn
|
||||
yarn android
|
||||
```
|
||||
|
||||
### iOS
|
||||
|
||||
For iOS, remember to install CocoaPods dependencies (this only needs to be run on first clone or after updating native deps).
|
||||
|
||||
The first time you create a new project, run the Ruby bundler to install CocoaPods itself:
|
||||
|
||||
```sh
|
||||
bundle install
|
||||
```
|
||||
|
||||
Then, and every time you update your native dependencies, run:
|
||||
|
||||
```sh
|
||||
bundle exec pod install
|
||||
```
|
||||
|
||||
For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html).
|
||||
|
||||
```sh
|
||||
# Using npm
|
||||
npm run ios
|
||||
|
||||
# OR using Yarn
|
||||
yarn ios
|
||||
```
|
||||
|
||||
If everything is set up correctly, you should see your new app running in the Android Emulator, iOS Simulator, or your connected device.
|
||||
|
||||
This is one way to run your app — you can also build it directly from Android Studio or Xcode.
|
||||
|
||||
## Step 3: Modify your app
|
||||
|
||||
Now that you have successfully run the app, let's make changes!
|
||||
|
||||
Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh).
|
||||
|
||||
When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload:
|
||||
|
||||
- **Android**: Press the <kbd>R</kbd> key twice or select **"Reload"** from the **Dev Menu**, accessed via <kbd>Ctrl</kbd> + <kbd>M</kbd> (Windows/Linux) or <kbd>Cmd ⌘</kbd> + <kbd>M</kbd> (macOS).
|
||||
- **iOS**: Press <kbd>R</kbd> in iOS Simulator.
|
||||
|
||||
## Congratulations! :tada:
|
||||
|
||||
You've successfully run and modified your React Native App. :partying_face:
|
||||
|
||||
### Now what?
|
||||
|
||||
- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps).
|
||||
- If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started).
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page.
|
||||
|
||||
# Learn More
|
||||
|
||||
To learn more about React Native, take a look at the following resources:
|
||||
|
||||
- [React Native Website](https://reactnative.dev) - learn more about React Native.
|
||||
- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
|
||||
- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
|
||||
- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
|
||||
- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactTestRenderer from 'react-test-renderer';
|
||||
import App from '../App';
|
||||
|
||||
test('renders correctly', async () => {
|
||||
await ReactTestRenderer.act(() => {
|
||||
ReactTestRenderer.create(<App />);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
apply plugin: "com.android.application"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||
*/
|
||||
react {
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
||||
// root = file("../../")
|
||||
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
|
||||
// reactNativeDir = file("../../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
|
||||
// codegenDir = file("../../node_modules/@react-native/codegen")
|
||||
// The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
|
||||
// cliFile = file("../../node_modules/react-native/cli.js")
|
||||
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. Default is "debug", "debugOptimized".
|
||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||
// debuggableVariants = ["liteDebug", "liteDebugOptimized", "prodDebug", "prodDebugOptimized"]
|
||||
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
// nodeExecutableAndArgs = ["node"]
|
||||
//
|
||||
// The command to run when bundling. By default is 'bundle'
|
||||
// bundleCommand = "ram-bundle"
|
||||
//
|
||||
// The path to the CLI configuration file. Default is empty.
|
||||
// bundleConfig = file(../rn-cli.config.js)
|
||||
//
|
||||
// The name of the generated asset file containing your JS bundle
|
||||
// bundleAssetName = "MyApplication.android.bundle"
|
||||
//
|
||||
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
||||
// entryFile = file("../js/MyApplication.android.js")
|
||||
//
|
||||
// A list of extra flags to pass to the 'bundle' commands.
|
||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||
// extraPackagerArgs = []
|
||||
|
||||
/* Hermes Commands */
|
||||
// The hermes compiler command to run. By default it is 'hermesc'
|
||||
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
||||
//
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
|
||||
/* Autolinking */
|
||||
autolinkLibrariesWithApp()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = false
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore (JSC)
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
|
||||
namespace "com.edasmart"
|
||||
defaultConfig {
|
||||
applicationId "com.edasmart"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="${usesCleartextTraffic}"
|
||||
android:supportsRtl="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.edasmart
|
||||
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "EdaSmart"
|
||||
|
||||
/**
|
||||
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate =
|
||||
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.edasmart
|
||||
|
||||
import android.app.Application
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
||||
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
|
||||
override val reactHost: ReactHost by lazy {
|
||||
getDefaultReactHost(
|
||||
context = applicationContext,
|
||||
packageList =
|
||||
PackageList(this).packages.apply {
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// add(MyReactNativePackage())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
loadReactNative(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetTop="@dimen/abc_edit_text_inset_top_material"
|
||||
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
|
||||
>
|
||||
|
||||
<selector>
|
||||
<!--
|
||||
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
|
||||
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
|
||||
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
|
||||
|
||||
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
|
||||
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
|
||||
-->
|
||||
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
|
||||
</selector>
|
||||
|
||||
</inset>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name">EdaSmart</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "36.0.0"
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 36
|
||||
targetSdkVersion = 36
|
||||
ndkVersion = "30.0.14904198"
|
||||
kotlinVersion = "2.1.20"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.facebook.react.rootproject"
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
||||
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||
|
||||
# Use this property to enable support to the new architecture.
|
||||
# This will allow you to use TurboModules and the Fabric render in
|
||||
# your application. You should enable this flag either if you want
|
||||
# to write custom TurboModules/Fabric components OR use libraries that
|
||||
# are providing them.
|
||||
newArchEnabled=true
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
|
||||
# Use this property to enable edge-to-edge display support.
|
||||
# This allows your app to draw behind system bars for an immersive UI.
|
||||
# Note: Only works with ReactActivity and should not be used with custom Activity.
|
||||
edgeToEdgeEnabled=false
|
||||
|
||||
systemProp.https.protocols=TLSv1.2,TLSv1.3
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
@REM Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
@REM
|
||||
@REM This source code is licensed under the MIT license found in the
|
||||
@REM LICENSE file in the root directory of this source tree.
|
||||
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
|
||||
plugins { id("com.facebook.react.settings") }
|
||||
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
|
||||
rootProject.name = 'EdaSmart'
|
||||
include ':app'
|
||||
includeBuild('../node_modules/@react-native/gradle-plugin')
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
module.exports = {
|
||||
presets: ['module:@react-native/babel-preset'],
|
||||
plugins: [
|
||||
[
|
||||
'module-resolver',
|
||||
{
|
||||
root: ['./'],
|
||||
alias: {
|
||||
'@Containers': './src/Containers',
|
||||
'@Constants': './src/Constants',
|
||||
'@Navigators': './src/Navigators',
|
||||
'@Dummy': './src/Dummy',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit c89a78682fa79a8186068b67edce19a67557c5cf
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
# EdaSmart ESP32 Firmware
|
||||
|
||||
Kode ini adalah firmware untuk ESP32 yang mengendalikan:
|
||||
- **Mesin Press** (naik/turun/tahan dengan limit switch & sonar HC-SR04)
|
||||
- **Mesin Penggiling** (timer + RPM via Hall Effect sensor)
|
||||
- **LCD 20×4 I2C** sebagai UI utama
|
||||
- **PCF8574** untuk ekspansi I/O (tombol & limit switch)
|
||||
- **MQTT over TLS** ke HiveMQ Cloud
|
||||
- **OTA firmware update** via Access Point
|
||||
|
||||
---
|
||||
|
||||
## Struktur File
|
||||
|
||||
```
|
||||
edasmart_esp32/
|
||||
├── edasmart_esp32.ino ← kode utama Arduino
|
||||
├── config.h ← konfigurasi MQTT & pin (edit di sini)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Konfigurasi
|
||||
|
||||
Edit `config.h` sebelum upload:
|
||||
|
||||
```cpp
|
||||
// WiFi (bisa diset lewat UI LCD langsung)
|
||||
// MQTT
|
||||
#define MQTT_SERVER "8dd0d24eadfb4eb0b1e6fd87628f0f4e.s1.eu.hivemq.cloud"
|
||||
#define MQTT_PORT 8883
|
||||
#define MQTT_USER "Edasmart"
|
||||
#define MQTT_PASS "KentangBalado1"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Topik MQTT
|
||||
|
||||
### Publish (ESP32 → Backend)
|
||||
|
||||
| Topic | Nilai | Keterangan |
|
||||
|--------------------------|---------------|--------------------------------|
|
||||
| `edasmart/press/status` | `ON` / `OFF` | Status mesin press |
|
||||
| `edasmart/press/phase` | `TURUN` dst | Fase gerakan press |
|
||||
| `edasmart/press/sisa` | integer detik | Sisa waktu tahan |
|
||||
| `edasmart/giling/status` | `ON` / `OFF` | Status mesin penggiling |
|
||||
| `edasmart/giling/rpm` | integer | RPM penggiling (Hall sensor) |
|
||||
| `edasmart/giling/sisa` | integer detik | Sisa waktu penggiling |
|
||||
| `edasmart/estop` | `TRIGGERED` / `RELEASED` | Status emergency stop |
|
||||
| `edasmart/device` | `ONLINE` | Device menyala (retained) |
|
||||
|
||||
### Subscribe (Backend → ESP32)
|
||||
|
||||
| Topic | Nilai | Keterangan |
|
||||
|------------------------|--------|-------------------------|
|
||||
| `edasmart/cmd/press` | `STOP` | Hentikan mesin press |
|
||||
| `edasmart/cmd/giling` | `STOP` | Hentikan mesin penggiling |
|
||||
|
||||
---
|
||||
|
||||
## Library Arduino yang Dibutuhkan
|
||||
|
||||
Install via **Arduino Library Manager**:
|
||||
|
||||
- `PubSubClient` by Nick O'Leary
|
||||
- `LiquidCrystal I2C` by Frank de Brabander
|
||||
- `PCF8574` by Rob Tillaart
|
||||
- `WiFi` (built-in ESP32)
|
||||
- `Preferences` (built-in ESP32)
|
||||
- `WebServer` (built-in ESP32)
|
||||
- `Update` (built-in ESP32)
|
||||
|
||||
---
|
||||
|
||||
## Pin Mapping
|
||||
|
||||
| Fungsi | Pin ESP32 |
|
||||
|------------------|-----------|
|
||||
| SDA (I2C) | 21 |
|
||||
| SCL (I2C) | 22 |
|
||||
| ENA (Motor A PWM)| 32 |
|
||||
| ENB (Motor B PWM)| 33 |
|
||||
| IN1 | 25 |
|
||||
| IN2 | 26 |
|
||||
| IN3 | 27 |
|
||||
| IN4 | 14 |
|
||||
| RELAY_PRESS | 16 |
|
||||
| RELAY_PENGGILING | 17 |
|
||||
| BUZZER | 23 |
|
||||
| LED BUILTIN | 2 |
|
||||
| HALL SENSOR | 34 |
|
||||
| TRIG HC-SR04 | 18 |
|
||||
| ECHO HC-SR04 | 19 |
|
||||
|
||||
**PCF8574 (I2C, addr 0x20):**
|
||||
|
||||
| Fungsi | Pin PCF |
|
||||
|--------------|---------|
|
||||
| BTN_OK | P1 |
|
||||
| BTN_BACK | P0 |
|
||||
| BTN_UP | P3 |
|
||||
| BTN_DOWN | P2 |
|
||||
| BTN_ESTOP | P6 (NC) |
|
||||
| BTN_LS_ATAS | P4 (NC) |
|
||||
| BTN_LS_BAWAH | P5 (NC) |
|
||||
|
||||
---
|
||||
|
||||
## Upload
|
||||
|
||||
1. Buka `edasmart_esp32.ino` di Arduino IDE
|
||||
2. Board: `ESP32 Dev Module`
|
||||
3. Upload speed: `115200`
|
||||
4. Setelah upload, set WiFi lewat menu **Setting → WiFi → Scan WiFi** di LCD
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import 'react-native-gesture-handler';
|
||||
import {AppRegistry} from 'react-native';
|
||||
import App from './App';
|
||||
import {name as appName} from './app.json';
|
||||
|
||||
AppRegistry.registerComponent(appName, () => App);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# This `.xcode.env` file is versioned and is used to source the environment
|
||||
# used when running script phases inside Xcode.
|
||||
# To customize your local environment, you can create an `.xcode.env.local`
|
||||
# file that is not versioned.
|
||||
|
||||
# NODE_BINARY variable contains the PATH to the node executable.
|
||||
#
|
||||
# Customize the NODE_BINARY variable here.
|
||||
# For example, to use nvm with brew, add the following line
|
||||
# . "$(brew --prefix nvm)/nvm.sh" --no-use
|
||||
export NODE_BINARY=$(command -v node)
|
||||
|
|
@ -0,0 +1,475 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0C80B921A6F3F58F76C31292 /* libPods-EdaSmart.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-EdaSmart.a */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
|
||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
13B07F961A680F5B00A75B9A /* EdaSmart.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EdaSmart.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = EdaSmart/Images.xcassets; sourceTree = "<group>"; };
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = EdaSmart/Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = EdaSmart/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
3B4392A12AC88292D35C810B /* Pods-EdaSmart.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-EdaSmart.debug.xcconfig"; path = "Target Support Files/Pods-EdaSmart/Pods-EdaSmart.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
5709B34CF0A7D63546082F79 /* Pods-EdaSmart.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-EdaSmart.release.xcconfig"; path = "Target Support Files/Pods-EdaSmart/Pods-EdaSmart.release.xcconfig"; sourceTree = "<group>"; };
|
||||
5DCACB8F33CDC322A6C60F78 /* libPods-EdaSmart.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-EdaSmart.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = EdaSmart/AppDelegate.swift; sourceTree = "<group>"; };
|
||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = EdaSmart/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0C80B921A6F3F58F76C31292 /* libPods-EdaSmart.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
13B07FAE1A68108700A75B9A /* EdaSmart */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
||||
761780EC2CA45674006654EE /* AppDelegate.swift */,
|
||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
|
||||
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
|
||||
);
|
||||
name = EdaSmart;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||
5DCACB8F33CDC322A6C60F78 /* libPods-EdaSmart.a */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Libraries;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07FAE1A68108700A75B9A /* EdaSmart */,
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
83CBBA001A601CBA00E9B192 /* Products */,
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
||||
BBD78D7AC51CEA395F1C20DB /* Pods */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
sourceTree = "<group>";
|
||||
tabWidth = 2;
|
||||
usesTabs = 0;
|
||||
};
|
||||
83CBBA001A601CBA00E9B192 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07F961A680F5B00A75B9A /* EdaSmart.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BBD78D7AC51CEA395F1C20DB /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B4392A12AC88292D35C810B /* Pods-EdaSmart.debug.xcconfig */,
|
||||
5709B34CF0A7D63546082F79 /* Pods-EdaSmart.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
13B07F861A680F5B00A75B9A /* EdaSmart */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "EdaSmart" */;
|
||||
buildPhases = (
|
||||
C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */,
|
||||
13B07F871A680F5B00A75B9A /* Sources */,
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||
00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */,
|
||||
E235C05ADACE081382539298 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = EdaSmart;
|
||||
productName = EdaSmart;
|
||||
productReference = 13B07F961A680F5B00A75B9A /* EdaSmart.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
83CBB9F71A601CBA00E9B192 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1210;
|
||||
TargetAttributes = {
|
||||
13B07F861A680F5B00A75B9A = {
|
||||
LastSwiftMigration = 1120;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "EdaSmart" */;
|
||||
compatibilityVersion = "Xcode 12.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 83CBB9F61A601CBA00E9B192;
|
||||
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
13B07F861A680F5B00A75B9A /* EdaSmart */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"$(SRCROOT)/.xcode.env.local",
|
||||
"$(SRCROOT)/.xcode.env",
|
||||
);
|
||||
name = "Bundle React Native code and images";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"\\\"$WITH_ENVIRONMENT\\\" \\\"$REACT_NATIVE_XCODE\\\"\"\n";
|
||||
};
|
||||
00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-EdaSmart/Pods-EdaSmart-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-EdaSmart/Pods-EdaSmart-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-EdaSmart/Pods-EdaSmart-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-EdaSmart-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-EdaSmart/Pods-EdaSmart-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-EdaSmart/Pods-EdaSmart-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-EdaSmart/Pods-EdaSmart-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
13B07F871A680F5B00A75B9A /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-EdaSmart.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = EdaSmart/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = EdaSmart;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
13B07F951A680F5B00A75B9A /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-EdaSmart.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
INFOPLIST_FILE = EdaSmart/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = EdaSmart;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
83CBBA201A601CBA00E9B192 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"\"$(SDKROOT)/usr/lib/swift\"",
|
||||
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
|
||||
"\"$(inherited)\"",
|
||||
);
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_CPLUSPLUSFLAGS = (
|
||||
"$(OTHER_CFLAGS)",
|
||||
"-DFOLLY_NO_CONFIG",
|
||||
"-DFOLLY_MOBILE=1",
|
||||
"-DFOLLY_USE_LIBCPP=1",
|
||||
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||
);
|
||||
SDKROOT = iphoneos;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
83CBBA211A601CBA00E9B192 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"\"$(SDKROOT)/usr/lib/swift\"",
|
||||
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
|
||||
"\"$(inherited)\"",
|
||||
);
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
OTHER_CPLUSPLUSFLAGS = (
|
||||
"$(OTHER_CFLAGS)",
|
||||
"-DFOLLY_NO_CONFIG",
|
||||
"-DFOLLY_MOBILE=1",
|
||||
"-DFOLLY_USE_LIBCPP=1",
|
||||
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||
);
|
||||
SDKROOT = iphoneos;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "EdaSmart" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
13B07F941A680F5B00A75B9A /* Debug */,
|
||||
13B07F951A680F5B00A75B9A /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "EdaSmart" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
83CBBA201A601CBA00E9B192 /* Debug */,
|
||||
83CBBA211A601CBA00E9B192 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1210"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "EdaSmart.app"
|
||||
BlueprintName = "EdaSmart"
|
||||
ReferencedContainer = "container:EdaSmart.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
|
||||
BuildableName = "EdaSmartTests.xctest"
|
||||
BlueprintName = "EdaSmartTests"
|
||||
ReferencedContainer = "container:EdaSmart.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "EdaSmart.app"
|
||||
BlueprintName = "EdaSmart"
|
||||
ReferencedContainer = "container:EdaSmart.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "EdaSmart.app"
|
||||
BlueprintName = "EdaSmart"
|
||||
ReferencedContainer = "container:EdaSmart.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import UIKit
|
||||
import React
|
||||
import React_RCTAppDelegate
|
||||
import ReactAppDependencyProvider
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
var reactNativeDelegate: ReactNativeDelegate?
|
||||
var reactNativeFactory: RCTReactNativeFactory?
|
||||
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||
) -> Bool {
|
||||
let delegate = ReactNativeDelegate()
|
||||
let factory = RCTReactNativeFactory(delegate: delegate)
|
||||
delegate.dependencyProvider = RCTAppDependencyProvider()
|
||||
|
||||
reactNativeDelegate = delegate
|
||||
reactNativeFactory = factory
|
||||
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
|
||||
factory.startReactNative(
|
||||
withModuleName: "EdaSmart",
|
||||
in: window,
|
||||
launchOptions: launchOptions
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
|
||||
override func sourceURL(for bridge: RCTBridge) -> URL? {
|
||||
self.bundleURL()
|
||||
}
|
||||
|
||||
override func bundleURL() -> URL? {
|
||||
#if DEBUG
|
||||
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
|
||||
#else
|
||||
Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>EdaSmart</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<!-- Do not change NSAllowsArbitraryLoads to true, or you will risk app rejection! -->
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<false/>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string></string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="EdaSmart" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="GJd-Yh-RWb">
|
||||
<rect key="frame" x="0.0" y="202" width="375" height="43"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Powered by React Native" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="MN2-I3-ftu">
|
||||
<rect key="frame" x="0.0" y="626" width="375" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Bcu-3y-fUS" firstAttribute="bottom" secondItem="MN2-I3-ftu" secondAttribute="bottom" constant="20" id="OZV-Vh-mqD"/>
|
||||
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="GJd-Yh-RWb" secondAttribute="centerX" id="Q3B-4B-g5h"/>
|
||||
<constraint firstItem="MN2-I3-ftu" firstAttribute="centerX" secondItem="Bcu-3y-fUS" secondAttribute="centerX" id="akx-eg-2ui"/>
|
||||
<constraint firstItem="MN2-I3-ftu" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" id="i1E-0Y-4RG"/>
|
||||
<constraint firstItem="GJd-Yh-RWb" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="bottom" multiplier="1/3" constant="1" id="moa-c2-u7t"/>
|
||||
<constraint firstItem="GJd-Yh-RWb" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" symbolic="YES" id="x7j-FC-K8j"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="52.173913043478265" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>35F9.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# Resolve react_native_pods.rb with node to allow for hoisting
|
||||
require Pod::Executable.execute_command('node', ['-p',
|
||||
'require.resolve(
|
||||
"react-native/scripts/react_native_pods.rb",
|
||||
{paths: [process.argv[1]]},
|
||||
)', __dir__]).strip
|
||||
|
||||
platform :ios, min_ios_version_supported
|
||||
prepare_react_native_project!
|
||||
|
||||
linkage = ENV['USE_FRAMEWORKS']
|
||||
if linkage != nil
|
||||
Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
|
||||
use_frameworks! :linkage => linkage.to_sym
|
||||
end
|
||||
|
||||
target 'EdaSmart' do
|
||||
config = use_native_modules!
|
||||
|
||||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
# An absolute path to your application root.
|
||||
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
||||
)
|
||||
|
||||
post_install do |installer|
|
||||
react_native_post_install(
|
||||
installer,
|
||||
config[:reactNativePath],
|
||||
:mac_catalyst_enabled => false,
|
||||
# :ccache_enabled => true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
preset: '@react-native/jest-preset',
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
|
||||
|
||||
/**
|
||||
* Metro configuration
|
||||
* https://reactnative.dev/docs/metro
|
||||
*
|
||||
* @type {import('@react-native/metro-config').MetroConfig}
|
||||
*/
|
||||
const config = {};
|
||||
|
||||
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"name": "EdaSmart",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
"ios": "react-native run-ios",
|
||||
"lint": "eslint .",
|
||||
"start": "react-native start",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^3.1.1",
|
||||
"@react-native/new-app-screen": "0.85.3",
|
||||
"@react-navigation/native": "^7.2.4",
|
||||
"@react-navigation/native-stack": "^7.15.1",
|
||||
"axios": "^1.17.0",
|
||||
"react": "19.2.3",
|
||||
"react-native": "0.85.3",
|
||||
"react-native-gesture-handler": "^2.31.2",
|
||||
"react-native-safe-area-context": "^5.8.0",
|
||||
"react-native-screens": "^4.25.2",
|
||||
"react-native-vector-icons": "^10.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.3",
|
||||
"@babel/runtime": "^7.25.0",
|
||||
"@react-native-community/cli": "20.1.0",
|
||||
"@react-native-community/cli-platform-android": "20.1.0",
|
||||
"@react-native-community/cli-platform-ios": "20.1.0",
|
||||
"@react-native/babel-preset": "0.85.3",
|
||||
"@react-native/eslint-config": "0.85.3",
|
||||
"@react-native/jest-preset": "0.85.3",
|
||||
"@react-native/metro-config": "0.85.3",
|
||||
"@react-native/typescript-config": "0.85.3",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-native-vector-icons": "^6.4.18",
|
||||
"@types/react-test-renderer": "^19.1.0",
|
||||
"babel-plugin-module-resolver": "^5.0.3",
|
||||
"eslint": "^8.19.0",
|
||||
"jest": "^29.6.3",
|
||||
"prettier": "2.8.8",
|
||||
"react-test-renderer": "19.2.3",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22.11.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import React from 'react';
|
||||
import { View, Text, Pressable, StyleSheet } from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { RootStackParamList } from '../../Constants/RouteParamsList.contants';
|
||||
import RouteName from '../../Constants/RouteName.constants';
|
||||
|
||||
type NavProp = NativeStackNavigationProp<RootStackParamList>;
|
||||
|
||||
interface Props {
|
||||
activeScreen: 'Dashboard' | 'Kontrol' | 'Riwayat' | 'Pengaturan';
|
||||
}
|
||||
|
||||
const NAV_CONFIG = [
|
||||
{
|
||||
iconActive: 'grid',
|
||||
iconInactive: 'grid-outline',
|
||||
label: 'Dashboard',
|
||||
key: 'Dashboard',
|
||||
route: RouteName.DashboardNavigation,
|
||||
},
|
||||
{
|
||||
iconActive: 'calendar',
|
||||
iconInactive: 'calendar-outline',
|
||||
label: 'Kontrol',
|
||||
key: 'Kontrol',
|
||||
route: RouteName.ScheduleNavigation,
|
||||
},
|
||||
{
|
||||
iconActive: 'time',
|
||||
iconInactive: 'time-outline',
|
||||
label: 'Riwayat',
|
||||
key: 'Riwayat',
|
||||
route: RouteName.HistoryNavigation,
|
||||
},
|
||||
];
|
||||
|
||||
const Navbar = ({ activeScreen }: Props) => {
|
||||
const navigation = useNavigation<NavProp>();
|
||||
|
||||
return (
|
||||
<View style={styles.navbar}>
|
||||
{NAV_CONFIG.map((item, i) => {
|
||||
const isActive = activeScreen === item.key;
|
||||
return (
|
||||
<Pressable
|
||||
key={i}
|
||||
style={styles.navItem}
|
||||
onPress={() => {
|
||||
if (item.route && !isActive) {
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [{ name: item.route as any }],
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<Icon
|
||||
name={isActive ? item.iconActive : item.iconInactive}
|
||||
size={24}
|
||||
color={isActive ? '#1d9e75' : '#4b5563'}
|
||||
/>
|
||||
<Text style={[styles.navLabel, isActive ? styles.navActive : styles.navInactive]}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
navbar: {
|
||||
backgroundColor: '#161b24',
|
||||
borderTopWidth: 0.5,
|
||||
borderTopColor: '#2a2e3a',
|
||||
flexDirection: 'row',
|
||||
paddingTop: 10,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
navItem: { flex: 1, alignItems: 'center', gap: 4 },
|
||||
navLabel: { fontSize: 11 },
|
||||
navActive: { color: '#1d9e75' },
|
||||
navInactive: { color: '#4b5563' },
|
||||
});
|
||||
|
||||
export default Navbar;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
const RouteName = {
|
||||
LoginNavigation: 'LoginNavigation',
|
||||
RegisterNavigation: 'RegisterNavigation',
|
||||
DashboardNavigation: 'DashboardNavigation',
|
||||
ScheduleNavigation: 'ScheduleNavigation',
|
||||
HistoryNavigation: 'HistoryNavigation',
|
||||
} as const;
|
||||
|
||||
export default RouteName;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import RouteName from './RouteName.constants';
|
||||
|
||||
export type RootStackParamList = {
|
||||
[RouteName.LoginNavigation]: undefined;
|
||||
[RouteName.RegisterNavigation]: undefined;
|
||||
[RouteName.DashboardNavigation]: undefined;
|
||||
[RouteName.ScheduleNavigation]: undefined;
|
||||
[RouteName.HistoryNavigation]: undefined;
|
||||
};
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import styles from './styles';
|
||||
import useLogin from './useLogin';
|
||||
|
||||
const LoginScreen = () => {
|
||||
const {
|
||||
email, setEmail,
|
||||
password, setPassword,
|
||||
showPassword, toggleShowPassword,
|
||||
loading, onLogin, goToRegister,
|
||||
} = useLogin();
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.content}
|
||||
keyboardShouldPersistTaps="handled">
|
||||
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<View style={styles.logoBox}>
|
||||
<Icon name="leaf" size={32} color="#1d9e75" />
|
||||
</View>
|
||||
<Text style={styles.logoTitle}>EdaSmart</Text>
|
||||
<Text style={styles.logoSub}>Sistem Manajemen Edamame</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<View>
|
||||
<Text style={styles.cardTitle}>Selamat datang</Text>
|
||||
<Text style={styles.cardSub}>Masuk ke akun kamu.</Text>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={styles.label}>Email</Text>
|
||||
<TextInput
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
placeholder="contoh@email.com"
|
||||
placeholderTextColor="#4b5563"
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
style={styles.input}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={styles.label}>Password</Text>
|
||||
<View style={styles.passwordRow}>
|
||||
<TextInput
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
placeholder="••••••••"
|
||||
placeholderTextColor="#4b5563"
|
||||
secureTextEntry={!showPassword}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
style={styles.passwordInput}
|
||||
/>
|
||||
<Pressable onPress={toggleShowPassword}>
|
||||
<Text style={styles.toggleText}>
|
||||
{showPassword ? 'Hide' : 'Show'}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
style={[styles.button, loading && styles.buttonDisabled]}
|
||||
onPress={onLogin}
|
||||
disabled={loading}>
|
||||
{loading
|
||||
? <ActivityIndicator color="#fff" />
|
||||
: <Text style={styles.buttonText}>Login</Text>}
|
||||
</Pressable>
|
||||
|
||||
<View style={styles.dividerRow}>
|
||||
<View style={styles.dividerLine} />
|
||||
<Text style={styles.dividerText}>atau</Text>
|
||||
<View style={styles.dividerLine} />
|
||||
</View>
|
||||
|
||||
<View style={styles.footerRow}>
|
||||
<Text style={styles.footerText}>Belum punya akun?</Text>
|
||||
<Pressable onPress={goToRegister}>
|
||||
<Text style={styles.footerLink}>Register</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginScreen;
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export default StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0f1117',
|
||||
},
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 24,
|
||||
gap: 24,
|
||||
},
|
||||
logoBox: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#1a2a22',
|
||||
borderWidth: 1.5,
|
||||
borderColor: '#1d9e75',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 4,
|
||||
},
|
||||
logoTitle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 24,
|
||||
fontWeight: '700',
|
||||
textAlign: 'center',
|
||||
marginTop: 8,
|
||||
},
|
||||
logoSub: {
|
||||
color: '#6b7280',
|
||||
fontSize: 13,
|
||||
textAlign: 'center',
|
||||
marginTop: 4,
|
||||
},
|
||||
card: {
|
||||
width: '100%',
|
||||
backgroundColor: '#161b24',
|
||||
borderRadius: 16,
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#2a2e3a',
|
||||
padding: 24,
|
||||
gap: 16,
|
||||
},
|
||||
cardTitle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
},
|
||||
cardSub: {
|
||||
color: '#6b7280',
|
||||
fontSize: 13,
|
||||
marginTop: 2,
|
||||
},
|
||||
label: {
|
||||
color: '#9ca3af',
|
||||
fontSize: 13,
|
||||
marginBottom: 6,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: '#0f1117',
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#2a2e3a',
|
||||
borderRadius: 10,
|
||||
padding: 12,
|
||||
color: '#ffffff',
|
||||
fontSize: 14,
|
||||
},
|
||||
passwordRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#0f1117',
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#2a2e3a',
|
||||
borderRadius: 10,
|
||||
},
|
||||
passwordInput: {
|
||||
flex: 1,
|
||||
padding: 12,
|
||||
color: '#ffffff',
|
||||
fontSize: 14,
|
||||
},
|
||||
toggleText: {
|
||||
color: '#6b7280',
|
||||
paddingHorizontal: 14,
|
||||
fontSize: 13,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#1d9e75',
|
||||
borderRadius: 10,
|
||||
padding: 14,
|
||||
alignItems: 'center',
|
||||
marginTop: 4,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: '#0f6e56',
|
||||
},
|
||||
buttonText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
},
|
||||
dividerRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
dividerLine: {
|
||||
flex: 1,
|
||||
height: 0.5,
|
||||
backgroundColor: '#2a2e3a',
|
||||
},
|
||||
dividerText: {
|
||||
color: '#4b5563',
|
||||
fontSize: 12,
|
||||
},
|
||||
footerRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
footerText: {
|
||||
color: '#6b7280',
|
||||
fontSize: 13,
|
||||
},
|
||||
footerLink: {
|
||||
color: '#1d9e75',
|
||||
fontSize: 13,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { useState } from 'react';
|
||||
import { Alert } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import RouteName from '../../../Constants/RouteName.constants';
|
||||
import { RootStackParamList } from '../../../Constants/RouteParamsList.contants';
|
||||
import api, { setToken } from '../../../api/api';
|
||||
|
||||
type LoginNavProp = NativeStackNavigationProp<RootStackParamList>;
|
||||
|
||||
const useLogin = () => {
|
||||
const navigation = useNavigation<LoginNavProp>();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const toggleShowPassword = () => setShowPassword(prev => !prev);
|
||||
const goToRegister = () => navigation.navigate(RouteName.RegisterNavigation);
|
||||
|
||||
const onLogin = async () => {
|
||||
if (!email.trim()) return Alert.alert('Validasi', 'Email wajib diisi');
|
||||
if (!password.trim()) return Alert.alert('Validasi', 'Password wajib diisi');
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await api.post('/auth/login', { email, password });
|
||||
|
||||
if (res.data.success) {
|
||||
// Cache token di memori dulu — interceptor langsung siap tanpa tunggu disk
|
||||
setToken(res.data.token);
|
||||
|
||||
// Tulis ke AsyncStorage di background, tidak perlu ditunggu sebelum navigate
|
||||
Promise.all([
|
||||
AsyncStorage.setItem('token', res.data.token),
|
||||
AsyncStorage.setItem('user', JSON.stringify(res.data.data)),
|
||||
]).catch(() => {}); // silent — kalau gagal, session tetap jalan via memori
|
||||
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [{ name: RouteName.DashboardNavigation }],
|
||||
});
|
||||
} else {
|
||||
Alert.alert('Gagal', res.data.message);
|
||||
}
|
||||
} catch (err: any) {
|
||||
Alert.alert('Error', err.response?.data?.message || 'Gagal terhubung ke server');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
email, setEmail,
|
||||
password, setPassword,
|
||||
showPassword, toggleShowPassword,
|
||||
loading, onLogin, goToRegister,
|
||||
};
|
||||
};
|
||||
|
||||
export default useLogin;
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import styles from './styles';
|
||||
import useRegister from './useRegister';
|
||||
|
||||
const RegisterScreen = () => {
|
||||
const {
|
||||
name, setName,
|
||||
email, setEmail,
|
||||
password, setPassword,
|
||||
confirmPassword, setConfirmPassword,
|
||||
showPassword, toggleShowPassword,
|
||||
loading, onRegister, goToLogin,
|
||||
} = useRegister();
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.content}
|
||||
keyboardShouldPersistTaps="handled">
|
||||
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<View style={styles.logoBox}>
|
||||
<Icon name="leaf" size={32} color="#1d9e75" />
|
||||
</View>
|
||||
<Text style={styles.logoTitle}>EdaSmart</Text>
|
||||
<Text style={styles.logoSub}>Sistem Manajemen Edamame</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<View>
|
||||
<Text style={styles.cardTitle}>Buat akun baru</Text>
|
||||
<Text style={styles.cardSub}>Daftarkan dirimu sekarang.</Text>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={styles.label}>Nama</Text>
|
||||
<TextInput
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
placeholder="Nama lengkap"
|
||||
placeholderTextColor="#4b5563"
|
||||
autoCapitalize="words"
|
||||
style={styles.input}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={styles.label}>Email</Text>
|
||||
<TextInput
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
placeholder="contoh@email.com"
|
||||
placeholderTextColor="#4b5563"
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
style={styles.input}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={styles.label}>Password</Text>
|
||||
<View style={styles.passwordRow}>
|
||||
<TextInput
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
placeholder="••••••••"
|
||||
placeholderTextColor="#4b5563"
|
||||
secureTextEntry={!showPassword}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
style={styles.passwordInput}
|
||||
/>
|
||||
<Pressable onPress={toggleShowPassword}>
|
||||
<Text style={styles.toggleText}>
|
||||
{showPassword ? 'Hide' : 'Show'}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={styles.label}>Konfirmasi Password</Text>
|
||||
<TextInput
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
placeholder="••••••••"
|
||||
placeholderTextColor="#4b5563"
|
||||
secureTextEntry={!showPassword}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
style={styles.input}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
style={[styles.button, loading && styles.buttonDisabled]}
|
||||
onPress={onRegister}
|
||||
disabled={loading}>
|
||||
{loading
|
||||
? <ActivityIndicator color="#fff" />
|
||||
: <Text style={styles.buttonText}>Register</Text>}
|
||||
</Pressable>
|
||||
|
||||
<View style={styles.dividerRow}>
|
||||
<View style={styles.dividerLine} />
|
||||
<Text style={styles.dividerText}>atau</Text>
|
||||
<View style={styles.dividerLine} />
|
||||
</View>
|
||||
|
||||
<View style={styles.footerRow}>
|
||||
<Text style={styles.footerText}>Sudah punya akun?</Text>
|
||||
<Pressable onPress={goToLogin}>
|
||||
<Text style={styles.footerLink}>Login</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterScreen;
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0f1117',
|
||||
},
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 24,
|
||||
gap: 24,
|
||||
},
|
||||
logoBox: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#1a2a22',
|
||||
borderWidth: 1.5,
|
||||
borderColor: '#1d9e75',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 4,
|
||||
},
|
||||
logoTitle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 24,
|
||||
fontWeight: '700',
|
||||
textAlign: 'center',
|
||||
marginTop: 8,
|
||||
},
|
||||
logoSub: {
|
||||
color: '#6b7280',
|
||||
fontSize: 13,
|
||||
textAlign: 'center',
|
||||
marginTop: 4,
|
||||
},
|
||||
card: {
|
||||
width: '100%',
|
||||
backgroundColor: '#161b24',
|
||||
borderRadius: 16,
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#2a2e3a',
|
||||
padding: 24,
|
||||
gap: 16,
|
||||
},
|
||||
cardTitle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
},
|
||||
cardSub: {
|
||||
color: '#6b7280',
|
||||
fontSize: 13,
|
||||
marginTop: 2,
|
||||
},
|
||||
label: {
|
||||
color: '#9ca3af',
|
||||
fontSize: 13,
|
||||
marginBottom: 6,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: '#0f1117',
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#2a2e3a',
|
||||
borderRadius: 10,
|
||||
padding: 12,
|
||||
color: '#ffffff',
|
||||
fontSize: 14,
|
||||
},
|
||||
passwordRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#0f1117',
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#2a2e3a',
|
||||
borderRadius: 10,
|
||||
},
|
||||
passwordInput: {
|
||||
flex: 1,
|
||||
padding: 12,
|
||||
color: '#ffffff',
|
||||
fontSize: 14,
|
||||
},
|
||||
toggleText: {
|
||||
color: '#6b7280',
|
||||
paddingHorizontal: 14,
|
||||
fontSize: 13,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#1d9e75',
|
||||
borderRadius: 10,
|
||||
padding: 14,
|
||||
alignItems: 'center',
|
||||
marginTop: 4,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: '#0f6e56',
|
||||
},
|
||||
buttonText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
},
|
||||
dividerRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
dividerLine: {
|
||||
flex: 1,
|
||||
height: 0.5,
|
||||
backgroundColor: '#2a2e3a',
|
||||
},
|
||||
dividerText: {
|
||||
color: '#4b5563',
|
||||
fontSize: 12,
|
||||
},
|
||||
footerRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
footerText: {
|
||||
color: '#6b7280',
|
||||
fontSize: 13,
|
||||
},
|
||||
footerLink: {
|
||||
color: '#1d9e75',
|
||||
fontSize: 13,
|
||||
},
|
||||
});
|
||||
|
||||
export default styles;
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { useState } from 'react';
|
||||
import { Alert } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import RouteName from '../../../Constants/RouteName.constants';
|
||||
import { RootStackParamList } from '../../../Constants/RouteParamsList.contants';
|
||||
import api from '../../../api/api';
|
||||
|
||||
type RegisterNavProp = NativeStackNavigationProp<RootStackParamList>;
|
||||
|
||||
const useRegister = () => {
|
||||
const navigation = useNavigation<RegisterNavProp>();
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const toggleShowPassword = () => setShowPassword(prev => !prev);
|
||||
const goToLogin = () => navigation.navigate(RouteName.LoginNavigation);
|
||||
|
||||
const onRegister = async () => {
|
||||
if (!name.trim()) return Alert.alert('Validasi', 'Nama wajib diisi');
|
||||
if (!email.trim()) return Alert.alert('Validasi', 'Email wajib diisi');
|
||||
if (!password.trim()) return Alert.alert('Validasi', 'Password wajib diisi');
|
||||
if (password !== confirmPassword)
|
||||
return Alert.alert('Validasi', 'Konfirmasi password tidak sama');
|
||||
if (password.length < 6)
|
||||
return Alert.alert('Validasi', 'Password minimal 6 karakter');
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await api.post('/auth/register', {
|
||||
nama: name,
|
||||
email,
|
||||
password,
|
||||
konfirmasi_password: confirmPassword,
|
||||
});
|
||||
|
||||
if (res.data.success) {
|
||||
Alert.alert('Berhasil', 'Register sukses. Silakan login.', [
|
||||
{ text: 'OK', onPress: () => navigation.navigate(RouteName.LoginNavigation) },
|
||||
]);
|
||||
} else {
|
||||
Alert.alert('Gagal', res.data.message);
|
||||
}
|
||||
} catch (err: any) {
|
||||
Alert.alert('Error', err.response?.data?.message || 'Gagal terhubung ke server');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
name, setName,
|
||||
email, setEmail,
|
||||
password, setPassword,
|
||||
confirmPassword, setConfirmPassword,
|
||||
showPassword, toggleShowPassword,
|
||||
loading, onRegister, goToLogin,
|
||||
};
|
||||
};
|
||||
|
||||
export default useRegister;
|
||||
|
|
@ -0,0 +1,574 @@
|
|||
import React, { useEffect, useState, memo } from 'react';
|
||||
import {
|
||||
View, Text, ScrollView, Pressable,
|
||||
StatusBar, ActivityIndicator, RefreshControl, Alert,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import styles from './styles';
|
||||
import Navbar from '../../Components/Navbar';
|
||||
import api, { clearToken } from '../../api/api';
|
||||
import { RootStackParamList } from '../../Constants/RouteParamsList.contants';
|
||||
import RouteName from '../../Constants/RouteName.constants';
|
||||
|
||||
interface IMonitoring {
|
||||
id: number;
|
||||
alat_id: number;
|
||||
nama_alat: string;
|
||||
status: string;
|
||||
waktu_aktif: string | null;
|
||||
waktu_nonaktif: string | null;
|
||||
durasi_menit: number;
|
||||
sumber: string;
|
||||
}
|
||||
|
||||
interface IAlat {
|
||||
id: number;
|
||||
nama_alat: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface ISensor {
|
||||
id: number;
|
||||
jarak_cm: number;
|
||||
status: string;
|
||||
waktu: string;
|
||||
}
|
||||
|
||||
interface IRPM {
|
||||
id: number;
|
||||
alat_id: number;
|
||||
rpm: number;
|
||||
status: string;
|
||||
waktu: string;
|
||||
}
|
||||
|
||||
interface IRealtimePress {
|
||||
status: string; // 'ON' | 'OFF'
|
||||
phase: string; // 'TURUN'|'TAHAN'|'NAIK'|'MUNDUR'|'IDLE'
|
||||
sisa: number;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
interface IRealtimeGiling {
|
||||
status: string; // 'ON' | 'OFF'
|
||||
rpm: number;
|
||||
sisa: number;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
interface IRealtimeSensor {
|
||||
jarak_cm: number;
|
||||
status: string;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
interface IRealtimeState {
|
||||
press: IRealtimePress;
|
||||
giling: IRealtimeGiling;
|
||||
estop: { aktif: boolean; updatedAt: string | null };
|
||||
device: { status: string; updatedAt: string | null };
|
||||
sensor: IRealtimeSensor;
|
||||
}
|
||||
|
||||
type DashboardNavProp = NativeStackNavigationProp<RootStackParamList>;
|
||||
|
||||
// Komponen terisolasi untuk timer durasi — hanya komponen ini yang re-render setiap detik,
|
||||
// bukan seluruh Dashboard.
|
||||
interface LiveTimerProps {
|
||||
waktuAktif: string;
|
||||
pad: (n: number) => string;
|
||||
timerStyle: any;
|
||||
unitStyle: any;
|
||||
}
|
||||
const LiveTimer = memo(({ waktuAktif, pad, timerStyle, unitStyle }: LiveTimerProps) => {
|
||||
const [now, setNow] = useState(Date.now());
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
const totalMenit = Math.floor((now - new Date(waktuAktif).getTime()) / 60000);
|
||||
return (
|
||||
<View style={{ flexDirection: 'row', alignItems: 'baseline' }}>
|
||||
<Text style={timerStyle}>{pad(Math.floor(totalMenit / 60))}</Text>
|
||||
<Text style={unitStyle}>jam</Text>
|
||||
<Text style={timerStyle}>{pad(totalMenit % 60)}</Text>
|
||||
<Text style={unitStyle}>mnt</Text>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const Dashboard = () => {
|
||||
const navigation = useNavigation<DashboardNavProp>();
|
||||
const [alatList, setAlatList] = useState<IAlat[]>([]);
|
||||
const [monitoring, setMonitoring] = useState<IMonitoring[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [namaUser, setNamaUser] = useState('');
|
||||
const [sensor, setSensor] = useState<ISensor | null>(null);
|
||||
const [rpmMap, setRpmMap] = useState<Record<number, IRPM>>({});
|
||||
const [realtime, setRealtime] = useState<IRealtimeState | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadUser = async () => {
|
||||
const savedUser = await AsyncStorage.getItem('user');
|
||||
if (savedUser) {
|
||||
const user = JSON.parse(savedUser);
|
||||
setNamaUser(user.nama || '');
|
||||
}
|
||||
};
|
||||
loadUser();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// 1 request ke backend menggantikan 5 request terpisah
|
||||
const [dashRes, realtimeRes] = await Promise.all([
|
||||
api.get('/device/dashboard'),
|
||||
api.get('/sensor/realtime'),
|
||||
]);
|
||||
|
||||
if (dashRes.data.success) {
|
||||
const { alat, monitoring, rpmMap: rpm, sensor: sensorData } = dashRes.data.data;
|
||||
setAlatList(alat);
|
||||
setMonitoring(monitoring);
|
||||
setRpmMap(rpm);
|
||||
setSensor(sensorData);
|
||||
}
|
||||
|
||||
if (realtimeRes.data.success) setRealtime(realtimeRes.data.data);
|
||||
|
||||
} catch (err) {
|
||||
console.log('Dashboard fetch error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch sensor HC-SR04 dari realtime state (polling lebih sering untuk update cepat)
|
||||
const fetchSensor = async () => {
|
||||
try {
|
||||
const res = await api.get('/sensor/realtime');
|
||||
if (res.data.success) setRealtime(res.data.data);
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
const fetchInterval = setInterval(() => { fetchData(); }, 10000);
|
||||
const sensorInterval = setInterval(() => { fetchSensor(); }, 1000);
|
||||
return () => {
|
||||
clearInterval(fetchInterval);
|
||||
clearInterval(sensorInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
Alert.alert('Logout', 'Yakin ingin keluar?', [
|
||||
{ text: 'Batal', style: 'cancel' },
|
||||
{
|
||||
text: 'Keluar', style: 'destructive', onPress: async () => {
|
||||
await AsyncStorage.removeItem('token');
|
||||
await AsyncStorage.removeItem('user');
|
||||
clearToken();
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [{ name: RouteName.LoginNavigation }],
|
||||
});
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
|
||||
const formatWaktu = (waktu: string) => {
|
||||
if (!waktu) return '-';
|
||||
const d = new Date(waktu);
|
||||
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
};
|
||||
|
||||
const formatDurasi = (menit: number) => {
|
||||
const j = Math.floor(menit / 60);
|
||||
const m = menit % 60;
|
||||
if (j > 0) return `${j}j ${pad(m)}m`;
|
||||
return `${m} menit`;
|
||||
};
|
||||
|
||||
const hitungTotalMenit = (alat_id: number): number => {
|
||||
const dataAlat = monitoring.filter(m => m.alat_id === alat_id);
|
||||
const alat = alatList.find(a => a.id === alat_id);
|
||||
const isPengepres = alat?.nama_alat.toLowerCase().includes('pres') ?? false;
|
||||
const rtAlat = isPengepres ? realtime?.press : realtime?.giling;
|
||||
const mesinNyala = rtAlat?.status === 'ON';
|
||||
|
||||
if (!mesinNyala) return 0;
|
||||
|
||||
const sesiAktif = dataAlat
|
||||
.filter(m => m.status === 'aktif' && m.waktu_aktif)
|
||||
.sort((a, b) => new Date(b.waktu_aktif!).getTime() - new Date(a.waktu_aktif!).getTime())[0];
|
||||
|
||||
if (!sesiAktif) return 0;
|
||||
|
||||
// Kembalikan waktuAktif string supaya LiveTimer yang hitung detiknya
|
||||
return new Date(sesiAktif.waktu_aktif!).getTime();
|
||||
};
|
||||
|
||||
// Hitung total menit statis (tidak real-time) — untuk progress bar & stats
|
||||
const hitungTotalMenitStatis = (alat_id: number): number => {
|
||||
const dataAlat = monitoring.filter(m => m.alat_id === alat_id);
|
||||
const alat = alatList.find(a => a.id === alat_id);
|
||||
const isPengepres = alat?.nama_alat.toLowerCase().includes('pres') ?? false;
|
||||
const rtAlat = isPengepres ? realtime?.press : realtime?.giling;
|
||||
const mesinNyala = rtAlat?.status === 'ON';
|
||||
|
||||
if (!mesinNyala) return 0;
|
||||
|
||||
const sesiAktif = dataAlat
|
||||
.filter(m => m.status === 'aktif' && m.waktu_aktif)
|
||||
.sort((a, b) => new Date(b.waktu_aktif!).getTime() - new Date(a.waktu_aktif!).getTime())[0];
|
||||
|
||||
if (!sesiAktif) return 0;
|
||||
return Math.floor((Date.now() - new Date(sesiAktif.waktu_aktif!).getTime()) / 60000);
|
||||
};
|
||||
|
||||
const getWaktuAktif = (alat_id: number): string | null => {
|
||||
const dataAlat = monitoring.filter(m => m.alat_id === alat_id);
|
||||
const alat = alatList.find(a => a.id === alat_id);
|
||||
const isPengepres = alat?.nama_alat.toLowerCase().includes('pres') ?? false;
|
||||
const rtAlat = isPengepres ? realtime?.press : realtime?.giling;
|
||||
if (rtAlat?.status !== 'ON') return null;
|
||||
const sesiAktif = dataAlat
|
||||
.filter(m => m.status === 'aktif' && m.waktu_aktif)
|
||||
.sort((a, b) => new Date(b.waktu_aktif!).getTime() - new Date(a.waktu_aktif!).getTime())[0];
|
||||
return sesiAktif?.waktu_aktif ?? null;
|
||||
};
|
||||
|
||||
const sensorColor = (status: string) => {
|
||||
if (status === 'peringatan') return '#ef9f27';
|
||||
if (status === 'bahaya') return '#e24b4a';
|
||||
return '#1d9e75';
|
||||
};
|
||||
|
||||
const sensorBgColor = (status: string) => {
|
||||
if (status === 'peringatan') return '#2a1f10';
|
||||
if (status === 'bahaya') return '#2a1010';
|
||||
return '#1a2a22';
|
||||
};
|
||||
|
||||
const sensorLabel = (status: string) => {
|
||||
if (status === 'peringatan') return 'Peringatan';
|
||||
if (status === 'bahaya') return 'Bahaya';
|
||||
return 'Normal';
|
||||
};
|
||||
|
||||
const rpmColor = (status: string) => {
|
||||
if (status === 'peringatan') return '#ef9f27';
|
||||
if (status === 'bahaya') return '#e24b4a';
|
||||
return '#ffffff';
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar barStyle="light-content" backgroundColor="#161b24" />
|
||||
<View style={styles.topbar}>
|
||||
<View>
|
||||
<Text style={styles.topbarTitle}>EdaSmart</Text>
|
||||
<Text style={styles.topbarSub}>
|
||||
{namaUser ? `Halo, ${namaUser} 👋` : 'Sistem Manajemen Edamame'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<ActivityIndicator color="#1d9e75" size="large" style={{ flex: 1 }} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||
<StatusBar barStyle="light-content" backgroundColor="#161b24" />
|
||||
|
||||
{/* Topbar */}
|
||||
<View style={styles.topbar}>
|
||||
<View>
|
||||
<Text style={styles.topbarTitle}>EdaSmart</Text>
|
||||
<Text style={styles.topbarSub}>
|
||||
{namaUser ? `Halo, ${namaUser} 👋` : 'Sistem Manajemen Edamame'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ alignItems: 'flex-end', gap: 6 }}>
|
||||
{/* Status device dari MQTT realtime */}
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
|
||||
<View style={[
|
||||
styles.statusDot,
|
||||
{ backgroundColor: realtime?.device.status === 'ONLINE' ? '#1d9e75' : '#6b7280' },
|
||||
]} />
|
||||
<Text style={styles.statusText}>
|
||||
{realtime?.device.status === 'ONLINE' ? 'Online' : 'Offline'}
|
||||
</Text>
|
||||
</View>
|
||||
{/* Badge E-Stop jika aktif */}
|
||||
{realtime?.estop.aktif && (
|
||||
<View style={{
|
||||
backgroundColor: '#2a1010',
|
||||
borderWidth: 1,
|
||||
borderColor: '#e24b4a',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
}}>
|
||||
<Text style={{ color: '#e24b4a', fontSize: 10, fontWeight: '700' }}>
|
||||
🚨 E-STOP AKTIF
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<Pressable
|
||||
onPress={handleLogout}
|
||||
style={{
|
||||
backgroundColor: '#2a1a1a',
|
||||
borderWidth: 1,
|
||||
borderColor: '#c0392b',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
}}>
|
||||
<Text style={{ color: '#e74c3c', fontSize: 11, fontWeight: '600' }}>
|
||||
Logout
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
style={styles.scroll}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={() => { setRefreshing(true); fetchData(); }}
|
||||
tintColor="#1d9e75"
|
||||
/>
|
||||
}>
|
||||
|
||||
{/* Status Alat */}
|
||||
<Text style={styles.sectionLabel}>STATUS ALAT</Text>
|
||||
<View style={styles.card}>
|
||||
{alatList.length === 0 && (
|
||||
<Text style={{ color: '#6b7280', textAlign: 'center' }}>Belum ada alat</Text>
|
||||
)}
|
||||
{alatList.map((alat, index) => {
|
||||
const isPengepres = alat.nama_alat.toLowerCase().includes('pres');
|
||||
const rtAlat = isPengepres ? realtime?.press : realtime?.giling;
|
||||
const alatNyala = rtAlat?.status === 'ON';
|
||||
|
||||
return (
|
||||
<View key={alat.id}>
|
||||
<View style={styles.machineRow}>
|
||||
<View style={styles.machineLeft}>
|
||||
<View style={styles.machineIcon}>
|
||||
<Text style={styles.machineIconText}>
|
||||
{isPengepres ? '🔧' : '⚙️'}
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.machineName}>{alat.nama_alat}</Text>
|
||||
<Text style={alatNyala ? styles.machineStatusOn : styles.machineStatusOff}>
|
||||
{alatNyala ? '● Sedang berjalan' : '● Mati'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[
|
||||
{ paddingHorizontal: 12, paddingVertical: 4, borderRadius: 20 },
|
||||
alatNyala ? { backgroundColor: '#1a2a22' } : { backgroundColor: '#1e1e24' },
|
||||
]}>
|
||||
<Text style={alatNyala ? styles.machineStatusOn : styles.machineStatusOff}>
|
||||
{alatNyala ? 'Aktif' : 'Nonaktif'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
{index < alatList.length - 1 && (
|
||||
<View style={{ height: 0.5, backgroundColor: '#2a2e3a', marginVertical: 10 }} />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* Monitoring Durasi */}
|
||||
<Text style={[styles.sectionLabel, { marginTop: 4 }]}>MONITORING DURASI</Text>
|
||||
{alatList.length === 0 && (
|
||||
<View style={styles.card}>
|
||||
<Text style={{ color: '#6b7280', textAlign: 'center' }}>Belum ada data monitoring</Text>
|
||||
</View>
|
||||
)}
|
||||
{alatList.map(alat => {
|
||||
const dataAlat = monitoring.filter(m => m.alat_id === alat.id);
|
||||
const totalMenit = hitungTotalMenitStatis(alat.id);
|
||||
const waktuAktif = getWaktuAktif(alat.id);
|
||||
const sesiHariIni = dataAlat.length;
|
||||
const progress = Math.min((totalMenit / 480) * 100, 100);
|
||||
const rpmData = rpmMap[alat.id];
|
||||
const isPengepres = alat.nama_alat.toLowerCase().includes('pres');
|
||||
|
||||
// Data realtime dari MQTT — jadikan sumber kebenaran status
|
||||
const rt = isPengepres ? realtime?.press : realtime?.giling;
|
||||
const mesinNyala = rt?.status === 'ON'; // pakai MQTT, bukan DB
|
||||
const rtRpm = realtime?.giling.rpm;
|
||||
const rtPhase = realtime?.press.phase;
|
||||
const rtSisa = rt?.sisa ?? 0;
|
||||
|
||||
return (
|
||||
<View key={alat.id} style={styles.card}>
|
||||
<View style={styles.monitorHeader}>
|
||||
<Text style={styles.monitorTitle}>
|
||||
{isPengepres ? '🔧' : '⚙️'} {alat.nama_alat}
|
||||
</Text>
|
||||
<Text style={mesinNyala ? styles.badgeActive : styles.badgeInactive}>
|
||||
{mesinNyala ? 'Aktif' : 'Tidak aktif'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Banner realtime — hanya tampil saat mesin benar-benar ON */}
|
||||
{isPengepres && mesinNyala && rtPhase && rtPhase !== 'IDLE' && (
|
||||
<View style={{
|
||||
flexDirection: 'row', alignItems: 'center', gap: 6,
|
||||
backgroundColor: '#1a2a22', borderRadius: 8,
|
||||
paddingHorizontal: 10, paddingVertical: 5, marginBottom: 8,
|
||||
}}>
|
||||
<Text style={{ color: '#1d9e75', fontSize: 12, fontWeight: '600' }}>
|
||||
⚡ Fase: {rtPhase}
|
||||
</Text>
|
||||
{rtSisa > 0 && (
|
||||
<Text style={{ color: '#9ca3af', fontSize: 11 }}>
|
||||
· Sisa {rtSisa}s
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
{!isPengepres && mesinNyala && (
|
||||
<View style={{
|
||||
flexDirection: 'row', alignItems: 'center', gap: 6,
|
||||
backgroundColor: '#1a2a22', borderRadius: 8,
|
||||
paddingHorizontal: 10, paddingVertical: 5, marginBottom: 8,
|
||||
}}>
|
||||
<Text style={{ color: '#1d9e75', fontSize: 12, fontWeight: '600' }}>
|
||||
⚡ RPM: {rtRpm ?? '—'}
|
||||
</Text>
|
||||
{rtSisa > 0 && (
|
||||
<Text style={{ color: '#9ca3af', fontSize: 11 }}>
|
||||
· Sisa {rtSisa}s
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Text style={styles.monitorSub}>Total durasi hari ini</Text>
|
||||
{waktuAktif && mesinNyala ? (
|
||||
<LiveTimer
|
||||
waktuAktif={waktuAktif}
|
||||
pad={pad}
|
||||
timerStyle={styles.timerNumber}
|
||||
unitStyle={styles.timerUnit}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.timerRow}>
|
||||
<Text style={styles.timerNumber}>00</Text>
|
||||
<Text style={styles.timerUnit}>jam</Text>
|
||||
<Text style={styles.timerNumber}>00</Text>
|
||||
<Text style={styles.timerUnit}>mnt</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.progressBar}>
|
||||
<View style={[styles.progressFill, { width: `${progress}%` }]} />
|
||||
</View>
|
||||
|
||||
<View style={styles.statsRow}>
|
||||
<View style={styles.statBox}>
|
||||
<Text style={styles.statLabel}>Sesi hari ini</Text>
|
||||
<Text style={styles.statValue}>{sesiHariIni}x</Text>
|
||||
</View>
|
||||
<View style={styles.statBox}>
|
||||
<Text style={styles.statLabel}>Total durasi</Text>
|
||||
<Text style={styles.statValue}>{formatDurasi(totalMenit)}</Text>
|
||||
</View>
|
||||
|
||||
{/* RPM hanya tampil untuk alat penggiling (bukan pengepres) */}
|
||||
{!isPengepres && (
|
||||
<View style={styles.statBox}>
|
||||
<Text style={styles.statLabel}>Kecepatan</Text>
|
||||
<Text style={[
|
||||
styles.statValue,
|
||||
{ color: rpmData ? rpmColor(rpmData.status) : '#6b7280' },
|
||||
]}>
|
||||
{rpmData ? `${rpmData.rpm} RPM` : '— RPM'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.statBox}>
|
||||
<Text style={styles.statLabel}>Status</Text>
|
||||
<Text style={styles.statValue}>
|
||||
{alat.status === 'aktif' ? '🟢 On' : '⚫ Off'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Sensor Jarak HC-SR04 */}
|
||||
<Text style={[styles.sectionLabel, { marginTop: 4 }]}>SENSOR JARAK HC-SR04</Text>
|
||||
<View style={styles.card}>
|
||||
{(() => {
|
||||
// Prioritas: realtime.sensor (MQTT langsung) → sensor (DB fallback)
|
||||
const rtSensor = realtime?.sensor;
|
||||
const hasRt = rtSensor && rtSensor.updatedAt !== null;
|
||||
const jarakCm = hasRt ? rtSensor!.jarak_cm : sensor?.jarak_cm ?? null;
|
||||
const status = hasRt ? rtSensor!.status : sensor?.status ?? 'normal';
|
||||
const waktu = hasRt ? rtSensor!.updatedAt : sensor?.waktu ?? null;
|
||||
|
||||
if (jarakCm === null) return (
|
||||
<Text style={{ color: '#6b7280', textAlign: 'center' }}>
|
||||
Belum ada data sensor
|
||||
</Text>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
|
||||
<Text style={{ color: '#9ca3af', fontSize: 12 }}>Jarak terdeteksi</Text>
|
||||
<View style={{
|
||||
paddingHorizontal: 10, paddingVertical: 3, borderRadius: 20,
|
||||
backgroundColor: sensorBgColor(status),
|
||||
borderWidth: 0.5, borderColor: sensorColor(status),
|
||||
}}>
|
||||
<Text style={{ fontSize: 10, fontWeight: '600', color: sensorColor(status) }}>
|
||||
{sensorLabel(status)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'baseline', gap: 6 }}>
|
||||
<Text style={{ fontSize: 48, fontWeight: '500', color: sensorColor(status), lineHeight: 56 }}>
|
||||
{jarakCm}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 18, color: '#6b7280' }}>cm</Text>
|
||||
</View>
|
||||
<Text style={{ fontSize: 11, color: '#6b7280', marginTop: 6 }}>
|
||||
Diperbarui: {waktu ? formatWaktu(waktu) : '-'}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<Navbar activeScreen="Dashboard" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export default StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0f1117',
|
||||
},
|
||||
topbar: {
|
||||
backgroundColor: '#161b24',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 14,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: '#2a2e3a',
|
||||
},
|
||||
topbarTitle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 18,
|
||||
fontWeight: '500',
|
||||
},
|
||||
topbarSub: {
|
||||
color: '#6b7280',
|
||||
fontSize: 12,
|
||||
marginTop: 2,
|
||||
},
|
||||
statusDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: '#1d9e75',
|
||||
marginBottom: 2,
|
||||
},
|
||||
statusText: {
|
||||
color: '#1d9e75',
|
||||
fontSize: 12,
|
||||
textAlign: 'right',
|
||||
},
|
||||
statusIp: {
|
||||
color: '#4b5563',
|
||||
fontSize: 11,
|
||||
textAlign: 'right',
|
||||
},
|
||||
scroll: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: 16,
|
||||
gap: 16,
|
||||
},
|
||||
sectionLabel: {
|
||||
color: '#6b7280',
|
||||
fontSize: 11,
|
||||
fontWeight: '500',
|
||||
letterSpacing: 1,
|
||||
marginBottom: 8,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#161b24',
|
||||
borderRadius: 14,
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#2a2e3a',
|
||||
padding: 16,
|
||||
},
|
||||
machineRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
machineLeft: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
machineIcon: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 10,
|
||||
backgroundColor: '#1a2a22',
|
||||
borderWidth: 1,
|
||||
borderColor: '#2a2e3a',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
machineIconText: {
|
||||
fontSize: 20,
|
||||
},
|
||||
machineName: {
|
||||
color: '#ffffff',
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
},
|
||||
machineStatusOn: {
|
||||
color: '#1d9e75',
|
||||
fontSize: 12,
|
||||
marginTop: 2,
|
||||
},
|
||||
machineStatusOff: {
|
||||
color: '#6b7280',
|
||||
fontSize: 12,
|
||||
marginTop: 2,
|
||||
},
|
||||
toggle: {
|
||||
width: 48,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 3,
|
||||
},
|
||||
toggleOn: {
|
||||
backgroundColor: '#1d9e75',
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
toggleOff: {
|
||||
backgroundColor: '#2a2e3a',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
toggleThumb: {
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
monitorHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 8,
|
||||
},
|
||||
monitorTitle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
},
|
||||
badge: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 20,
|
||||
fontSize: 12,
|
||||
},
|
||||
badgeActive: {
|
||||
backgroundColor: '#1a2a22',
|
||||
color: '#1d9e75',
|
||||
},
|
||||
badgeInactive: {
|
||||
backgroundColor: '#1e1e24',
|
||||
color: '#6b7280',
|
||||
},
|
||||
monitorSub: {
|
||||
color: '#6b7280',
|
||||
fontSize: 12,
|
||||
marginBottom: 6,
|
||||
},
|
||||
timerRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
gap: 4,
|
||||
marginBottom: 10,
|
||||
},
|
||||
timerNumber: {
|
||||
color: '#ffffff',
|
||||
fontSize: 36,
|
||||
fontWeight: '300',
|
||||
},
|
||||
timerUnit: {
|
||||
color: '#6b7280',
|
||||
fontSize: 14,
|
||||
},
|
||||
progressBar: {
|
||||
height: 3,
|
||||
backgroundColor: '#2a2e3a',
|
||||
borderRadius: 2,
|
||||
marginBottom: 14,
|
||||
},
|
||||
progressFill: {
|
||||
height: 3,
|
||||
backgroundColor: '#1d9e75',
|
||||
borderRadius: 2,
|
||||
},
|
||||
statsRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
},
|
||||
statBox: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0f1117',
|
||||
borderRadius: 10,
|
||||
padding: 10,
|
||||
},
|
||||
statLabel: {
|
||||
color: '#6b7280',
|
||||
fontSize: 11,
|
||||
marginBottom: 4,
|
||||
},
|
||||
statValue: {
|
||||
color: '#ffffff',
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
scheduleItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: '#2a2e3a',
|
||||
},
|
||||
scheduleTime: {
|
||||
color: '#ffffff',
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
},
|
||||
scheduleDesc: {
|
||||
color: '#6b7280',
|
||||
fontSize: 12,
|
||||
marginTop: 2,
|
||||
},
|
||||
scheduleBadgeActive: {
|
||||
backgroundColor: '#1a2a22',
|
||||
color: '#1d9e75',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 20,
|
||||
fontSize: 12,
|
||||
},
|
||||
scheduleBadgeWaiting: {
|
||||
backgroundColor: '#1e1e24',
|
||||
color: '#6b7280',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 20,
|
||||
fontSize: 12,
|
||||
},
|
||||
navbar: {
|
||||
backgroundColor: '#161b24',
|
||||
borderTopWidth: 0.5,
|
||||
borderTopColor: '#2a2e3a',
|
||||
flexDirection: 'row',
|
||||
paddingTop: 10,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
navItem: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
navIcon: {
|
||||
fontSize: 22,
|
||||
},
|
||||
navLabel: {
|
||||
fontSize: 11,
|
||||
},
|
||||
navActive: {
|
||||
color: '#1d9e75',
|
||||
},
|
||||
navInactive: {
|
||||
color: '#4b5563',
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
View, Text, ScrollView, Pressable,
|
||||
StatusBar, ActivityIndicator, RefreshControl, Alert,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import styles from './styles';
|
||||
import Navbar from '../../Components/Navbar';
|
||||
import api from '../../api/api';
|
||||
|
||||
interface IRiwayat {
|
||||
id: number;
|
||||
alat_id: number;
|
||||
user_id: number;
|
||||
nama_alat: string;
|
||||
nama_user: string;
|
||||
aksi: string;
|
||||
keterangan: string;
|
||||
waktu: string;
|
||||
}
|
||||
|
||||
const FILTER_LIST = ['Semua', 'aktifkan', 'nonaktifkan'];
|
||||
const FILTER_LABEL: Record<string, string> = {
|
||||
'Semua': 'Semua',
|
||||
'aktifkan': 'Nyalakan',
|
||||
'nonaktifkan': 'Matikan',
|
||||
};
|
||||
|
||||
const HistoryScreen = () => {
|
||||
const [riwayat, setRiwayat] = useState<IRiwayat[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [activeFilter, setActiveFilter] = useState('Semua');
|
||||
|
||||
const fetchRiwayat = useCallback(async () => {
|
||||
try {
|
||||
const res = await api.get('/device/riwayat');
|
||||
if (res.data.success) setRiwayat(res.data.data);
|
||||
} catch (err) {
|
||||
console.log('Riwayat fetch error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { fetchRiwayat(); }, []);
|
||||
|
||||
// Hapus satu riwayat
|
||||
const hapusSatu = (id: number) => {
|
||||
Alert.alert('Hapus Riwayat', 'Yakin ingin menghapus riwayat ini?', [
|
||||
{ text: 'Batal', style: 'cancel' },
|
||||
{
|
||||
text: 'Hapus', style: 'destructive', onPress: async () => {
|
||||
try {
|
||||
await api.delete(`/device/riwayat/${id}`);
|
||||
setRiwayat(prev => prev.filter(r => r.id !== id));
|
||||
} catch (err) {
|
||||
Alert.alert('Error', 'Gagal menghapus riwayat');
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// Hapus semua riwayat
|
||||
const mengedit = () => {
|
||||
Alert.alert('Mengedit Semua Riwayat', 'Yakin ingin mengedit semua riwayat? Tindakan ini tidak bisa dibatalkan.', [
|
||||
{ text: 'Batal', style: 'cancel' },
|
||||
{
|
||||
text: 'edit Semua', style: 'destructive', onPress: async () => {
|
||||
try {
|
||||
await api.delete('/device/riwayat');
|
||||
setRiwayat([]);
|
||||
} catch (err) {
|
||||
Alert.alert('Error', 'Gagal mengedit semua riwayat');
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const filtered = activeFilter === 'Semua'
|
||||
? riwayat
|
||||
: riwayat.filter(r => r.aksi === activeFilter);
|
||||
|
||||
const grouped = filtered.reduce((acc, item) => {
|
||||
const tanggal = formatTanggal(item.waktu);
|
||||
if (!acc[tanggal]) acc[tanggal] = [];
|
||||
acc[tanggal].push(item);
|
||||
return acc;
|
||||
}, {} as Record<string, IRiwayat[]>);
|
||||
|
||||
function formatTanggal(waktu: string): string {
|
||||
const d = new Date(waktu);
|
||||
const today = new Date();
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
if (d.toDateString() === today.toDateString()) return 'Hari ini';
|
||||
if (d.toDateString() === yesterday.toDateString()) return 'Kemarin';
|
||||
return d.toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' });
|
||||
}
|
||||
|
||||
function formatJam(waktu: string): string {
|
||||
const d = new Date(waktu);
|
||||
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
const getIcon = (nama_alat: string) =>
|
||||
nama_alat.toLowerCase().includes('pres') ? '🔧' : '⚙️';
|
||||
|
||||
const getAksiLabel = (aksi: string) => {
|
||||
if (aksi === 'aktifkan') return 'Dinyalakan';
|
||||
if (aksi === 'nonaktifkan') return 'Dimatikan';
|
||||
return aksi;
|
||||
};
|
||||
|
||||
const getBadgeStyle = (aksi: string) => {
|
||||
if (aksi === 'aktifkan') return styles.badgeSelesai;
|
||||
if (aksi === 'nonaktifkan') return styles.badgeDibatalkan;
|
||||
return styles.badgeTerlewat;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<ActivityIndicator color="#1d9e75" size="large" style={{ flex: 1 }} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||
<StatusBar barStyle="light-content" backgroundColor="#161b24" />
|
||||
|
||||
{/* Topbar */}
|
||||
<View style={styles.topbar}>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<View>
|
||||
<Text style={styles.topbarTitle}>Riwayat</Text>
|
||||
<Text style={styles.topbarSub}>Histori aksi mesin</Text>
|
||||
</View>
|
||||
{riwayat.length > 0 && (
|
||||
<Pressable
|
||||
onPress={mengedit}
|
||||
style={{
|
||||
backgroundColor: '#2a1a1a',
|
||||
borderWidth: 1,
|
||||
borderColor: '#c0392b',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
}}>
|
||||
<Text style={{ color: '#e74c3c', fontSize: 12, fontWeight: '600' }}>
|
||||
🗑 edit Semua
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Filter */}
|
||||
<View style={styles.filterRow}>
|
||||
{FILTER_LIST.map(f => (
|
||||
<Pressable
|
||||
key={f}
|
||||
style={[styles.filterBtn, activeFilter === f && styles.filterBtnActive]}
|
||||
onPress={() => setActiveFilter(f)}>
|
||||
<Text style={[styles.filterText, activeFilter === f && styles.filterTextActive]}>
|
||||
{FILTER_LABEL[f]}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
style={styles.scroll}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={() => { setRefreshing(true); fetchRiwayat(); }}
|
||||
tintColor="#1d9e75"
|
||||
/>
|
||||
}>
|
||||
|
||||
{Object.keys(grouped).length === 0 ? (
|
||||
<View style={styles.emptyBox}>
|
||||
<Text style={styles.emptyIcon}>📋</Text>
|
||||
<Text style={styles.emptyText}>Tidak ada riwayat</Text>
|
||||
</View>
|
||||
) : (
|
||||
Object.entries(grouped).map(([tanggal, items]) => (
|
||||
<View key={tanggal}>
|
||||
<Text style={styles.dateLabel}>{tanggal}</Text>
|
||||
<View style={styles.card}>
|
||||
{items.map((item, index) => (
|
||||
<View
|
||||
key={item.id}
|
||||
style={[
|
||||
styles.riwayatItem,
|
||||
index === items.length - 1 && { borderBottomWidth: 0 },
|
||||
]}>
|
||||
<View style={styles.riwayatLeft}>
|
||||
<View style={styles.mesinIcon}>
|
||||
<Text style={styles.mesinIconText}>{getIcon(item.nama_alat)}</Text>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.riwayatMesin}>{item.nama_alat}</Text>
|
||||
<Text style={styles.riwayatLabel}>{item.keterangan}</Text>
|
||||
<View style={styles.riwayatMeta}>
|
||||
<Text style={styles.riwayatJam}>🕐 {formatJam(item.waktu)}</Text>
|
||||
{item.nama_user && (
|
||||
<Text style={styles.riwayatDurasi}>👤 {item.nama_user}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ alignItems: 'flex-end', gap: 6 }}>
|
||||
<Text style={getBadgeStyle(item.aksi)}>
|
||||
{getAksiLabel(item.aksi)}
|
||||
</Text>
|
||||
<Pressable onPress={() => hapusSatu(item.id)}>
|
||||
<Text style={{ color: '#e74c3c', fontSize: 11 }}>🗑 Hapus</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
<Navbar activeScreen="Riwayat" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default HistoryScreen;
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export default StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: '#0f1117' },
|
||||
topbar: {
|
||||
backgroundColor: '#161b24',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 14,
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: '#2a2e3a',
|
||||
},
|
||||
topbarTitle: { color: '#ffffff', fontSize: 18, fontWeight: '500' },
|
||||
topbarSub: { color: '#6b7280', fontSize: 12, marginTop: 2 },
|
||||
filterRow: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
gap: 8,
|
||||
backgroundColor: '#0f1117',
|
||||
},
|
||||
filterBtn: {
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#161b24',
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#2a2e3a',
|
||||
},
|
||||
filterBtnActive: {
|
||||
backgroundColor: '#1a2a22',
|
||||
borderColor: '#1d9e75',
|
||||
},
|
||||
filterText: { color: '#6b7280', fontSize: 12 },
|
||||
filterTextActive: { color: '#1d9e75', fontSize: 12 },
|
||||
scroll: { flex: 1 },
|
||||
scrollContent: { padding: 16, gap: 12 },
|
||||
dateLabel: {
|
||||
color: '#6b7280',
|
||||
fontSize: 11,
|
||||
fontWeight: '500',
|
||||
letterSpacing: 0.8,
|
||||
marginBottom: 8,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#161b24',
|
||||
borderRadius: 12,
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#2a2e3a',
|
||||
marginBottom: 8,
|
||||
},
|
||||
riwayatItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: 14,
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: '#2a2e3a',
|
||||
},
|
||||
riwayatLeft: { flexDirection: 'row', alignItems: 'center', gap: 12, flex: 1 },
|
||||
mesinIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
backgroundColor: '#1a2a22',
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#2a2e3a',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
mesinIconText: { fontSize: 18 },
|
||||
riwayatMesin: { color: '#ffffff', fontSize: 13, fontWeight: '500' },
|
||||
riwayatLabel: { color: '#6b7280', fontSize: 12, marginTop: 1 },
|
||||
riwayatMeta: { flexDirection: 'row', gap: 10, marginTop: 4 },
|
||||
riwayatJam: { color: '#4b5563', fontSize: 11 },
|
||||
riwayatDurasi: { color: '#4b5563', fontSize: 11 },
|
||||
badgeSelesai: {
|
||||
backgroundColor: '#1a2a22',
|
||||
color: '#1d9e75',
|
||||
fontSize: 11,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 20,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
badgeTerlewat: {
|
||||
backgroundColor: '#2a1f1a',
|
||||
color: '#f97316',
|
||||
fontSize: 11,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 20,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
badgeDibatalkan: {
|
||||
backgroundColor: '#1e1e24',
|
||||
color: '#6b7280',
|
||||
fontSize: 11,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 20,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
emptyBox: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 60,
|
||||
gap: 12,
|
||||
},
|
||||
emptyIcon: { fontSize: 40 },
|
||||
emptyText: { color: '#4b5563', fontSize: 14 },
|
||||
navbar: {
|
||||
backgroundColor: '#161b24',
|
||||
borderTopWidth: 0.5,
|
||||
borderTopColor: '#2a2e3a',
|
||||
flexDirection: 'row',
|
||||
paddingTop: 10,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
navItem: { flex: 1, alignItems: 'center', gap: 4 },
|
||||
navIcon: { fontSize: 22 },
|
||||
navLabel: { fontSize: 11 },
|
||||
navActive: { color: '#1d9e75' },
|
||||
navInactive: { color: '#4b5563' },
|
||||
});
|
||||
|
|
@ -0,0 +1,717 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
View, Text, ScrollView, Pressable,
|
||||
StatusBar, Modal, TextInput, Alert, ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import styles from './styles';
|
||||
import Navbar from '../../Components/Navbar';
|
||||
import api from '../../api/api';
|
||||
|
||||
// Langkah slider kecepatan: 0, 5, 10, ... 100
|
||||
const SPEED_STEPS = Array.from({ length: 21 }, (_, i) => i * 5); // [0, 5, 10, ..., 100]
|
||||
const SPEED_STORAGE_KEY = 'edasmart_kecepatan_giling';
|
||||
|
||||
interface IMesin {
|
||||
id: string;
|
||||
nama: string;
|
||||
icon: string;
|
||||
on: boolean;
|
||||
seconds: number;
|
||||
timerAktif: boolean;
|
||||
timerTarget: number | null;
|
||||
waktuMulai: number | null; // timestamp ms
|
||||
}
|
||||
|
||||
interface IRealtimePress {
|
||||
status: string; // 'ON' | 'OFF'
|
||||
phase: string;
|
||||
sisa: number;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
interface IRealtimeGiling {
|
||||
status: string; // 'ON' | 'OFF'
|
||||
rpm: number;
|
||||
sisa: number;
|
||||
kecepatan?: number; // persen 0-100, dikirim ESP32 saat ubah dari LCD
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
interface IRealtimeState {
|
||||
press: IRealtimePress;
|
||||
giling: IRealtimeGiling;
|
||||
estop: { aktif: boolean; updatedAt: string | null };
|
||||
device: { status: string; updatedAt: string | null };
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'edasmart_mesin_state';
|
||||
|
||||
const ScheduleScreen = () => {
|
||||
const [mesinList, setMesinList] = useState<IMesin[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [modalTimerId, setModalTimerId] = useState<string | null>(null);
|
||||
const [inputJam, setInputJam] = useState('');
|
||||
const [inputMenit, setInputMenit] = useState('');
|
||||
const [realtime, setRealtime] = useState<IRealtimeState | null>(null);
|
||||
const [kecepatanGiling, setKecepatanGiling] = useState(100); // persen, default 100%
|
||||
const intervalRefs = useRef<Record<string, ReturnType<typeof setInterval>>>({});
|
||||
|
||||
// Load kecepatan tersimpan
|
||||
useEffect(() => {
|
||||
AsyncStorage.getItem(SPEED_STORAGE_KEY).then(val => {
|
||||
if (val !== null) setKecepatanGiling(parseInt(val));
|
||||
});
|
||||
}, []);
|
||||
|
||||
// ── Ambil data alat dari backend ──────────────────────────────────────────
|
||||
const fetchAlat = async () => {
|
||||
try {
|
||||
const res = await api.get('/device/alat');
|
||||
if (res.data.success) {
|
||||
const savedRaw = await AsyncStorage.getItem(STORAGE_KEY);
|
||||
const savedState: Record<string, any> = savedRaw ? JSON.parse(savedRaw) : {};
|
||||
const now = Date.now();
|
||||
|
||||
const data: IMesin[] = res.data.data.map((alat: any) => {
|
||||
const saved = savedState[String(alat.id)];
|
||||
const isOn = alat.status === 'aktif';
|
||||
|
||||
let seconds = 0;
|
||||
let timerAktif = false;
|
||||
let timerTarget: number | null = null;
|
||||
let waktuMulai: number | null = null;
|
||||
|
||||
if (isOn && saved?.waktuMulai) {
|
||||
waktuMulai = saved.waktuMulai;
|
||||
seconds = Math.floor((now - saved.waktuMulai) / 1000);
|
||||
timerAktif = saved.timerAktif || false;
|
||||
timerTarget = saved.timerTarget || null;
|
||||
|
||||
if (timerTarget !== null && seconds >= timerTarget) {
|
||||
seconds = 0;
|
||||
timerAktif = false;
|
||||
timerTarget = null;
|
||||
waktuMulai = null;
|
||||
kirimKontrol(String(alat.id), 'nonaktif');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: String(alat.id),
|
||||
nama: alat.nama_alat,
|
||||
icon: alat.nama_alat.toLowerCase().includes('pres') ? '🔧' : '⚙️',
|
||||
on: isOn && waktuMulai !== null ? true : isOn,
|
||||
seconds,
|
||||
timerAktif,
|
||||
timerTarget,
|
||||
waktuMulai,
|
||||
};
|
||||
});
|
||||
setMesinList(data);
|
||||
}
|
||||
} catch (err) {
|
||||
Alert.alert('Error', 'Gagal memuat data alat');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Ambil status realtime dari MQTT via backend ───────────────────────────
|
||||
const fetchRealtime = async () => {
|
||||
try {
|
||||
const res = await api.get('/sensor/realtime');
|
||||
if (res.data.success) {
|
||||
const rt: IRealtimeState = res.data.data;
|
||||
setRealtime(rt);
|
||||
|
||||
// Sync kecepatan dari ESP32 ke slider app hanya saat mesin penggiling ON
|
||||
// Kalau mesin OFF, biarkan nilai slider milik user (untuk sesi berikutnya)
|
||||
if (rt.giling.kecepatan !== undefined && rt.giling.status === 'ON') {
|
||||
// Bulatkan ke kelipatan 5 terdekat supaya cocok dengan SPEED_STEPS
|
||||
const rounded = Math.round(rt.giling.kecepatan / 5) * 5;
|
||||
const clamped = Math.min(100, Math.max(0, rounded));
|
||||
setKecepatanGiling(prev => {
|
||||
if (prev !== clamped) {
|
||||
AsyncStorage.setItem(SPEED_STORAGE_KEY, String(clamped));
|
||||
return clamped;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
|
||||
// Sinkronkan status mesin di list dengan status MQTT
|
||||
setMesinList(prev => prev.map(m => {
|
||||
const isPengepres = m.nama.toLowerCase().includes('pres');
|
||||
const rtAlat = isPengepres ? rt.press : rt.giling;
|
||||
const nyalaRt = rtAlat.status === 'ON';
|
||||
|
||||
// Kalau MQTT bilang OFF tapi app masih ON → sinkronkan
|
||||
if (!nyalaRt && m.on) {
|
||||
if (intervalRefs.current[m.id]) {
|
||||
clearInterval(intervalRefs.current[m.id]);
|
||||
delete intervalRefs.current[m.id];
|
||||
}
|
||||
return { ...m, on: false, seconds: 0, timerAktif: false, timerTarget: null, waktuMulai: null };
|
||||
}
|
||||
|
||||
// Kalau MQTT bilang ON tapi app masih OFF → sinkronkan
|
||||
if (nyalaRt && !m.on) {
|
||||
const now = Date.now();
|
||||
return { ...m, on: true, waktuMulai: m.waktuMulai ?? now };
|
||||
}
|
||||
|
||||
return m;
|
||||
}));
|
||||
}
|
||||
} catch (_) {
|
||||
// silent — jangan ganggu UI kalau gagal
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAlat();
|
||||
}, []);
|
||||
|
||||
// Polling realtime tiap 3 detik
|
||||
useEffect(() => {
|
||||
fetchRealtime();
|
||||
const interval = setInterval(fetchRealtime, 3000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// ── Simpan state ke AsyncStorage ─────────────────────────────────────────
|
||||
const simpanState = async (list: IMesin[]) => {
|
||||
const state: Record<string, any> = {};
|
||||
list.forEach(m => {
|
||||
state[m.id] = {
|
||||
waktuMulai: m.waktuMulai,
|
||||
timerAktif: m.timerAktif,
|
||||
timerTarget: m.timerTarget,
|
||||
};
|
||||
});
|
||||
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
};
|
||||
|
||||
// ── Kirim perintah ke backend (backend sudah publish MQTT, tidak perlu double) ──
|
||||
const kirimKontrol = async (id: string, perintah: 'aktif' | 'nonaktif', durasiMenit?: number, kecepatan?: number) => {
|
||||
try {
|
||||
await api.post('/device/kontrol', {
|
||||
alat_id: id,
|
||||
perintah,
|
||||
durasi_menit: durasiMenit,
|
||||
kecepatan_persen: kecepatan,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('Kontrol error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Timer countdown ───────────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
mesinList.forEach(mesin => {
|
||||
if (mesin.on && mesin.waktuMulai) {
|
||||
if (!intervalRefs.current[mesin.id]) {
|
||||
intervalRefs.current[mesin.id] = setInterval(() => {
|
||||
setMesinList(prev => {
|
||||
const updated = prev.map(m => {
|
||||
if (m.id !== mesin.id || !m.waktuMulai) return m;
|
||||
const newSeconds = Math.floor((Date.now() - m.waktuMulai) / 1000);
|
||||
if (m.timerTarget !== null && newSeconds >= m.timerTarget) {
|
||||
clearInterval(intervalRefs.current[m.id]);
|
||||
delete intervalRefs.current[m.id];
|
||||
kirimKontrol(m.id, 'nonaktif');
|
||||
Alert.alert('Timer Selesai', `${m.nama} otomatis dimatikan`);
|
||||
// Simpan state hanya saat timer selesai (perubahan bermakna)
|
||||
const selesai = prev.map(x =>
|
||||
x.id === m.id ? { ...x, seconds: 0, on: false, timerAktif: false, timerTarget: null, waktuMulai: null } : x
|
||||
);
|
||||
simpanState(selesai);
|
||||
return { ...m, seconds: 0, on: false, timerAktif: false, timerTarget: null, waktuMulai: null };
|
||||
}
|
||||
// Hanya update seconds — TIDAK tulis AsyncStorage setiap detik
|
||||
return { ...m, seconds: newSeconds };
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
} else {
|
||||
if (intervalRefs.current[mesin.id]) {
|
||||
clearInterval(intervalRefs.current[mesin.id]);
|
||||
delete intervalRefs.current[mesin.id];
|
||||
}
|
||||
}
|
||||
});
|
||||
return () => { Object.values(intervalRefs.current).forEach(clearInterval); };
|
||||
}, [mesinList.map(m => `${m.id}:${m.on}`).join(',')]);
|
||||
|
||||
const toggleMesin = (id: string) => {
|
||||
const now = Date.now();
|
||||
setMesinList(prev => {
|
||||
const updated = prev.map(m => {
|
||||
if (m.id !== id) return m;
|
||||
const isPengepres = m.nama.toLowerCase().includes('pres');
|
||||
if (m.on) {
|
||||
if (intervalRefs.current[id]) {
|
||||
clearInterval(intervalRefs.current[id]);
|
||||
delete intervalRefs.current[id];
|
||||
}
|
||||
kirimKontrol(id, 'nonaktif');
|
||||
return { ...m, on: false, seconds: 0, timerAktif: false, timerTarget: null, waktuMulai: null };
|
||||
}
|
||||
// Kirim kecepatan hanya untuk penggiling
|
||||
kirimKontrol(id, 'aktif', undefined, isPengepres ? undefined : kecepatanGiling);
|
||||
return { ...m, on: true, waktuMulai: now };
|
||||
});
|
||||
simpanState(updated);
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const bukaModalTimer = (id: string) => {
|
||||
setModalTimerId(id);
|
||||
setInputJam('');
|
||||
setInputMenit('');
|
||||
};
|
||||
|
||||
const simpanTimer = () => {
|
||||
if (!modalTimerId) return;
|
||||
const jam = parseInt(inputJam || '0', 10);
|
||||
const menit = parseInt(inputMenit || '0', 10);
|
||||
const totalDetik = (jam * 3600) + (menit * 60);
|
||||
if (totalDetik <= 0) {
|
||||
Alert.alert('Error', 'Masukkan durasi yang valid');
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
setMesinList(prev => {
|
||||
const updated = prev.map(m =>
|
||||
m.id === modalTimerId
|
||||
? { ...m, timerAktif: true, timerTarget: totalDetik, seconds: 0, on: true, waktuMulai: now }
|
||||
: m
|
||||
);
|
||||
simpanState(updated);
|
||||
return updated;
|
||||
});
|
||||
kirimKontrol(modalTimerId, 'aktif', menit + jam * 60);
|
||||
setModalTimerId(null);
|
||||
};
|
||||
|
||||
const hapusTimer = (id: string) => {
|
||||
setMesinList(prev => {
|
||||
const updated = prev.map(m =>
|
||||
m.id === id ? { ...m, timerAktif: false, timerTarget: null } : m
|
||||
);
|
||||
simpanState(updated);
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
const formatDurasi = (seconds: number) => {
|
||||
const j = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = seconds % 60;
|
||||
return j > 0 ? `${pad(j)}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
||||
};
|
||||
const formatSisa = (seconds: number, target: number) => {
|
||||
const sisa = Math.max(target - seconds, 0);
|
||||
const j = Math.floor(sisa / 3600);
|
||||
const m = Math.floor((sisa % 3600) / 60);
|
||||
const s = sisa % 60;
|
||||
return j > 0 ? `${pad(j)}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar barStyle="light-content" backgroundColor="#161b24" />
|
||||
<View style={styles.topbar}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.topbarTitle}>Kontrol Manual</Text>
|
||||
<Text style={styles.topbarSub}>Kendalikan mesin secara langsung</Text>
|
||||
</View>
|
||||
</View>
|
||||
<ActivityIndicator color="#2ecc71" size="large" style={{ flex: 1 }} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||
<StatusBar barStyle="light-content" backgroundColor="#161b24" />
|
||||
|
||||
<View style={styles.topbar}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.topbarTitle}>Kontrol Manual</Text>
|
||||
<Text style={styles.topbarSub}>Kendalikan mesin secara langsung</Text>
|
||||
</View>
|
||||
{/* Status device dari MQTT */}
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
||||
<View style={{
|
||||
width: 8, height: 8, borderRadius: 4,
|
||||
backgroundColor: realtime?.device.status === 'ONLINE' ? '#1d9e75' : '#6b7280',
|
||||
}} />
|
||||
<Text style={{ color: realtime?.device.status === 'ONLINE' ? '#1d9e75' : '#6b7280', fontSize: 11 }}>
|
||||
{realtime?.device.status === 'ONLINE' ? 'Online' : 'Offline'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Banner E-Stop */}
|
||||
{realtime?.estop.aktif && (
|
||||
<View style={{
|
||||
backgroundColor: '#2a1010', borderBottomWidth: 1, borderBottomColor: '#e24b4a',
|
||||
paddingHorizontal: 16, paddingVertical: 8,
|
||||
flexDirection: 'row', alignItems: 'center', gap: 8,
|
||||
}}>
|
||||
<Text style={{ color: '#e24b4a', fontWeight: '700', fontSize: 13 }}>
|
||||
🚨 EMERGENCY STOP AKTIF — semua kontrol dinonaktifkan
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<ScrollView style={styles.scroll} contentContainerStyle={styles.scrollContent}>
|
||||
<Text style={styles.sectionLabel}>STATUS MESIN</Text>
|
||||
|
||||
{mesinList.length === 0 && (
|
||||
<Text style={{ color: '#888', textAlign: 'center', marginTop: 40 }}>
|
||||
Belum ada alat terdaftar
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{mesinList.map(mesin => {
|
||||
const isPengepres = mesin.nama.toLowerCase().includes('pres');
|
||||
const rtAlat = isPengepres ? realtime?.press : realtime?.giling;
|
||||
// mesinNyala dari MQTT = sumber kebenaran status teks & banner
|
||||
// mesin.on = optimistic state lokal, dipakai untuk posisi visual toggle supaya
|
||||
// langsung berubah saat diklik tanpa nunggu polling 3 detik
|
||||
const mesinNyala = rtAlat?.status === 'ON';
|
||||
const toggleNyala = mesin.on; // optimistic — langsung update saat toggle diklik
|
||||
const progress = mesin.timerTarget
|
||||
? Math.min((mesin.seconds / mesin.timerTarget) * 100, 100)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<View key={mesin.id} style={styles.mesinCard}>
|
||||
<View style={styles.mesinHeader}>
|
||||
<View style={styles.mesinIconBox}>
|
||||
<Text style={styles.mesinIconText}>{mesin.icon}</Text>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.mesinNama}>{mesin.nama}</Text>
|
||||
<Text style={mesinNyala ? styles.statusOn : styles.statusOff}>
|
||||
{mesinNyala ? '● Sedang berjalan' : '● Mati'}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
disabled={realtime?.estop.aktif === true}
|
||||
style={[styles.toggle, toggleNyala ? styles.toggleOn : styles.toggleOff,
|
||||
realtime?.estop.aktif && { opacity: 0.4 },
|
||||
]}
|
||||
onPress={() => toggleMesin(mesin.id)}>
|
||||
<View style={styles.toggleThumb} />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* Banner fase press realtime */}
|
||||
{isPengepres && mesinNyala && realtime?.press.phase && realtime.press.phase !== 'IDLE' && (
|
||||
<View style={{
|
||||
backgroundColor: '#1a2a22', borderRadius: 8,
|
||||
paddingHorizontal: 10, paddingVertical: 5, marginBottom: 6,
|
||||
flexDirection: 'row', alignItems: 'center', gap: 6,
|
||||
}}>
|
||||
<Text style={{ color: '#1d9e75', fontSize: 12, fontWeight: '600' }}>
|
||||
⚡ Fase: {realtime.press.phase}
|
||||
</Text>
|
||||
{(realtime.press.sisa ?? 0) > 0 && (
|
||||
<Text style={{ color: '#9ca3af', fontSize: 11 }}>
|
||||
· Sisa {realtime.press.sisa}s
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Banner RPM giling realtime */}
|
||||
{!isPengepres && mesinNyala && (
|
||||
<View style={{
|
||||
backgroundColor: '#1a2a22', borderRadius: 8,
|
||||
paddingHorizontal: 10, paddingVertical: 5, marginBottom: 6,
|
||||
flexDirection: 'row', alignItems: 'center', gap: 6,
|
||||
}}>
|
||||
<Text style={{ color: '#1d9e75', fontSize: 12, fontWeight: '600' }}>
|
||||
⚡ RPM: {realtime?.giling.rpm ?? '—'}
|
||||
</Text>
|
||||
{(realtime?.giling.sisa ?? 0) > 0 && (
|
||||
<Text style={{ color: '#9ca3af', fontSize: 11 }}>
|
||||
· Sisa {realtime?.giling.sisa}s
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Slider kecepatan — hanya untuk penggiling */}
|
||||
{!isPengepres && (
|
||||
<View style={{
|
||||
backgroundColor: '#0f1117',
|
||||
borderRadius: 10,
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#2a2e3a',
|
||||
padding: 12,
|
||||
gap: 10,
|
||||
}}>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text style={{ color: '#9ca3af', fontSize: 12 }}>⚙️ Kecepatan Penggiling</Text>
|
||||
<View style={{
|
||||
backgroundColor: kecepatanGiling >= 80 ? '#2a1f10' : kecepatanGiling >= 40 ? '#1a2a22' : '#1e1e24',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 3,
|
||||
borderWidth: 0.5,
|
||||
borderColor: kecepatanGiling >= 80 ? '#ef9f27' : kecepatanGiling >= 40 ? '#1d9e75' : '#4b5563',
|
||||
}}>
|
||||
<Text style={{
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
color: kecepatanGiling >= 80 ? '#ef9f27' : kecepatanGiling >= 40 ? '#1d9e75' : '#6b7280',
|
||||
}}>
|
||||
{kecepatanGiling}%
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Track + thumb tombol */}
|
||||
<View style={{ gap: 8 }}>
|
||||
{/* Progress bar visual */}
|
||||
<View style={{ height: 6, backgroundColor: '#2a2e3a', borderRadius: 3 }}>
|
||||
<View style={{
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
width: `${kecepatanGiling}%`,
|
||||
backgroundColor: kecepatanGiling >= 80 ? '#ef9f27' : '#1d9e75',
|
||||
}} />
|
||||
</View>
|
||||
|
||||
{/* Tombol langkah: −5 dan +5 */}
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
const idx = SPEED_STEPS.indexOf(kecepatanGiling);
|
||||
if (idx > 0) {
|
||||
const newVal = SPEED_STEPS[idx - 1];
|
||||
setKecepatanGiling(newVal);
|
||||
AsyncStorage.setItem(SPEED_STORAGE_KEY, String(newVal));
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
width: 36, height: 36, borderRadius: 8,
|
||||
backgroundColor: '#161b24', borderWidth: 0.5, borderColor: '#2a2e3a',
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<Text style={{ color: '#ffffff', fontSize: 18, lineHeight: 22 }}>−</Text>
|
||||
</Pressable>
|
||||
|
||||
{/* Tombol preset kecepatan */}
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={{ flex: 1 }}>
|
||||
<View style={{ flexDirection: 'row', gap: 6 }}>
|
||||
{[0, 25, 50, 75, 100].map(preset => (
|
||||
<Pressable
|
||||
key={preset}
|
||||
onPress={() => {
|
||||
setKecepatanGiling(preset);
|
||||
AsyncStorage.setItem(SPEED_STORAGE_KEY, String(preset));
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 8,
|
||||
backgroundColor: kecepatanGiling === preset ? '#1a2a22' : '#161b24',
|
||||
borderWidth: 0.5,
|
||||
borderColor: kecepatanGiling === preset ? '#1d9e75' : '#2a2e3a',
|
||||
}}>
|
||||
<Text style={{
|
||||
fontSize: 11,
|
||||
color: kecepatanGiling === preset ? '#1d9e75' : '#6b7280',
|
||||
fontWeight: kecepatanGiling === preset ? '600' : '400',
|
||||
}}>
|
||||
{preset}%
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
const idx = SPEED_STEPS.indexOf(kecepatanGiling);
|
||||
if (idx < SPEED_STEPS.length - 1) {
|
||||
const newVal = SPEED_STEPS[idx + 1];
|
||||
setKecepatanGiling(newVal);
|
||||
AsyncStorage.setItem(SPEED_STORAGE_KEY, String(newVal));
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
width: 36, height: 36, borderRadius: 8,
|
||||
backgroundColor: '#161b24', borderWidth: 0.5, borderColor: '#2a2e3a',
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<Text style={{ color: '#ffffff', fontSize: 18, lineHeight: 22 }}>+</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* Label 0% - 100% */}
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<Text style={{ color: '#4b5563', fontSize: 10 }}>0%</Text>
|
||||
<Text style={{ color: '#4b5563', fontSize: 10 }}>50%</Text>
|
||||
<Text style={{ color: '#4b5563', fontSize: 10 }}>100%</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{mesinNyala && (
|
||||
<Text style={{ color: '#4b5563', fontSize: 11, textAlign: 'center' }}>
|
||||
Perubahan kecepatan berlaku saat mesin dinyalakan ulang
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
<View style={styles.durasiRow}>
|
||||
<View>
|
||||
<Text style={styles.durasiLabel}>Durasi berjalan</Text>
|
||||
<Text style={styles.durasiValue}>
|
||||
{mesinNyala ? formatDurasi(mesin.seconds) : '—'}
|
||||
</Text>
|
||||
</View>
|
||||
{mesin.timerAktif && mesin.timerTarget && (
|
||||
<View style={{ alignItems: 'flex-end' }}>
|
||||
<Text style={styles.durasiLabel}>Sisa waktu</Text>
|
||||
<Text style={styles.sisaValue}>
|
||||
{formatSisa(mesin.seconds, mesin.timerTarget)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{mesin.timerAktif && mesin.timerTarget && (
|
||||
<View style={styles.progressBar}>
|
||||
<View style={[styles.progressFill, { width: `${progress}%` }]} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
<View style={styles.timerSection}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.timerTitle}>⏱ Timer Otomatis</Text>
|
||||
{mesin.timerAktif && mesin.timerTarget ? (
|
||||
<Text style={styles.timerSet}>
|
||||
Diset: {Math.floor(mesin.timerTarget / 3600)}j {Math.floor((mesin.timerTarget % 3600) / 60)}m
|
||||
</Text>
|
||||
) : (
|
||||
<Text style={styles.timerEmpty}>Belum diset</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.timerBtnRow}>
|
||||
<Pressable
|
||||
style={[styles.timerBtn, realtime?.estop.aktif && { opacity: 0.4 }]}
|
||||
disabled={realtime?.estop.aktif === true}
|
||||
onPress={() => bukaModalTimer(mesin.id)}>
|
||||
<Text style={styles.timerBtnText}>Set Timer</Text>
|
||||
</Pressable>
|
||||
{mesin.timerAktif && (
|
||||
<Pressable style={styles.timerBtnCancel} onPress={() => hapusTimer(mesin.id)}>
|
||||
<Text style={styles.timerBtnCancelText}>Hapus</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
|
||||
<View style={styles.infoBox}>
|
||||
<Text style={styles.infoIcon}>💡</Text>
|
||||
<Text style={styles.infoText}>
|
||||
Status mesin diperbarui otomatis dari sensor ESP32. Aktifkan dengan toggle, set timer agar mati otomatis.
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<Modal visible={modalTimerId !== null} transparent animationType="slide">
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalCard}>
|
||||
<Text style={styles.modalTitle}>
|
||||
Set Timer — {mesinList.find(m => m.id === modalTimerId)?.nama}
|
||||
</Text>
|
||||
<Text style={styles.modalSub}>Mesin akan mati otomatis setelah durasi selesai</Text>
|
||||
|
||||
<View style={styles.timerInputRow}>
|
||||
<View style={styles.timerInputBox}>
|
||||
<TextInput
|
||||
value={inputJam}
|
||||
onChangeText={setInputJam}
|
||||
placeholder="0"
|
||||
placeholderTextColor="#4b5563"
|
||||
keyboardType="numeric"
|
||||
maxLength={2}
|
||||
style={styles.timerInput}
|
||||
/>
|
||||
<Text style={styles.timerInputLabel}>Jam</Text>
|
||||
</View>
|
||||
<Text style={styles.timerSeparator}>:</Text>
|
||||
<View style={styles.timerInputBox}>
|
||||
<TextInput
|
||||
value={inputMenit}
|
||||
onChangeText={setInputMenit}
|
||||
placeholder="0"
|
||||
placeholderTextColor="#4b5563"
|
||||
keyboardType="numeric"
|
||||
maxLength={2}
|
||||
style={styles.timerInput}
|
||||
/>
|
||||
<Text style={styles.timerInputLabel}>Menit</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.presetLabel}>Preset cepat</Text>
|
||||
<View style={styles.presetRow}>
|
||||
{[
|
||||
{ label: '30 mnt', jam: 0, menit: 30 },
|
||||
{ label: '1 jam', jam: 1, menit: 0 },
|
||||
{ label: '2 jam', jam: 2, menit: 0 },
|
||||
{ label: '4 jam', jam: 4, menit: 0 },
|
||||
].map(p => (
|
||||
<Pressable
|
||||
key={p.label}
|
||||
style={styles.presetBtn}
|
||||
onPress={() => {
|
||||
setInputJam(String(p.jam));
|
||||
setInputMenit(String(p.menit));
|
||||
}}>
|
||||
<Text style={styles.presetText}>{p.label}</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View style={styles.modalBtnRow}>
|
||||
<Pressable style={styles.cancelBtn} onPress={() => setModalTimerId(null)}>
|
||||
<Text style={styles.cancelText}>Batal</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.saveBtn} onPress={simpanTimer}>
|
||||
<Text style={styles.saveText}>Set Timer</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
<Navbar activeScreen="Kontrol" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScheduleScreen;
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export default StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: '#0f1117' },
|
||||
topbar: {
|
||||
backgroundColor: '#161b24',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 14,
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: '#2a2e3a',
|
||||
},
|
||||
topbarTitle: { color: '#ffffff', fontSize: 18, fontWeight: '500' },
|
||||
topbarSub: { color: '#6b7280', fontSize: 12, marginTop: 2 },
|
||||
scroll: { flex: 1 },
|
||||
scrollContent: { padding: 16, gap: 14 },
|
||||
sectionLabel: {
|
||||
color: '#6b7280',
|
||||
fontSize: 11,
|
||||
fontWeight: '500',
|
||||
letterSpacing: 0.8,
|
||||
},
|
||||
mesinCard: {
|
||||
backgroundColor: '#161b24',
|
||||
borderRadius: 14,
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#2a2e3a',
|
||||
padding: 16,
|
||||
gap: 12,
|
||||
},
|
||||
mesinHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
mesinIconBox: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 10,
|
||||
backgroundColor: '#1a2a22',
|
||||
borderWidth: 1,
|
||||
borderColor: '#2a2e3a',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
mesinIconText: { fontSize: 20 },
|
||||
mesinNama: { color: '#ffffff', fontSize: 15, fontWeight: '500' },
|
||||
statusOn: { color: '#1d9e75', fontSize: 12, marginTop: 2 },
|
||||
statusOff: { color: '#6b7280', fontSize: 12, marginTop: 2 },
|
||||
toggle: {
|
||||
width: 50,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 3,
|
||||
},
|
||||
toggleOn: { backgroundColor: '#1d9e75', alignItems: 'flex-end' },
|
||||
toggleOff: { backgroundColor: '#2a2e3a', alignItems: 'flex-start' },
|
||||
toggleThumb: {
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
divider: { height: 0.5, backgroundColor: '#2a2e3a' },
|
||||
durasiRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
durasiLabel: { color: '#6b7280', fontSize: 11, marginBottom: 3 },
|
||||
durasiValue: { color: '#ffffff', fontSize: 22, fontWeight: '300' },
|
||||
sisaValue: { color: '#f97316', fontSize: 22, fontWeight: '300' },
|
||||
progressBar: {
|
||||
height: 3,
|
||||
backgroundColor: '#2a2e3a',
|
||||
borderRadius: 2,
|
||||
},
|
||||
progressFill: {
|
||||
height: 3,
|
||||
backgroundColor: '#1d9e75',
|
||||
borderRadius: 2,
|
||||
},
|
||||
timerSection: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
timerTitle: { color: '#ffffff', fontSize: 13, fontWeight: '500' },
|
||||
timerSet: { color: '#1d9e75', fontSize: 12, marginTop: 2 },
|
||||
timerEmpty: { color: '#4b5563', fontSize: 12, marginTop: 2 },
|
||||
timerBtnRow: { flexDirection: 'row', gap: 8 },
|
||||
timerBtn: {
|
||||
backgroundColor: '#1a2a22',
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#1d9e75',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 7,
|
||||
},
|
||||
timerBtnText: { color: '#1d9e75', fontSize: 12, fontWeight: '500' },
|
||||
timerBtnCancel: {
|
||||
backgroundColor: '#1e1217',
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#3a1a22',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 7,
|
||||
},
|
||||
timerBtnCancelText: { color: '#f87171', fontSize: 12 },
|
||||
infoBox: {
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
backgroundColor: '#161b24',
|
||||
borderRadius: 12,
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#2a2e3a',
|
||||
padding: 14,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
infoIcon: { fontSize: 16 },
|
||||
infoText: { color: '#6b7280', fontSize: 12, flex: 1, lineHeight: 18 },
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
modalCard: {
|
||||
backgroundColor: '#161b24',
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
padding: 24,
|
||||
gap: 14,
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#2a2e3a',
|
||||
},
|
||||
modalTitle: { color: '#ffffff', fontSize: 16, fontWeight: '500' },
|
||||
modalSub: { color: '#6b7280', fontSize: 12, marginTop: -8 },
|
||||
timerInputRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
timerInputBox: { alignItems: 'center', gap: 6 },
|
||||
timerInput: {
|
||||
backgroundColor: '#0f1117',
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#2a2e3a',
|
||||
borderRadius: 10,
|
||||
width: 80,
|
||||
padding: 12,
|
||||
color: '#ffffff',
|
||||
fontSize: 28,
|
||||
fontWeight: '300',
|
||||
textAlign: 'center',
|
||||
},
|
||||
timerInputLabel: { color: '#6b7280', fontSize: 12 },
|
||||
timerSeparator: { color: '#6b7280', fontSize: 28, marginBottom: 20 },
|
||||
presetLabel: { color: '#9ca3af', fontSize: 12 },
|
||||
presetRow: { flexDirection: 'row', gap: 8 },
|
||||
presetBtn: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0f1117',
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#2a2e3a',
|
||||
borderRadius: 8,
|
||||
padding: 10,
|
||||
alignItems: 'center',
|
||||
},
|
||||
presetText: { color: '#9ca3af', fontSize: 12 },
|
||||
modalBtnRow: { flexDirection: 'row', gap: 10 },
|
||||
cancelBtn: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0f1117',
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#2a2e3a',
|
||||
borderRadius: 10,
|
||||
padding: 14,
|
||||
alignItems: 'center',
|
||||
},
|
||||
cancelText: { color: '#6b7280', fontSize: 14 },
|
||||
saveBtn: {
|
||||
flex: 1,
|
||||
backgroundColor: '#1d9e75',
|
||||
borderRadius: 10,
|
||||
padding: 14,
|
||||
alignItems: 'center',
|
||||
},
|
||||
saveText: { color: '#ffffff', fontSize: 14, fontWeight: '500' },
|
||||
navbar: {
|
||||
backgroundColor: '#161b24',
|
||||
borderTopWidth: 0.5,
|
||||
borderTopColor: '#2a2e3a',
|
||||
flexDirection: 'row',
|
||||
paddingTop: 10,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
navItem: { flex: 1, alignItems: 'center', gap: 4 },
|
||||
navIcon: { fontSize: 22 },
|
||||
navLabel: { fontSize: 11 },
|
||||
navActive: { color: '#1d9e75' },
|
||||
navInactive: { color: '#4b5563' },
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
export type DummyUser = {
|
||||
name?: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
let dummyUsers: DummyUser[] = [
|
||||
{
|
||||
name: 'Admin',
|
||||
email: 'admin@gmail.com',
|
||||
password: '123456',
|
||||
},
|
||||
];
|
||||
export const getDummyUsers = () => dummyUsers;
|
||||
export const addDummyUser = (user: DummyUser) => {
|
||||
dummyUsers = [...dummyUsers, user];
|
||||
};
|
||||
export const findDummyUser = (email: string, password: string) => {
|
||||
return dummyUsers.find(
|
||||
item =>
|
||||
item.email.toLowerCase() === email.toLowerCase() &&
|
||||
item.password === password,
|
||||
);
|
||||
};
|
||||
export const isEmailUsed = (email: string) => {
|
||||
return dummyUsers.some(
|
||||
item => item.email.toLowerCase() === email.toLowerCase(),
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import React from 'react';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import RouteName from '../Constants/RouteName.constants';
|
||||
import { RootStackParamList } from '../Constants/RouteParamsList.contants';
|
||||
import { UnauthorizedScreens, AuthorizedScreens } from './config';
|
||||
|
||||
const Stack = createNativeStackNavigator<RootStackParamList>();
|
||||
|
||||
interface Props {
|
||||
initialRoute: string;
|
||||
}
|
||||
|
||||
const AppStack = ({ initialRoute }: Props) => {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
initialRouteName={initialRoute as keyof RootStackParamList}
|
||||
screenOptions={{ headerShown: false }}>
|
||||
{[...UnauthorizedScreens, ...AuthorizedScreens].map(screen => (
|
||||
<Stack.Screen
|
||||
key={screen.name}
|
||||
name={screen.name as keyof RootStackParamList}
|
||||
component={screen.component}
|
||||
options={screen.options}
|
||||
/>
|
||||
))}
|
||||
</Stack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppStack;
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import RouteName from '../Constants/RouteName.constants';
|
||||
import LoginScreen from '../Containers/Auth/LoginScreen';
|
||||
import RegisterScreen from '../Containers/Auth/RegisterScreen';
|
||||
import DashboardScreen from '../Containers/Dashboard';
|
||||
import ScheduleScreen from '../Containers/Schedule';
|
||||
import HistoryScreen from '../Containers/History';
|
||||
|
||||
interface IScreen {
|
||||
name: string;
|
||||
component: any;
|
||||
options?: any;
|
||||
}
|
||||
|
||||
export const UnauthorizedScreens: IScreen[] = [
|
||||
{
|
||||
name: RouteName.LoginNavigation,
|
||||
component: LoginScreen,
|
||||
options: { headerShown: false },
|
||||
},
|
||||
{
|
||||
name: RouteName.RegisterNavigation,
|
||||
component: RegisterScreen,
|
||||
options: { headerShown: false },
|
||||
},
|
||||
];
|
||||
|
||||
export const AuthorizedScreens: IScreen[] = [
|
||||
{
|
||||
name: RouteName.DashboardNavigation,
|
||||
component: DashboardScreen,
|
||||
options: { headerShown: false },
|
||||
},
|
||||
{
|
||||
name: RouteName.ScheduleNavigation,
|
||||
component: ScheduleScreen,
|
||||
options: { headerShown: false },
|
||||
},
|
||||
{
|
||||
name: RouteName.HistoryNavigation,
|
||||
component: HistoryScreen,
|
||||
options: { headerShown: false },
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import axios from 'axios';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
export const BASE_URL = 'https://backendaplikasi-production.up.railway.app/api';
|
||||
|
||||
// Cache token di memori — hindari disk read AsyncStorage setiap request
|
||||
let cachedToken: string | null = null;
|
||||
|
||||
export const loadToken = async () => {
|
||||
cachedToken = await AsyncStorage.getItem('token');
|
||||
};
|
||||
|
||||
export const setToken = (token: string | null) => {
|
||||
cachedToken = token;
|
||||
};
|
||||
|
||||
export const clearToken = () => {
|
||||
cachedToken = null;
|
||||
};
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
if (cachedToken) config.headers.Authorization = `Bearer ${cachedToken}`;
|
||||
return config;
|
||||
});
|
||||
|
||||
export { cachedToken };
|
||||
export default api;
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"jsx": "react-native",
|
||||
"ignoreDeprecations": "6.0",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@Containers/*": ["src/Containers/*"],
|
||||
"@Constants/*": ["src/Constants/*"],
|
||||
"@Navigators/*": ["src/Navigators/*"],
|
||||
"@Dummy/*": ["src/Dummy/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||