๐ Overview
In this part, we'll set up our Expo project with:
- Expo Router for navigation
- NativeWind (Tailwind CSS) for styling
- Folder structure for scalable development
- Base API configuration
๐ Step 1: Create Expo Project
# Create new Expo project with tabs template
npx create-expo-app@latest lampung-dev-mobile --template tabs
# Navigate to project
cd lampung-dev-mobile
๐ฆ Step 2: Install Dependencies
# Core dependencies
npm install axios zustand
# Expo packages
npx expo install expo-secure-store expo-camera
# Styling (NativeWind - Tailwind for React Native)
npm install nativewind
npm install --save-dev [email protected]
# Icons
npm install lucide-react-native
npm install react-native-svg
๐จ Step 3: Configure NativeWind
A. Initialize Tailwind
npx tailwindcss init
B. Configure tailwind.config.js
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,jsx,ts,tsx}",
"./components/**/*.{js,jsx,ts,tsx}"
],
presets: [require("nativewind/preset")],
theme: {
extend: {
colors: {
primary: '#f0880a',
secondary: '#1a1a1a',
},
},
},
plugins: [],
}
C. Create global.css
/* global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
D. Update babel.config.js
// babel.config.js
module.exports = function (api) {
api.cache(true);
return {
presets: [
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
};
};
E. Create nativewind-env.d.ts
// nativewind-env.d.ts
/// <reference types="nativewind/types" />
F. Update metro.config.js
// metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require('nativewind/metro');
const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, { input: './global.css' });
๐ Step 4: Create Folder Structure
# Create folders
mkdir -p services store components constants utils
Final structure:
lampung-dev-mobile/
โโโ app/ # Screens (Expo Router)
โ โโโ (tabs)/ # Tab navigation
โ โโโ event/ # Event screens
โ โโโ login.tsx # Login screen
โ โโโ _layout.tsx # Root layout
โ
โโโ components/ # Reusable components
โโโ services/ # API services
โโโ store/ # State management
โโโ constants/ # App constants
โโโ utils/ # Utilities
โโโ assets/ # Images, fonts
๐ง Step 5: Configure app.json
{
"expo": {
"name": "Lampung Dev",
"slug": "lampung-dev",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "lampungdev",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/images/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#1a1a1a"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "org.lampungdev.mobile"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#1a1a1a"
},
"package": "org.lampungdev.mobile",
"permissions": ["CAMERA"]
},
"plugins": [
"expo-router",
[
"expo-camera",
{
"cameraPermission": "Allow $(PRODUCT_NAME) to access camera for QR scanning"
}
]
]
}
}
๐ Step 6: Setup API Service
A. Create Base API
File: services/api.ts
import axios from 'axios';
import * as SecureStore from 'expo-secure-store';
// Development: Use your local IP
// Production: Use your API domain
export const BASE_URL = __DEV__
? 'http://192.168.1.100:3000' // Replace with your IP
: 'https://api.lampungdev.org';
export const API_BASE_URL = `${BASE_URL}/api`;
export const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
timeout: 10000, // 10 seconds timeout
});
// Request interceptor - add auth token
api.interceptors.request.use(async (config) => {
try {
const token = await SecureStore.getItemAsync('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
} catch (error) {
console.error('Error getting token:', error);
}
return config;
});
// Response interceptor - handle errors
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Token expired - clear auth
await SecureStore.deleteItemAsync('auth_token');
await SecureStore.deleteItemAsync('auth_user');
}
return Promise.reject(error);
}
);
B. Create Auth Service
File: services/auth.ts
import { api } from './api';
interface LoginResponse {
success: boolean;
data?: {
token: string;
user: {
id: string;
name: string;
email: string;
role: string;
picture?: string;
};
};
error?: string;
}
export const loginAdmin = async (
email: string,
password: string
): Promise<LoginResponse> => {
try {
const response = await api.post('/admin/auth', { email, password });
return response.data;
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || 'Login failed',
};
}
};
C. Create Events Service
File: services/events.ts
import { api } from './api';
export interface Event {
id: string;
title: string;
description: string;
startDate: string;
endDate: string;
location: string;
locationMapUrl?: string;
imageUrl?: string;
status: 'DRAFT' | 'UPCOMING' | 'ONGOING' | 'FINISHED';
_count?: {
registrations: number;
attendances: number;
};
}
export const getAdminEvents = async (): Promise<Event[]> => {
const response = await api.get('/admin/events');
return response.data.events || [];
};
export const getEventDetail = async (id: string): Promise<Event> => {
const response = await api.get(`/admin/events/${id}`);
return response.data;
};
๐งช Step 7: Test Setup
Start the development server:
npx expo start
If everything is configured correctly:
- No errors in terminal
- QR code displayed
- App opens on Expo Go
โ Checklist
- Expo project created with tabs template
- Dependencies installed (axios, zustand, expo-secure-store, expo-camera)
- NativeWind configured
- Folder structure created
- app.json configured
- Base API service created
- Auth and Events services created
๐ Next Steps
In Part 4, we'll implement the authentication system with Zustand store, login screen, and auth guard.
โ Part 2: Installation | Next: Authentication โ
This series is created to support the Casual Meetup #15 at LampungDev.
