Compare commits
9 Commits
amalia/22-
...
amalia/05-
| Author | SHA1 | Date | |
|---|---|---|---|
| bbacc2a5bd | |||
| 5ad7874a92 | |||
| a5e379cc7f | |||
| 36c2519fa0 | |||
| e2ffef1085 | |||
| cb2a57ee8e | |||
| f3b677f847 | |||
| 6ffe599ad0 | |||
| 4a92def490 |
43
CLAUDE.md
43
CLAUDE.md
@@ -26,47 +26,8 @@ bunx jest path/to/test.tsx --no-coverage
|
||||
|
||||
## Architecture
|
||||
|
||||
### Routing (Expo Router — file-based)
|
||||
|
||||
- `app/index.tsx` — Login/splash (public); OTP verification is handled inline via `components/auth/viewVerification.tsx` (not a separate route)
|
||||
- `app/(application)/` — All authenticated screens; Expo Router enforces auth guard here
|
||||
- Deep-link navigation is handled by `lib/pushToPage.ts`, which maps notification payloads to routes
|
||||
|
||||
### State Management (three layers)
|
||||
|
||||
1. **Context** (`providers/`) — Auth (token encryption/decryption via CryptoES.AES), Theme (light/dark, persisted to AsyncStorage), and React Query client
|
||||
2. **Redux Toolkit** (`lib/store.ts` + slices) — Feature-level state for CRUD operations. Slices follow a naming pattern: `*Slice.ts` for read state, `*Update.ts`/`*Create.ts` for mutation state
|
||||
3. **TanStack React Query** — All server data fetching; configured with 5-min stale time, 24-hour cache retention, 2 retries, and AsyncStorage persistence for offline support
|
||||
|
||||
### API Layer (`lib/api.ts`)
|
||||
|
||||
Single 773-line file defining 50+ Axios-based endpoints. The Axios instance reads `baseURL` from `Constants.expoConfig.extra.URL_API` (set in `.env` via `app.config.js`). Authentication uses Bearer tokens in headers. File uploads use `FormData` with `multipart/form-data`.
|
||||
|
||||
Three separate backend services are integrated:
|
||||
- **REST API** (axios) — main business logic
|
||||
- **WhatsApp server** — OTP delivery (separate token in `.env`)
|
||||
- **Firebase** — real-time database (`lib/firebaseDatabase.ts`) and push notifications (`lib/useNotification.ts`, `lib/registerForPushNotificationsAsync.ts`)
|
||||
|
||||
### Providers Initialization Order
|
||||
|
||||
`app/_layout.tsx` wraps the app in: `ErrorBoundary` → `NotifierWrapper` → `ThemeProvider` → `QueryProvider` → `AuthProvider` → navigation stack. Redux `store` is provided inside `app/(application)/_layout.tsx`, not at the root.
|
||||
|
||||
### Error Boundary
|
||||
|
||||
`components/ErrorBoundary.tsx` is a class component (required by React) wrapping the entire app. It uses React Native's built-in `Text` — **do not replace it with the custom `components/Text.tsx`** as that pulls in `ThemeProvider` → `AsyncStorage`, which breaks Jest tests.
|
||||
|
||||
Tests for ErrorBoundary live in `__tests__/ErrorBoundary-test.tsx` and use `@testing-library/react-native`.
|
||||
See @docs/ARCHITECTURE.md
|
||||
|
||||
## Key Conventions
|
||||
|
||||
**Imports:** Use `@/` alias (maps to project root, configured in `tsconfig.json`). Never use relative paths like `../../`.
|
||||
|
||||
**Utility functions:** Prefixed with `fun_` (e.g., `lib/fun_stringToDate.ts`, `lib/fun_validateName.ts`).
|
||||
|
||||
**Styling:** Use theme-aware colors from `useTheme()` hook. Global `StyleSheet` definitions live in `constants/Styles.ts`. Color tokens are in `constants/Colors.ts` with explicit `light`/`dark` variants.
|
||||
|
||||
**Component structure:** Feature-specific subdirectories under `components/` (e.g., `components/announcement/`) typically contain a header component alongside list/card components for that feature.
|
||||
|
||||
**Environment config:** All env vars are declared in `.env`, exposed through `app.config.js` `extra` field, and accessed via `Constants.expoConfig.extra.*` or the `constants/ConstEnv.ts` wrapper.
|
||||
|
||||
**EAS builds:** Profiles are `development`, `preview`, and `production` in `eas.json`. Production builds auto-increment the app version via the `bump` script.
|
||||
See @docs/CONVENTIONS.md
|
||||
|
||||
@@ -206,6 +206,8 @@ export default function TaskDivisionAddTask() {
|
||||
day_label: { color: colors.text },
|
||||
time_label: { color: colors.text },
|
||||
weekday_label: { color: colors.text },
|
||||
button_next_image: { tintColor: colors.text },
|
||||
button_prev_image: { tintColor: colors.text },
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -238,7 +240,7 @@ export default function TaskDivisionAddTask() {
|
||||
>
|
||||
<Text style={[dsbButton ? Styles.cGray : Styles.cWhite]}>Detail</Text>
|
||||
</Pressable> */}
|
||||
<ButtonSelect value="Detail" onPress={() => { setModalDetail(true) }} />
|
||||
<ButtonSelect value="Detail" onPress={() => { setModalDetail(true) }} disabled={from == "" || to == ""} />
|
||||
</View>
|
||||
<InputForm
|
||||
label="Judul Tugas"
|
||||
|
||||
@@ -177,6 +177,8 @@ export default function CreateTaskAddTugas() {
|
||||
day_label: { color: colors.text },
|
||||
time_label: { color: colors.text },
|
||||
weekday_label: { color: colors.text },
|
||||
button_next_image: { tintColor: colors.text },
|
||||
button_prev_image: { tintColor: colors.text },
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -209,7 +211,7 @@ export default function CreateTaskAddTugas() {
|
||||
>
|
||||
<Text style={[dsbButton ? Styles.cGray : Styles.cWhite]}>Detail</Text>
|
||||
</Pressable> */}
|
||||
<ButtonSelect value="Detail" onPress={() => { setModalDetail(true) }} />
|
||||
<ButtonSelect value="Detail" onPress={() => { setModalDetail(true) }} disabled={from == "" || to == ""} />
|
||||
</View>
|
||||
<InputForm
|
||||
label="Judul Tugas"
|
||||
|
||||
@@ -255,6 +255,8 @@ export default function UpdateProjectTaskDivision() {
|
||||
day_label: { color: colors.text },
|
||||
time_label: { color: colors.text },
|
||||
weekday_label: { color: colors.text },
|
||||
button_next_image: { tintColor: colors.text },
|
||||
button_prev_image: { tintColor: colors.text },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -290,7 +292,7 @@ export default function UpdateProjectTaskDivision() {
|
||||
>
|
||||
<Text style={[dsbButton ? Styles.cGray : Styles.cWhite]}>Detail</Text>
|
||||
</Pressable> */}
|
||||
<ButtonSelect value="Detail" onPress={() => { setModalDetail(true) }} />
|
||||
<ButtonSelect value="Detail" onPress={() => { setModalDetail(true) }} disabled={from == "" || to == ""} />
|
||||
</View>
|
||||
<InputForm
|
||||
label="Judul Tugas"
|
||||
|
||||
@@ -195,6 +195,8 @@ export default function ProjectAddTask() {
|
||||
day_label: { color: colors.text },
|
||||
time_label: { color: colors.text },
|
||||
weekday_label: { color: colors.text },
|
||||
button_next_image: { tintColor: colors.text },
|
||||
button_prev_image: { tintColor: colors.text },
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -227,7 +229,7 @@ export default function ProjectAddTask() {
|
||||
>
|
||||
<Text style={[dsbButton ? Styles.cGray : Styles.cWhite]}>Detail</Text>
|
||||
</Pressable> */}
|
||||
<ButtonSelect value="Detail" onPress={() => { setModalDetail(true) }} />
|
||||
<ButtonSelect value="Detail" onPress={() => { setModalDetail(true) }} disabled={from == "" || to == ""} />
|
||||
</View>
|
||||
<InputForm
|
||||
label="Judul Tugas"
|
||||
|
||||
@@ -179,6 +179,8 @@ export default function CreateProjectAddTask() {
|
||||
day_label: { color: colors.text },
|
||||
time_label: { color: colors.text },
|
||||
weekday_label: { color: colors.text },
|
||||
button_next_image: { tintColor: colors.text },
|
||||
button_prev_image: { tintColor: colors.text },
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -204,14 +206,7 @@ export default function CreateProjectAddTask() {
|
||||
{
|
||||
(error.endDate || error.startDate) && <Text style={[Styles.textInformation, Styles.mt05, { color: colors.error }]}>Tanggal tidak boleh kosong</Text>
|
||||
}
|
||||
{/* <Pressable
|
||||
style={[Styles.btnTab, Styles.btnLainnya, dsbButton && Styles.btnDisabled]}
|
||||
disabled={dsbButton}
|
||||
onPress={() => { setModalDetail(true) }}
|
||||
>
|
||||
<Text style={[dsbButton ? Styles.cGray : Styles.cWhite]}>Detail</Text>
|
||||
</Pressable> */}
|
||||
<ButtonSelect value="Detail" onPress={() => { setModalDetail(true) }} />
|
||||
<ButtonSelect value="Detail" onPress={() => { setModalDetail(true) }} disabled={from == "" || to == ""} />
|
||||
</View>
|
||||
<InputForm
|
||||
label="Judul Tugas"
|
||||
|
||||
@@ -229,6 +229,8 @@ export default function UpdateProjectTask() {
|
||||
day_label: { color: colors.text },
|
||||
time_label: { color: colors.text },
|
||||
weekday_label: { color: colors.text },
|
||||
button_next_image: { tintColor: colors.text },
|
||||
button_prev_image: { tintColor: colors.text },
|
||||
}}
|
||||
/>
|
||||
}
|
||||
@@ -259,7 +261,7 @@ export default function UpdateProjectTask() {
|
||||
>
|
||||
<Text style={[dsbButton ? Styles.cGray : Styles.cWhite]}>Detail</Text>
|
||||
</Pressable> */}
|
||||
<ButtonSelect value="Detail" onPress={() => { setModalDetail(true) }} />
|
||||
<ButtonSelect value="Detail" onPress={() => { setModalDetail(true) }} disabled={from == "" || to == ""} />
|
||||
</View>
|
||||
<InputForm
|
||||
label="Judul Tugas"
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useAuthSession } from "@/providers/AuthProvider";
|
||||
import { useTheme } from "@/providers/ThemeProvider";
|
||||
import { Feather, Ionicons } from "@expo/vector-icons";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import Constants from "expo-constants";
|
||||
import { router } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { AppState, AppStateStatus, Pressable, View } from "react-native";
|
||||
@@ -196,6 +197,10 @@ export default function ListSetting() {
|
||||
<ThemeOption label="Gelap" value="dark" icon="moon-outline" />
|
||||
<ThemeOption label="Sistem" value="system" icon="phone-portrait-outline" />
|
||||
</DrawerBottom>
|
||||
|
||||
<Text style={{ color: colors.icon, textAlign: 'center', marginTop: 'auto', fontSize: 12 }}>
|
||||
Versi {Constants.expoConfig?.version}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export default class ErrorBoundary extends Component<Props, State> {
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Terjadi Kesalahan</Text>
|
||||
<Text style={styles.message}>
|
||||
{this.state.error?.message ?? 'Kesalahan tidak diketahui'}
|
||||
Silahkan coba lagi beberapa saat lagi atau hubungi admin untuk bantuan.
|
||||
</Text>
|
||||
<TouchableOpacity style={styles.button} onPress={this.handleReset}>
|
||||
<Text style={styles.buttonText}>Coba Lagi</Text>
|
||||
|
||||
@@ -10,20 +10,22 @@ type Props = {
|
||||
round?: boolean
|
||||
error?: boolean
|
||||
errorText?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export default function ButtonSelect({ value, onPress, round, error, errorText }: Props) {
|
||||
export default function ButtonSelect({ value, onPress, round, error, errorText, disabled }: Props) {
|
||||
const { colors } = useTheme();
|
||||
return (
|
||||
<View style={[Styles.mv05]}>
|
||||
<Pressable onPress={onPress}>
|
||||
<Pressable onPress={onPress} disabled={disabled}>
|
||||
<View style={[
|
||||
Styles.inputRoundForm,
|
||||
Styles.inputRoundFormRight,
|
||||
round && Styles.round30,
|
||||
Styles.pv10,
|
||||
{ borderColor: colors.icon + '20', backgroundColor: colors.input },
|
||||
error && { borderColor: "red" }
|
||||
error && { borderColor: "red" },
|
||||
disabled && { opacity: 0.4 }
|
||||
]}>
|
||||
<Feather name="arrow-right-circle" size={20} color={colors.text} />
|
||||
<Text style={[{ color: colors.text }]}>{value}</Text>
|
||||
|
||||
@@ -30,6 +30,10 @@ export function InputForm({ label, value, placeholder, onChange, info, disable,
|
||||
const { colors } = useTheme();
|
||||
|
||||
if (itemLeft != undefined || itemRight != undefined) {
|
||||
const hasBothItems = itemLeft != undefined && itemRight != undefined;
|
||||
const baseInputWidth = width ? lebar * width / 100 : lebar * 0.78;
|
||||
// When both icons present, shrink TextInput by right icon size to keep container width stable
|
||||
const textInputWidth = hasBothItems ? baseInputWidth - 30 : baseInputWidth;
|
||||
return (
|
||||
<View style={[mb && Styles.mb10]}>
|
||||
{
|
||||
@@ -42,7 +46,7 @@ export function InputForm({ label, value, placeholder, onChange, info, disable,
|
||||
}
|
||||
<View style={[
|
||||
Styles.inputRoundForm,
|
||||
itemRight != undefined ? Styles.inputRoundFormRight : Styles.inputRoundFormLeft,
|
||||
hasBothItems ? Styles.inputRoundFormLeft : itemRight != undefined ? Styles.inputRoundFormRight : Styles.inputRoundFormLeft,
|
||||
multiline && { alignItems: 'flex-end' },
|
||||
round && Styles.round30,
|
||||
{
|
||||
@@ -50,12 +54,11 @@ export function InputForm({ label, value, placeholder, onChange, info, disable,
|
||||
},
|
||||
error ? { borderColor: colors.error } : { borderColor: colors.icon + '20' },
|
||||
Platform.OS == 'ios' ? { paddingVertical: 10 } : { paddingVertical: 0, minHeight: 40 },
|
||||
{ alignItems: 'center' },
|
||||
multiline
|
||||
? { alignItems: "flex-end" }
|
||||
: { alignItems: "center" },
|
||||
]}>
|
||||
{itemRight != undefined ? itemRight : itemLeft}
|
||||
{hasBothItems ? itemLeft : (itemRight != undefined ? itemRight : itemLeft)}
|
||||
<TextInput
|
||||
editable={!disable}
|
||||
value={value}
|
||||
@@ -68,10 +71,11 @@ export function InputForm({ label, value, placeholder, onChange, info, disable,
|
||||
style={[
|
||||
Styles.mh05,
|
||||
multiline && { height: '100%', maxHeight: 100 },
|
||||
{ width: width ? lebar * width / 100 : lebar * 0.78, color: colors.text },
|
||||
{ width: textInputWidth, color: colors.text },
|
||||
Platform.OS == 'ios' ? { paddingVertical: 1, paddingTop: 4 } : { paddingVertical: 0 },
|
||||
]}
|
||||
/>
|
||||
{hasBothItems && itemRight}
|
||||
</View>
|
||||
{error && (<Text style={[Styles.textInformation, { color: colors.error }, Styles.mt05]}>{errorText}</Text>)}
|
||||
{info != undefined && (<Text style={[Styles.textInformation, { color: colors.dimmed }, Styles.mt05]}>{info}</Text>)}
|
||||
|
||||
@@ -1,19 +1,42 @@
|
||||
import { useTheme } from "@/providers/ThemeProvider";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { useState } from "react";
|
||||
import { TouchableOpacity } from "react-native";
|
||||
import { InputForm } from "./inputForm";
|
||||
|
||||
export default function InputSearch({ onChange, width, value, bg }: { onChange?: (val: string) => void, width?: number, value?: string, bg?: string }) {
|
||||
const { colors } = useTheme();
|
||||
const [internalValue, setInternalValue] = useState(value ?? "");
|
||||
|
||||
const displayValue = value !== undefined ? value : internalValue;
|
||||
|
||||
const handleChange = (val: string) => {
|
||||
setInternalValue(val);
|
||||
onChange?.(val);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setInternalValue("");
|
||||
onChange?.("");
|
||||
};
|
||||
|
||||
return (
|
||||
<InputForm
|
||||
type="default"
|
||||
placeholder="Pencarian"
|
||||
round
|
||||
itemLeft={<Feather name="search" size={20} color={colors.dimmed} />}
|
||||
onChange={onChange}
|
||||
itemRight={
|
||||
displayValue ? (
|
||||
<TouchableOpacity onPress={handleClear}>
|
||||
<Feather name="x" size={20} color={colors.dimmed} />
|
||||
</TouchableOpacity>
|
||||
) : undefined
|
||||
}
|
||||
onChange={handleChange}
|
||||
width={width}
|
||||
bg={bg}
|
||||
value={value}
|
||||
value={displayValue}
|
||||
mb={false}
|
||||
/>
|
||||
)
|
||||
|
||||
32
docs/ARCHITECTURE.md
Normal file
32
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Architecture
|
||||
|
||||
## Routing (Expo Router — file-based)
|
||||
|
||||
- `app/index.tsx` — Login/splash (public); OTP verification is handled inline via `components/auth/viewVerification.tsx` (not a separate route)
|
||||
- `app/(application)/` — All authenticated screens; Expo Router enforces auth guard here
|
||||
- Deep-link navigation is handled by `lib/pushToPage.ts`, which maps notification payloads to routes
|
||||
|
||||
## State Management (three layers)
|
||||
|
||||
1. **Context** (`providers/`) — Auth (token encryption/decryption via CryptoES.AES), Theme (light/dark, persisted to AsyncStorage), and React Query client
|
||||
2. **Redux Toolkit** (`lib/store.ts` + slices) — Feature-level state for CRUD operations. Slices follow a naming pattern: `*Slice.ts` for read state, `*Update.ts`/`*Create.ts` for mutation state
|
||||
3. **TanStack React Query** — All server data fetching; configured with 5-min stale time, 24-hour cache retention, 2 retries, and AsyncStorage persistence for offline support
|
||||
|
||||
## API Layer (`lib/api.ts`)
|
||||
|
||||
Single file defining 50+ Axios-based endpoints. The Axios instance reads `baseURL` from `Constants.expoConfig.extra.URL_API` (set in `.env` via `app.config.js`). Authentication uses Bearer tokens in headers. File uploads use `FormData` with `multipart/form-data`.
|
||||
|
||||
Three separate backend services are integrated:
|
||||
- **REST API** (axios) — main business logic
|
||||
- **WhatsApp server** — OTP delivery (separate token in `.env`)
|
||||
- **Firebase** — real-time database (`lib/firebaseDatabase.ts`) and push notifications (`lib/useNotification.ts`, `lib/registerForPushNotificationsAsync.ts`)
|
||||
|
||||
## Providers Initialization Order
|
||||
|
||||
`app/_layout.tsx` wraps the app in: `ErrorBoundary` → `NotifierWrapper` → `ThemeProvider` → `QueryProvider` → `AuthProvider` → navigation stack. Redux `store` is provided inside `app/(application)/_layout.tsx`, not at the root.
|
||||
|
||||
## Error Boundary
|
||||
|
||||
`components/ErrorBoundary.tsx` is a class component (required by React) wrapping the entire app. It uses React Native's built-in `Text` — **do not replace it with the custom `components/Text.tsx`** as that pulls in `ThemeProvider` → `AsyncStorage`, which breaks Jest tests.
|
||||
|
||||
Tests for ErrorBoundary live in `__tests__/ErrorBoundary-test.tsx` and use `@testing-library/react-native`.
|
||||
13
docs/CONVENTIONS.md
Normal file
13
docs/CONVENTIONS.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Key Conventions
|
||||
|
||||
**Imports:** Use `@/` alias (maps to project root, configured in `tsconfig.json`). Never use relative paths like `../../`.
|
||||
|
||||
**Utility functions:** Prefixed with `fun_` (e.g., `lib/fun_stringToDate.ts`, `lib/fun_validateName.ts`).
|
||||
|
||||
**Styling:** Use theme-aware colors from `useTheme()` hook. Global `StyleSheet` definitions live in `constants/Styles.ts`. Color tokens are in `constants/Colors.ts` with explicit `light`/`dark` variants.
|
||||
|
||||
**Component structure:** Feature-specific subdirectories under `components/` (e.g., `components/announcement/`) typically contain a header component alongside list/card components for that feature.
|
||||
|
||||
**Environment config:** All env vars are declared in `.env`, exposed through `app.config.js` `extra` field, and accessed via `Constants.expoConfig.extra.*` or the `constants/ConstEnv.ts` wrapper.
|
||||
|
||||
**EAS builds:** Profiles are `development`, `preview`, and `production` in `eas.json`. Production builds auto-increment the app version via the `bump` script.
|
||||
Reference in New Issue
Block a user