create-du-app 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/package.json +1 -1
- package/src/generate.js +15 -2
- package/src/index.js +7 -1
- package/templates/mobile/expo/.env.example +2 -2
- package/templates/mobile/expo/README.md +31 -3
- package/templates/mobile/expo/_package.json +13 -15
- package/templates/mobile/expo/app.json +10 -2
- package/templates/mobile/expo/index.js +2 -0
- package/templates/mobile/expo/src/app/app-provider.tsx +7 -3
- package/templates/mobile/expo/src/app/config/translation.ts +7 -3
- package/templates/mobile/expo/src/assets/i18n/en.json +19 -3
- package/templates/mobile/expo/src/assets/i18n/fr.json +19 -3
- package/templates/mobile/expo/src/core/components/forms/date-time-picker.modal.tsx +116 -0
- package/templates/mobile/expo/src/core/components/forms/hf-date-time.tsx +2 -10
- package/templates/mobile/expo/src/core/components/forms/hf-time-picker.tsx +2 -3
- package/templates/mobile/expo/src/core/components/screen/screen-container/screen-container.tsx +25 -29
- package/templates/mobile/expo/src/core/components/ui/app-image/app-image.tsx +16 -19
- package/templates/mobile/expo/src/core/components/ui/app-image/app-image.type.ts +6 -6
- package/templates/mobile/expo/src/core/components/ui/avatar-image/avatar-image.tsx +1 -1
- package/templates/mobile/expo/src/core/components/ui/image-slider/image-slider.tsx +3 -3
- package/templates/mobile/expo/src/core/components/ui/screen/screen-gradient.tsx +1 -1
- package/templates/mobile/expo/src/core/components/ui/skeleton/skeleton.tsx +1 -1
- package/templates/mobile/expo/src/core/services/api.service.ts +3 -3
- package/templates/mobile/expo/src/core/services/device-id.service.ts +16 -2
- package/templates/mobile/expo/src/core/utils/device-locale.util.ts +10 -8
- package/templates/mobile/expo/src/core/utils/image-picker.util.ts +37 -58
- package/templates/mobile/expo/src/core/utils/query-persister.util.ts +16 -21
- package/templates/mobile/expo/src/modules/home/home.screen.tsx +97 -20
- package/templates/mobile/rn/.bundle/config +2 -0
- package/templates/mobile/rn/.watchmanconfig +1 -0
- package/templates/mobile/rn/Gemfile +17 -0
- package/templates/mobile/rn/README.md +34 -2
- package/templates/mobile/rn/_package.json +2 -0
- package/templates/mobile/rn/android/app/build.gradle +126 -0
- package/templates/mobile/rn/android/app/debug.keystore +0 -0
- package/templates/mobile/rn/android/app/proguard-rules.pro +10 -0
- package/templates/mobile/rn/android/app/src/main/AndroidManifest.xml +27 -0
- package/templates/mobile/rn/android/app/src/main/java/com/dumobile/MainActivity.kt +22 -0
- package/templates/mobile/rn/android/app/src/main/java/com/dumobile/MainApplication.kt +27 -0
- package/templates/mobile/rn/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
- package/templates/mobile/rn/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/templates/mobile/rn/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/templates/mobile/rn/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/templates/mobile/rn/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/templates/mobile/rn/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/templates/mobile/rn/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/templates/mobile/rn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/templates/mobile/rn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/templates/mobile/rn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/templates/mobile/rn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/templates/mobile/rn/android/app/src/main/res/values/strings.xml +3 -0
- package/templates/mobile/rn/android/app/src/main/res/values/styles.xml +9 -0
- package/templates/mobile/rn/android/build.gradle +21 -0
- package/templates/mobile/rn/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/templates/mobile/rn/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/templates/mobile/rn/android/gradle.properties +44 -0
- package/templates/mobile/rn/android/gradlew +248 -0
- package/templates/mobile/rn/android/gradlew.bat +98 -0
- package/templates/mobile/rn/android/settings.gradle +21 -0
- package/templates/mobile/rn/app.json +1 -1
- package/templates/mobile/rn/index.js +2 -0
- package/templates/mobile/rn/ios/.xcode.env +11 -0
- package/templates/mobile/rn/ios/DuMobile/AppDelegate.swift +48 -0
- package/templates/mobile/rn/ios/DuMobile/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
- package/templates/mobile/rn/ios/DuMobile/Images.xcassets/Contents.json +6 -0
- package/templates/mobile/rn/ios/DuMobile/Info.plist +59 -0
- package/templates/mobile/rn/ios/DuMobile/LaunchScreen.storyboard +47 -0
- package/templates/mobile/rn/ios/DuMobile/PrivacyInfo.xcprivacy +37 -0
- package/templates/mobile/rn/ios/DuMobile.xcodeproj/project.pbxproj +475 -0
- package/templates/mobile/rn/ios/DuMobile.xcodeproj/xcshareddata/xcschemes/DuMobile.xcscheme +88 -0
- package/templates/mobile/rn/ios/Podfile +34 -0
- package/templates/mobile/rn/src/app/app-provider.tsx +19 -14
- package/templates/mobile/rn/src/app/config/translation.ts +3 -0
- package/templates/mobile/rn/src/assets/i18n/en.json +13 -3
- package/templates/mobile/rn/src/assets/i18n/fr.json +13 -3
- package/templates/mobile/rn/src/modules/home/home.screen.tsx +53 -19
package/README.md
CHANGED
|
@@ -19,10 +19,13 @@ You'll be asked:
|
|
|
19
19
|
2. **Select the groups** → press **`Space`** to tick Mobile / Frontend / Backend, then **`Enter`**
|
|
20
20
|
3. **Pick one technology** per group → **↑ / ↓** then **`Enter`**
|
|
21
21
|
|
|
22
|
-
Then
|
|
22
|
+
Then bootstrap it — one command installs deps, builds `@repo/shared`, and
|
|
23
|
+
verifies every app can import it:
|
|
23
24
|
|
|
24
25
|
```bash
|
|
25
|
-
cd my-shop
|
|
26
|
+
cd my-shop
|
|
27
|
+
pnpm bootstrap
|
|
28
|
+
pnpm dev
|
|
26
29
|
```
|
|
27
30
|
|
|
28
31
|
Done. 🎉
|
package/package.json
CHANGED
package/src/generate.js
CHANGED
|
@@ -81,7 +81,19 @@ async function copyTemplate(srcDir, destDir, projectName) {
|
|
|
81
81
|
// ---------------------------------------------------------------------------
|
|
82
82
|
|
|
83
83
|
// Build the project's root package.json (pnpm: NO "workspaces" field).
|
|
84
|
-
function buildRootPackageJson(projectName, hasShared) {
|
|
84
|
+
function buildRootPackageJson(projectName, hasShared, createdAppDirs = []) {
|
|
85
|
+
// Per-app convenience scripts (life-master style): build @repo/shared first
|
|
86
|
+
// (if it exists), then run that app's dev server. Keyed by the app folder
|
|
87
|
+
// name, filtered to the app's package (e.g. `pnpm mobile` -> runs apps/mobile).
|
|
88
|
+
const sharedPrefix = hasShared
|
|
89
|
+
? `turbo run build --filter=${SHARED_PACKAGE_NAME} && `
|
|
90
|
+
: '';
|
|
91
|
+
const appScripts = {};
|
|
92
|
+
for (const dir of createdAppDirs) {
|
|
93
|
+
const app = dir.split('/').pop(); // 'apps/mobile' -> 'mobile'
|
|
94
|
+
appScripts[app] = `${sharedPrefix}turbo run dev --filter=${projectName}-${app}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
85
97
|
return {
|
|
86
98
|
name: projectName,
|
|
87
99
|
version: '0.1.0',
|
|
@@ -98,6 +110,7 @@ function buildRootPackageJson(projectName, hasShared) {
|
|
|
98
110
|
...(hasShared
|
|
99
111
|
? { 'shared:build': `turbo run build --filter=${SHARED_PACKAGE_NAME}` }
|
|
100
112
|
: {}),
|
|
113
|
+
...appScripts,
|
|
101
114
|
},
|
|
102
115
|
devDependencies: {
|
|
103
116
|
turbo: '^2.3.3',
|
|
@@ -333,7 +346,7 @@ export async function generate(plan, repoRoot, cwd = process.cwd()) {
|
|
|
333
346
|
// 3) Generate the root config files dynamically.
|
|
334
347
|
await fse.writeJson(
|
|
335
348
|
path.join(projectRoot, 'package.json'),
|
|
336
|
-
buildRootPackageJson(projectName, hasShared),
|
|
349
|
+
buildRootPackageJson(projectName, hasShared, createdAppDirs),
|
|
337
350
|
{ spaces: 2 },
|
|
338
351
|
);
|
|
339
352
|
logs.push('✓ package.json');
|
package/src/index.js
CHANGED
|
@@ -168,7 +168,13 @@ async function main() {
|
|
|
168
168
|
console.log('');
|
|
169
169
|
|
|
170
170
|
const rel = path.relative(process.cwd(), projectRoot) || projectRoot;
|
|
171
|
-
|
|
171
|
+
// `pnpm bootstrap` is the one-command setup: it installs deps, builds
|
|
172
|
+
// @repo/shared, and verifies each app resolves it. Then `pnpm dev`.
|
|
173
|
+
const msg =
|
|
174
|
+
`Done! Next:\n\n` +
|
|
175
|
+
` cd ${rel}\n` +
|
|
176
|
+
` pnpm bootstrap # install deps + build @repo/shared + verify\n` +
|
|
177
|
+
` pnpm dev # start the dev servers\n`;
|
|
172
178
|
// outro() (clack) only looks good in a TTY; print plainly in non-interactive mode.
|
|
173
179
|
if (process.stdout.isTTY && !hasSelectionFlags(args)) outro(msg);
|
|
174
180
|
else console.log(msg);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# Copy to .env and fill in.
|
|
1
|
+
# Copy to .env and fill in. Expo inlines any EXPO_PUBLIC_* var at build time.
|
|
2
2
|
# NEVER commit the real .env — only this example.
|
|
3
3
|
|
|
4
4
|
# Base URL of your API (include trailing slash).
|
|
5
|
-
|
|
5
|
+
EXPO_PUBLIC_BASE_URL=https://api.example.com/
|
|
@@ -19,7 +19,7 @@ src/
|
|
|
19
19
|
| Area | Where |
|
|
20
20
|
|------|-------|
|
|
21
21
|
| API | `services/api.service.ts` (axios + auth header + device-id + 401 session handling), `api/example.api.ts` (React Query pattern) |
|
|
22
|
-
| Data | `utils/query-client.util.ts`, `utils/query-persister.util.ts` (
|
|
22
|
+
| Data | `utils/query-client.util.ts`, `utils/query-persister.util.ts` (AsyncStorage offline cache) |
|
|
23
23
|
| Theme | `theme/` — context + `Colors` design tokens (`useTheme`, `useThemedStyles`) |
|
|
24
24
|
| UI kit | `components/ui/*` — button, text-field/area, checkbox, radio, toggle, modal, bottom-sheet, header, tabs, otp-input, search-box, skeleton, app-image, image-slider… |
|
|
25
25
|
| Forms | `components/forms/*` — react-hook-form bindings over the DS fields |
|
|
@@ -33,14 +33,42 @@ package exists; the CLI wires `"@repo/shared": "workspace:*"` automatically.
|
|
|
33
33
|
|
|
34
34
|
## Getting started
|
|
35
35
|
|
|
36
|
+
> [!TIP]
|
|
37
|
+
> Every native dependency in this starter ships inside **Expo Go**, so you can
|
|
38
|
+
> run it by scanning the QR code — **no dev build / Xcode / Android Studio
|
|
39
|
+
> needed**. (See [Expo Go compatibility](#expo-go-compatibility) for how.)
|
|
40
|
+
|
|
36
41
|
```bash
|
|
37
42
|
pnpm bootstrap # install + build @repo/shared
|
|
38
43
|
pnpm --filter {{PROJECT_NAME}}-mobile typecheck # verify
|
|
39
|
-
pnpm --filter {{PROJECT_NAME}}-mobile start # expo
|
|
44
|
+
pnpm --filter {{PROJECT_NAME}}-mobile start # expo start — scan QR in Expo Go
|
|
40
45
|
```
|
|
41
46
|
|
|
47
|
+
Press `i` (iOS simulator) / `a` (Android emulator) in the Metro terminal, or scan
|
|
48
|
+
the QR with the **Expo Go** app on a physical device.
|
|
49
|
+
|
|
42
50
|
Add an icon: drop `name.svg` in `src/assets/svgs/` then `pnpm --filter {{PROJECT_NAME}}-mobile sync-svgs`.
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
## Expo Go compatibility
|
|
53
|
+
|
|
54
|
+
This starter deliberately uses only libraries bundled in Expo Go, so the standard
|
|
55
|
+
QR-code workflow works out of the box. Where the original production app used a
|
|
56
|
+
bare-RN native module, the Expo equivalent is used instead:
|
|
57
|
+
|
|
58
|
+
| Need | Library used |
|
|
59
|
+
|------|--------------|
|
|
60
|
+
| Images (cache, prefetch) | `expo-image` |
|
|
61
|
+
| Pick / capture photos | `expo-image-picker` |
|
|
62
|
+
| Date / time picker | `@react-native-community/datetimepicker` |
|
|
63
|
+
| Gradients | `expo-linear-gradient` |
|
|
64
|
+
| Device locale | `expo-localization` |
|
|
65
|
+
| Device id | `expo-application` (+ `expo-crypto` fallback) |
|
|
66
|
+
| Offline query cache | `@react-native-async-storage/async-storage` |
|
|
67
|
+
| Env vars | `process.env.EXPO_PUBLIC_*` (set in `.env`) |
|
|
68
|
+
| Keyboard avoidance | React Native `KeyboardAvoidingView` |
|
|
69
|
+
|
|
70
|
+
> Need a bare-RN native module later (e.g. MMKV, a custom SDK)? Run
|
|
71
|
+
> `npx expo prebuild` + `expo run:ios` / `run:android` to switch to a development
|
|
72
|
+
> build. Until then, plain `expo start` + Expo Go is all you need.
|
|
45
73
|
|
|
46
74
|
> Before shipping: set `name`, `slug`, `scheme`, and the iOS/Android bundle ids in `app.json`.
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
+
"dev": "expo start",
|
|
7
8
|
"start": "expo start",
|
|
8
9
|
"android": "expo run:android",
|
|
9
10
|
"ios": "expo run:ios",
|
|
@@ -15,38 +16,35 @@
|
|
|
15
16
|
},
|
|
16
17
|
"dependencies": {
|
|
17
18
|
"@hookform/error-message": "2.0.0",
|
|
18
|
-
"@react-native-async-storage/async-storage": "
|
|
19
|
-
"@react-native-community/netinfo": "
|
|
19
|
+
"@react-native-async-storage/async-storage": "2.2.0",
|
|
20
|
+
"@react-native-community/netinfo": "12.0.1",
|
|
20
21
|
"@react-navigation/bottom-tabs": "^7.12.0",
|
|
21
22
|
"@react-navigation/native": "^7.0.13",
|
|
23
|
+
"@react-native-community/datetimepicker": "9.1.0",
|
|
22
24
|
"@react-navigation/native-stack": "^7.1.14",
|
|
23
25
|
"@tanstack/react-query": "^5.101.0",
|
|
24
26
|
"@tanstack/react-query-persist-client": "^5.101.0",
|
|
25
27
|
"axios": "^1.7.9",
|
|
26
28
|
"expo": "~56.0.6",
|
|
29
|
+
"expo-application": "~56.0.3",
|
|
30
|
+
"expo-crypto": "~56.0.4",
|
|
31
|
+
"expo-image": "~56.0.11",
|
|
32
|
+
"expo-image-picker": "~56.0.18",
|
|
33
|
+
"expo-linear-gradient": "~56.0.4",
|
|
34
|
+
"expo-localization": "~56.0.6",
|
|
27
35
|
"i18next": "^21.5.4",
|
|
28
36
|
"lodash": "^4.17.21",
|
|
29
|
-
"lottie-react-native": "
|
|
37
|
+
"lottie-react-native": "~7.3.4",
|
|
30
38
|
"qs": "^6.10.1",
|
|
31
39
|
"react": "19.2.3",
|
|
32
40
|
"react-hook-form": "^7.53.2",
|
|
33
41
|
"react-i18next": "^11.14.3",
|
|
34
42
|
"react-native": "0.85.3",
|
|
35
|
-
"react-native-config": "^1.6.1",
|
|
36
|
-
"react-native-date-picker": "5.0.12",
|
|
37
|
-
"react-native-device-info": "^15.0.2",
|
|
38
|
-
"react-native-fast-image": "^8.6.3",
|
|
39
43
|
"react-native-gesture-handler": "~2.31.1",
|
|
40
|
-
"react-native-image-picker": "^8.2.1",
|
|
41
|
-
"react-native-keyboard-controller": "^1.21.6",
|
|
42
|
-
"react-native-linear-gradient": "^2.8.3",
|
|
43
|
-
"react-native-localize": "^3.7.0",
|
|
44
|
-
"react-native-mmkv": "^4.3.1",
|
|
45
|
-
"react-native-nitro-modules": "^0.35.9",
|
|
46
44
|
"react-native-reanimated": "4.3.1",
|
|
47
|
-
"react-native-safe-area-context": "
|
|
45
|
+
"react-native-safe-area-context": "~5.7.0",
|
|
48
46
|
"react-native-screens": "4.25.2",
|
|
49
|
-
"react-native-svg": "
|
|
47
|
+
"react-native-svg": "15.15.4",
|
|
50
48
|
"react-native-toast-message": "^2.2.1",
|
|
51
49
|
"react-native-worklets": "0.8.3",
|
|
52
50
|
"yup": "^0.32.11",
|
|
@@ -9,10 +9,18 @@
|
|
|
9
9
|
"scheme": "myapp",
|
|
10
10
|
"ios": {
|
|
11
11
|
"supportsTablet": true,
|
|
12
|
-
"bundleIdentifier": "com.company.myapp"
|
|
12
|
+
"bundleIdentifier": "com.company.myapp",
|
|
13
|
+
"infoPlist": {
|
|
14
|
+
"NSPhotoLibraryUsageDescription": "Allow $(PRODUCT_NAME) to access your photos.",
|
|
15
|
+
"NSCameraUsageDescription": "Allow $(PRODUCT_NAME) to use the camera."
|
|
16
|
+
}
|
|
13
17
|
},
|
|
14
18
|
"android": {
|
|
15
|
-
"package": "com.company.myapp"
|
|
19
|
+
"package": "com.company.myapp",
|
|
20
|
+
"permissions": [
|
|
21
|
+
"android.permission.CAMERA",
|
|
22
|
+
"android.permission.READ_MEDIA_IMAGES"
|
|
23
|
+
]
|
|
16
24
|
}
|
|
17
25
|
}
|
|
18
26
|
}
|
|
@@ -5,7 +5,8 @@ import { I18nextProvider } from 'react-i18next';
|
|
|
5
5
|
import { onlineManager } from '@tanstack/react-query';
|
|
6
6
|
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
|
|
7
7
|
import NetInfo from '@react-native-community/netinfo';
|
|
8
|
-
import
|
|
8
|
+
import Toast from 'react-native-toast-message';
|
|
9
|
+
import { queryClient, queryPersister, toastConfig } from '@src/core/utils';
|
|
9
10
|
import { ThemeProvider } from '@src/core/theme';
|
|
10
11
|
import { i18n } from './config/translation';
|
|
11
12
|
import { App } from './App';
|
|
@@ -16,17 +17,20 @@ onlineManager.setEventListener(setOnline =>
|
|
|
16
17
|
);
|
|
17
18
|
|
|
18
19
|
// The provider tree. Order: gesture root → persisted query cache → i18n →
|
|
19
|
-
// safe-area → theme → app UI.
|
|
20
|
+
// safe-area → theme → app UI. <Toast> is mounted last so it overlays all.
|
|
21
|
+
// Keyboard handling uses React Native's built-in KeyboardAvoidingView (in
|
|
22
|
+
// ScreenContainer) so the app runs in Expo Go without a custom dev build.
|
|
20
23
|
export default function AppProvider() {
|
|
21
24
|
return (
|
|
22
25
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
23
26
|
<PersistQueryClientProvider
|
|
24
27
|
client={queryClient}
|
|
25
|
-
persistOptions={{ persister:
|
|
28
|
+
persistOptions={{ persister: queryPersister }}>
|
|
26
29
|
<I18nextProvider i18n={i18n}>
|
|
27
30
|
<SafeAreaProvider>
|
|
28
31
|
<ThemeProvider>
|
|
29
32
|
<App />
|
|
33
|
+
<Toast config={toastConfig} />
|
|
30
34
|
</ThemeProvider>
|
|
31
35
|
</SafeAreaProvider>
|
|
32
36
|
</I18nextProvider>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getLocales } from 'expo-localization';
|
|
2
2
|
import { Translator } from '@src/core/utils';
|
|
3
3
|
import en from '@src/assets/i18n/en.json';
|
|
4
4
|
import fr from '@src/assets/i18n/fr.json';
|
|
@@ -9,15 +9,19 @@ const resources = {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
const fallbackLng = 'en';
|
|
12
|
-
|
|
12
|
+
// expo-localization reads the OS preferred-languages list (ships in Expo Go).
|
|
13
|
+
const deviceLanguage = getLocales()[0]?.languageCode ?? fallbackLng;
|
|
13
14
|
|
|
14
15
|
// Initialize i18next once and expose the instance. The provider mounts it in
|
|
15
16
|
// app-provider.tsx; the active language can be changed via Translator.changeLanguages.
|
|
16
17
|
export const i18n = Translator.setup({
|
|
17
18
|
resources,
|
|
18
|
-
lng:
|
|
19
|
+
lng: deviceLanguage in resources ? deviceLanguage : fallbackLng,
|
|
19
20
|
fallbackLng,
|
|
20
21
|
interpolation: { escapeValue: false },
|
|
22
|
+
// RN's JS engine lacks full Intl.PluralRules — use i18next's v3 plural format
|
|
23
|
+
// so it doesn't warn and fall back at runtime.
|
|
24
|
+
compatibilityJSON: 'v3',
|
|
21
25
|
});
|
|
22
26
|
|
|
23
27
|
// Hook persisted-language restore here if you store a user preference.
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"home": {
|
|
3
3
|
"title": "Home",
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
4
|
+
"subtitle": "Your company mobile starter — built on the shared monorepo core.",
|
|
5
|
+
"features": {
|
|
6
|
+
"nav": { "title": "Navigation", "desc": "React Navigation — typed native-stack + bottom tabs." },
|
|
7
|
+
"data": { "title": "Networking", "desc": "Axios client + React Query, offline cache via MMKV." },
|
|
8
|
+
"i18n": { "title": "i18n", "desc": "react-i18next with device locale (en / fr)." },
|
|
9
|
+
"theme": { "title": "Theming", "desc": "Theme context + design tokens (dark / light)." },
|
|
10
|
+
"forms": { "title": "Forms", "desc": "react-hook-form bindings over the design-system fields." },
|
|
11
|
+
"ui": { "title": "UI kit", "desc": "Buttons, inputs, modals, tabs, skeletons and more." },
|
|
12
|
+
"state": { "title": "State", "desc": "Zustand stores (auth, loading) + AsyncStorage." },
|
|
13
|
+
"shared": { "title": "@repo/shared", "desc": "Types, enums and API contracts shared across apps." }
|
|
14
|
+
},
|
|
15
|
+
"footer": "Edit src/modules/home to make it yours.",
|
|
16
|
+
"language": "Language",
|
|
17
|
+
"author": "Created by Long Mobile",
|
|
18
|
+
"loadError": "Failed to load."
|
|
7
19
|
},
|
|
8
20
|
"profile": {
|
|
9
21
|
"title": "Profile",
|
|
10
22
|
"signOut": "Sign out"
|
|
23
|
+
},
|
|
24
|
+
"common": {
|
|
25
|
+
"confirm": "Confirm",
|
|
26
|
+
"cancel": "Cancel"
|
|
11
27
|
}
|
|
12
28
|
}
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"home": {
|
|
3
3
|
"title": "Accueil",
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
4
|
+
"subtitle": "Votre starter mobile d'entreprise — basé sur le cœur partagé du monorepo.",
|
|
5
|
+
"features": {
|
|
6
|
+
"nav": { "title": "Navigation", "desc": "React Navigation — native-stack + onglets typés." },
|
|
7
|
+
"data": { "title": "Réseau", "desc": "Client Axios + React Query, cache hors-ligne via MMKV." },
|
|
8
|
+
"i18n": { "title": "i18n", "desc": "react-i18next avec la langue de l'appareil (en / fr)." },
|
|
9
|
+
"theme": { "title": "Thème", "desc": "Contexte de thème + design tokens (sombre / clair)." },
|
|
10
|
+
"forms": { "title": "Formulaires", "desc": "react-hook-form sur les champs du design-system." },
|
|
11
|
+
"ui": { "title": "Kit UI", "desc": "Boutons, champs, modales, onglets, skeletons, etc." },
|
|
12
|
+
"state": { "title": "State", "desc": "Stores Zustand (auth, loading) + AsyncStorage." },
|
|
13
|
+
"shared": { "title": "@repo/shared", "desc": "Types, enums et contrats d'API partagés entre apps." }
|
|
14
|
+
},
|
|
15
|
+
"footer": "Modifiez src/modules/home pour l'adapter.",
|
|
16
|
+
"language": "Langue",
|
|
17
|
+
"author": "Créé par Long Mobile",
|
|
18
|
+
"loadError": "Échec du chargement."
|
|
7
19
|
},
|
|
8
20
|
"profile": {
|
|
9
21
|
"title": "Profil",
|
|
10
22
|
"signOut": "Se déconnecter"
|
|
23
|
+
},
|
|
24
|
+
"common": {
|
|
25
|
+
"confirm": "Confirmer",
|
|
26
|
+
"cancel": "Annuler"
|
|
11
27
|
}
|
|
12
28
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Modal, Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
|
|
3
|
+
import DateTimePicker, {
|
|
4
|
+
DateTimePickerAndroid,
|
|
5
|
+
} from '@react-native-community/datetimepicker';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { ThemeColors, useThemedStyles } from '@src/core/theme';
|
|
8
|
+
import { Font, fontSize } from '../../utils';
|
|
9
|
+
import { Label } from '../ui';
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
open: boolean;
|
|
13
|
+
date: Date;
|
|
14
|
+
mode: 'date' | 'time';
|
|
15
|
+
maximumDate?: Date;
|
|
16
|
+
onConfirm: (date: Date) => void;
|
|
17
|
+
onCancel: () => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Cross-platform date/time picker built on @react-native-community/datetimepicker
|
|
22
|
+
* (ships in Expo Go — no dev build needed). On Android it opens the native OS
|
|
23
|
+
* dialog imperatively; on iOS it renders a spinner inside a bottom-sheet modal
|
|
24
|
+
* with Confirm / Cancel actions.
|
|
25
|
+
*/
|
|
26
|
+
export const DateTimePickerModal = ({
|
|
27
|
+
open,
|
|
28
|
+
date,
|
|
29
|
+
mode,
|
|
30
|
+
maximumDate,
|
|
31
|
+
onConfirm,
|
|
32
|
+
onCancel,
|
|
33
|
+
}: Props) => {
|
|
34
|
+
const { t } = useTranslation();
|
|
35
|
+
const styles = useThemedStyles(makeStyles);
|
|
36
|
+
const [temp, setTemp] = useState(date);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (open) setTemp(date);
|
|
40
|
+
}, [open, date]);
|
|
41
|
+
|
|
42
|
+
// Android has no in-tree picker view — open the OS dialog when `open` flips on.
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (Platform.OS !== 'android' || !open) return;
|
|
45
|
+
DateTimePickerAndroid.open({
|
|
46
|
+
value: date,
|
|
47
|
+
mode,
|
|
48
|
+
maximumDate,
|
|
49
|
+
onChange: (event, selected) => {
|
|
50
|
+
if (event.type === 'set' && selected) onConfirm(selected);
|
|
51
|
+
else onCancel();
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
// Reacts only to `open` flipping true; date/mode are read at open time.
|
|
55
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
56
|
+
}, [open]);
|
|
57
|
+
|
|
58
|
+
if (Platform.OS === 'android') return null;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Modal visible={open} transparent animationType="slide" onRequestClose={onCancel}>
|
|
62
|
+
<View style={styles.backdrop}>
|
|
63
|
+
<View style={styles.sheet}>
|
|
64
|
+
<View style={styles.actions}>
|
|
65
|
+
<TouchableOpacity onPress={onCancel} hitSlop={8}>
|
|
66
|
+
<Label style={styles.cancel} value={t('common.cancel')} />
|
|
67
|
+
</TouchableOpacity>
|
|
68
|
+
<TouchableOpacity onPress={() => onConfirm(temp)} hitSlop={8}>
|
|
69
|
+
<Label style={styles.confirm} value={t('common.confirm')} />
|
|
70
|
+
</TouchableOpacity>
|
|
71
|
+
</View>
|
|
72
|
+
<DateTimePicker
|
|
73
|
+
value={temp}
|
|
74
|
+
mode={mode}
|
|
75
|
+
display="spinner"
|
|
76
|
+
maximumDate={maximumDate}
|
|
77
|
+
onChange={(_, selected) => selected && setTemp(selected)}
|
|
78
|
+
/>
|
|
79
|
+
</View>
|
|
80
|
+
</View>
|
|
81
|
+
</Modal>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default DateTimePickerModal;
|
|
86
|
+
|
|
87
|
+
const makeStyles = (c: ThemeColors) =>
|
|
88
|
+
StyleSheet.create({
|
|
89
|
+
backdrop: {
|
|
90
|
+
flex: 1,
|
|
91
|
+
justifyContent: 'flex-end',
|
|
92
|
+
backgroundColor: 'rgba(0,0,0,0.4)',
|
|
93
|
+
},
|
|
94
|
+
sheet: {
|
|
95
|
+
backgroundColor: c.bg_elevation_level_1_normal,
|
|
96
|
+
paddingBottom: 24,
|
|
97
|
+
},
|
|
98
|
+
actions: {
|
|
99
|
+
flexDirection: 'row',
|
|
100
|
+
justifyContent: 'space-between',
|
|
101
|
+
paddingHorizontal: 16,
|
|
102
|
+
paddingVertical: 12,
|
|
103
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
104
|
+
borderBottomColor: c.bd_neutral_faded,
|
|
105
|
+
},
|
|
106
|
+
cancel: {
|
|
107
|
+
fontFamily: Font.rethinkSansRegular,
|
|
108
|
+
fontSize: fontSize(16),
|
|
109
|
+
color: c.fg_neutral_faded,
|
|
110
|
+
},
|
|
111
|
+
confirm: {
|
|
112
|
+
fontFamily: Font.rethinkSansBold,
|
|
113
|
+
fontSize: fontSize(16),
|
|
114
|
+
color: c.fg_neutral_normal,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ErrorMessage } from '@hookform/error-message';
|
|
2
2
|
import React, { FC, useMemo, useState } from 'react';
|
|
3
3
|
import { useController, useFormContext, useFormState } from 'react-hook-form';
|
|
4
|
-
import { useTranslation } from 'react-i18next';
|
|
5
4
|
import {
|
|
6
5
|
StyleProp,
|
|
7
6
|
StyleSheet,
|
|
@@ -11,7 +10,7 @@ import {
|
|
|
11
10
|
ViewProps,
|
|
12
11
|
ViewStyle,
|
|
13
12
|
} from 'react-native';
|
|
14
|
-
import
|
|
13
|
+
import { DateTimePickerModal } from './date-time-picker.modal';
|
|
15
14
|
import {
|
|
16
15
|
Font,
|
|
17
16
|
fontSize,
|
|
@@ -23,7 +22,6 @@ import { ThemeColors, useThemedStyles } from '@src/core/theme';
|
|
|
23
22
|
import { Label } from '../ui';
|
|
24
23
|
import { SvgProps } from 'react-native-svg';
|
|
25
24
|
import { IconCalendar } from '@src/assets/svgs';
|
|
26
|
-
import { i18n } from '@src/app/config/translation';
|
|
27
25
|
|
|
28
26
|
type Props = ViewProps & {
|
|
29
27
|
containerStyle?: StyleProp<ViewStyle>;
|
|
@@ -99,7 +97,6 @@ const formatDate = (date: Date, yearOnly?: boolean) => {
|
|
|
99
97
|
};
|
|
100
98
|
|
|
101
99
|
const HFDateTime = (props: Props) => {
|
|
102
|
-
const { t } = useTranslation();
|
|
103
100
|
const styles = useThemedStyles(makeStyles);
|
|
104
101
|
const {
|
|
105
102
|
containerStyle = {},
|
|
@@ -220,16 +217,11 @@ const HFDateTime = (props: Props) => {
|
|
|
220
217
|
}}
|
|
221
218
|
/>
|
|
222
219
|
</View>
|
|
223
|
-
<
|
|
224
|
-
modal
|
|
220
|
+
<DateTimePickerModal
|
|
225
221
|
mode="date"
|
|
226
222
|
open={open}
|
|
227
223
|
date={date}
|
|
228
224
|
maximumDate={new Date()}
|
|
229
|
-
locale={i18n.language}
|
|
230
|
-
title={t('common.selectDate')}
|
|
231
|
-
confirmText={t('common.confirm')}
|
|
232
|
-
cancelText={t('common.cancel')}
|
|
233
225
|
onConfirm={handleConfirm}
|
|
234
226
|
onCancel={() => setOpen(false)}
|
|
235
227
|
/>
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
TouchableOpacity,
|
|
7
7
|
ViewStyle,
|
|
8
8
|
} from 'react-native';
|
|
9
|
-
import
|
|
9
|
+
import { DateTimePickerModal } from './date-time-picker.modal';
|
|
10
10
|
import { Font, fontSize, horizontalScale, verticalScale, Radius } from '../../utils';
|
|
11
11
|
import { ThemeColors, useThemedStyles } from '@src/core/theme';
|
|
12
12
|
import { Label } from '../ui';
|
|
@@ -80,8 +80,7 @@ const HFTimePicker = (props: Props) => {
|
|
|
80
80
|
value={field.value || placeholder}
|
|
81
81
|
/>
|
|
82
82
|
</TouchableOpacity>
|
|
83
|
-
<
|
|
84
|
-
modal
|
|
83
|
+
<DateTimePickerModal
|
|
85
84
|
mode="time"
|
|
86
85
|
open={open}
|
|
87
86
|
date={time}
|
package/templates/mobile/expo/src/core/components/screen/screen-container/screen-container.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import React, { PropsWithChildren, ReactNode } from 'react';
|
|
|
11
11
|
import {
|
|
12
12
|
Image,
|
|
13
13
|
ImageSourcePropType,
|
|
14
|
+
KeyboardAvoidingView,
|
|
14
15
|
NativeScrollEvent,
|
|
15
16
|
NativeSyntheticEvent,
|
|
16
17
|
StatusBar,
|
|
@@ -20,7 +21,6 @@ import {
|
|
|
20
21
|
ViewStyle,
|
|
21
22
|
} from 'react-native';
|
|
22
23
|
import { ScrollView } from 'react-native-gesture-handler';
|
|
23
|
-
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
|
|
24
24
|
import { Edge, SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
25
25
|
import { ImgScreenBgGradian } from '@src/assets/Images';
|
|
26
26
|
import Header from '../../ui/header/header';
|
|
@@ -43,15 +43,14 @@ interface Props {
|
|
|
43
43
|
/** Keyboard-aware scroll for forms. @default true */
|
|
44
44
|
isForm?: boolean;
|
|
45
45
|
/**
|
|
46
|
-
* Extra breathing space (px) kept
|
|
47
|
-
*
|
|
48
|
-
*
|
|
46
|
+
* Extra breathing space (px) kept above the keyboard while `isForm` — passed
|
|
47
|
+
* to KeyboardAvoidingView as `keyboardVerticalOffset`.
|
|
48
|
+
* @default Spacing.spacing_2xl (32)
|
|
49
49
|
*/
|
|
50
50
|
bottomOffset?: number;
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* (e.g. a translateY/height worklet) so the two don't fight. @default true
|
|
52
|
+
* @deprecated No longer used — kept for API compatibility. Keyboard handling
|
|
53
|
+
* now uses React Native's KeyboardAvoidingView (Expo Go compatible).
|
|
55
54
|
*/
|
|
56
55
|
keyboardAware?: boolean;
|
|
57
56
|
showHeader?: boolean;
|
|
@@ -91,7 +90,6 @@ const ScreenContainer = (props: PropsWithChildren<Props>) => {
|
|
|
91
90
|
background = resolved === 'dark' ? ImgScreenBgGradian : undefined,
|
|
92
91
|
isForm = true,
|
|
93
92
|
bottomOffset = Spacing.spacing_2xl, // 32
|
|
94
|
-
keyboardAware = true,
|
|
95
93
|
showHeader = true,
|
|
96
94
|
onScroll,
|
|
97
95
|
onContentSizeChange,
|
|
@@ -165,28 +163,26 @@ const ScreenContainer = (props: PropsWithChildren<Props>) => {
|
|
|
165
163
|
{renderHeader()}
|
|
166
164
|
|
|
167
165
|
{isForm ? (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// Reanimated) and keep `bottomOffset` of breathing room
|
|
176
|
-
// above the focused caret. `mode="layout"` makes the flex
|
|
177
|
-
// layout (topSpacer / marginTop:auto actions / gap) reflow
|
|
178
|
-
// around the keyboard space so inputs are never covered.
|
|
179
|
-
bottomOffset={bottomOffset}
|
|
180
|
-
mode="layout"
|
|
181
|
-
// When false, the screen drives its own keyboard animation
|
|
182
|
-
// (translateY/height worklet) and this view stops auto-scrolling.
|
|
183
|
-
enabled={keyboardAware}
|
|
184
|
-
keyboardDismissMode="interactive"
|
|
185
|
-
keyboardShouldPersistTaps="handled"
|
|
186
|
-
contentContainerStyle={[styles.content, style]}
|
|
166
|
+
// KeyboardAvoidingView (RN core) lifts the form above the
|
|
167
|
+
// keyboard — works in Expo Go without a custom dev build.
|
|
168
|
+
// `bottomOffset` keeps breathing room above the focused input.
|
|
169
|
+
<KeyboardAvoidingView
|
|
170
|
+
style={styles.flex}
|
|
171
|
+
behavior={isIOS ? 'padding' : undefined}
|
|
172
|
+
keyboardVerticalOffset={bottomOffset}
|
|
187
173
|
>
|
|
188
|
-
|
|
189
|
-
|
|
174
|
+
<ScrollView
|
|
175
|
+
showsVerticalScrollIndicator={false}
|
|
176
|
+
onScroll={onScroll}
|
|
177
|
+
scrollEventThrottle={16}
|
|
178
|
+
onContentSizeChange={onContentSizeChange}
|
|
179
|
+
keyboardDismissMode="interactive"
|
|
180
|
+
keyboardShouldPersistTaps="handled"
|
|
181
|
+
contentContainerStyle={[styles.content, style]}
|
|
182
|
+
>
|
|
183
|
+
{body}
|
|
184
|
+
</ScrollView>
|
|
185
|
+
</KeyboardAvoidingView>
|
|
190
186
|
) : (
|
|
191
187
|
// Non-form screens (e.g. the auth screens) render a plain View.
|
|
192
188
|
<View style={[styles.content, styles.flex, style]}>{body}</View>
|