Files
hipmi-mobile/CLAUDE.md
bagasbanuna d5634c1f67 chore: reorganize docs folder + add CLAUDE.md
- Kelompokkan docs/ ke subfolder: architecture/, testing/, notes/, ai/, tasks/
- Pindahkan tasks/ (root) ke docs/tasks/
- Tambah docs/README.md sebagai index navigasi
- Tambah CLAUDE.md (project instructions)
- Hapus .qwen/settings.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 12:10:27 +08:00

7.1 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.


Project

HIPMI Badung Connect — Cross-platform mobile app (iOS, Android, Web) for HIPMI (Himpunan Pengusaha Muda Indonesia) chapter Badung members.

  • Framework: Expo v54 + React Native v0.81.5, TypeScript strict
  • Routing: Expo Router v6 (file-based)
  • Package manager: Bun

Commands

bun install                  # install dependencies
bunx expo start              # dev server
bunx expo start --ios        # iOS simulator
bunx expo start --android    # Android emulator
bunx expo start -c           # clear cache
bun run lint                 # expo lint

# Build (EAS)
eas build --profile production
eas build --profile preview
eas build --profile development

# Troubleshooting
rm -rf node_modules && bun install
bunx expo start --clear

Architecture

Route → Screen separation (strict)

app/ contains route files only (max 5 lines). All business logic lives in screens/.

// app/(application)/admin/feature/screen-name.tsx
import { Admin_ScreenXXX } from "@/screens/Admin/Feature/ScreenXXX";
export default function AdminScreenXXX() { return <Admin_ScreenXXX />; }
// screens/Admin/Feature/ScreenXXX.tsx — ALL logic here
export function Admin_ScreenXXX() {
  // hooks, state, handlers, render
  return <OS_Wrapper ... />;
}

Naming conventions

Item Convention Example
Admin screen Admin_ScreenXXX Admin_ScreenDonationList
User screen PascalCase ScreenDonationDetail
Admin card Admin_BoxXXX Admin_BoxDonation
Path alias @/* @/components/...

Key Components

OS_Wrapper — always use as root wrapper

Automatically selects iOS/Android layout. Two modes:

// List screen
<OS_Wrapper
  listData={pagination.listData}
  renderItem={renderItem}
  headerComponent={headerComponent}
  ListEmptyComponent={ListEmptyComponent}
  ListFooterComponent={ListFooterComponent}
  onEndReached={pagination.loadMore}
  refreshControl={<RefreshControl refreshing={pagination.refreshing} onRefresh={pagination.onRefresh} />}
/>

// Form screen (with TextInput / TextArea)
<OS_Wrapper enableKeyboardHandling contentPaddingBottom={250}>
  <FormContent />
</OS_Wrapper>
  • contentPaddingBottom={100} — default for list screens
  • contentPaddingBottom={250}only for screens with TextInput/TextArea
  • enableKeyboardHandling — Android keyboard auto-scroll (ignored on iOS)

AdminBasicBox + GridSpan_4_8

<AdminBasicBox onPress={() => router.push(`/path/${item.id}`)} style={{ marginHorizontal: 10, marginVertical: 5 }}>
  <StackCustom gap={0}>
    <GridSpan_4_8 label="Nama" value={<TextCustom>{item.name}</TextCustom>} />
  </StackCustom>
</AdminBasicBox>

Pagination Pattern

import { usePagination } from "@/hooks/use-pagination";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";

const pagination = usePagination({
  fetchFunction: async (page, searchQuery) => {
    const response = await apiXXX({ page: String(page) });
    if (response.success) return { data: response.data };
    return { data: [] };
  },
  pageSize: PAGINATION_DEFAULT_TAKE, // 10
  searchQuery: search,
  dependencies: [dependency],
});

const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
  loading: pagination.loading,
  refreshing: pagination.refreshing,
  listData: pagination.listData,
  emptyMessage: "Belum ada data",
  skeletonCount: PAGINATION_DEFAULT_TAKE,
});

usePagination returns: listData, loading, refreshing, hasMore, page, onRefresh, loadMore, reset, setListData, isInitialLoad


API Service Pattern

Always use apiConfig (not axios directly) — auth token injected automatically via request interceptor from AsyncStorage.getItem("authToken").

// service/api-admin/api-xxx.ts
export async function apiXXX({ page = "1" }: { page?: string }) {
  try {
    const response = await apiConfig.get(`/mobile/admin/xxx?page=${page}`);
    return response.data;
  } catch (error) {
    throw error;
  }
}
  • All list endpoints require page param (default "1" as string)
  • Response shape: { success: boolean, data: T[], message?: string }
  • Admin APIs: service/api-admin/
  • Client APIs: service/api-client/

Authentication

// context/AuthContext.tsx — access via useAuth()
const { user, token, isAdmin, isUserActive, loginWithNomor, validateOtp, logout } = useAuth();

// Role check
const isAdmin = user?.masterUserRoleId !== "1";  // "1" = regular user

// Post-login routing
// active user   → /(application)/(user)/home
// inactive user → /(application)/(user)/waiting-room

AsyncStorage keys: "authToken", "userData"


File Upload

import { uploadFileService, deleteFileService } from "@/service/upload-service";
import { API_IMAGE } from "@/constants/api-storage";

const result = await uploadFileService({ dirId: "folder-name", imageUri: localUri });
// result.data.id → save this ID to backend

await deleteFileService({ id: fileId });

const imageUrl = API_IMAGE.GET({ fileId: "xxx", size: 200 }); // wibu-storage CDN

Constants (constants/constans-value.ts)

PADDING_INLINE = 16        // use selectively, not on every screen by default
PADDING_EXTRA_SMALL = 10
PADDING_SMALL = 12
PADDING_MEDIUM = 16
PADDING_LARGE = 20

OS_ANDROID_HEIGHT = 60 / OS_IOS_HEIGHT = 80
OS_ANDROID_PADDING_TOP = 6 / OS_IOS_PADDING_TOP = 12

TEXT_SIZE_SMALL = 12 / MEDIUM = 14 / LARGE = 16 / XLARGE = 18
PAGINATION_DEFAULT_TAKE = 10
DRAWER_HEIGHT = 500
RADIUS_BUTTON = 50
// constants/color-palet.ts
MainColor.darkblue = "#001D3D"   // user app primary
AdminColor.bgAdmin = "#182c47"   // admin panel background

Known Issues

iOS Maplibre crash — "Attempt to recycle a mounted view"

Never conditionally render PointAnnotation. Control visibility via opacity instead:

// ✅ correct
<PointAnnotation coordinate={coordinate}>
  <View style={{ opacity: selectedLocation ? 1 : 0 }}>
    <SelectedLocationMarker />
  </View>
</PointAnnotation>

// ❌ crashes iOS
{selectedLocation && <PointAnnotation ... />}

Known Typos — Do NOT fix unless asked

Path Typo
constants/constans-value.ts constans
constants/base-url-api-strorage.ts strorage
screens/Invesment/ Invesment
screens/Portofolio/ Portofolio
screens/UserSeach/ UserSeach

Environment Variables (.env)

API_BASE_URL=https://your-api-url.com
BASE_URL=https://your-app-url.com
DEEP_LINK_URL=hipmimobile://

Accessed via: Constants.expoConfig?.extra?.API_BASE_URL


New Screen Checklist

  1. service/api-admin/api-xxx.ts — API function with page param
  2. screens/Admin/Feature/ScreenXXX.tsx — screen with usePagination + OS_Wrapper
  3. screens/Admin/Feature/BoxXXX.tsx — card component (optional)
  4. app/(application)/admin/feature/index.tsx — thin route file