create-ern-boilerplate 0.0.4 → 0.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ern-boilerplate",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Expo React Native boilerplate generator",
5
5
  "bin": {
6
6
  "create-ern-boilerplate": "./create.js",
@@ -29,11 +29,15 @@ import {
29
29
  import { useTheme } from '@hooks/useTheme';
30
30
  import { userService } from '@/services/userServices';
31
31
  import { CreateUserInput, UpdateUserInput, UserData, UserFilters } from '@/types/user.types';
32
+ import { SafeAreaView } from 'react-native-safe-area-context';
33
+ import { AppHeader } from '@/components/header/AppHeader';
34
+ import { useRouter } from 'expo-router';
32
35
 
33
36
  const ITEMS_PER_PAGE = 10;
34
37
 
35
38
  export default function UserManagementScreen() {
36
39
  const { colors } = useTheme();
40
+ const router = useRouter();
37
41
  const [searchQuery, setSearchQuery] = useState('');
38
42
  const [filters, setFilters] = useState<UserFilters>({
39
43
  page: 1,
@@ -571,14 +575,15 @@ export default function UserManagementScreen() {
571
575
  }
572
576
 
573
577
  return (
574
- <View style={[styles.container, { backgroundColor: colors.background }]}>
578
+ <SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
575
579
  {/* Header */}
576
- <View style={styles.header}>
577
- <Text style={[styles.headerTitle, { color: colors.text }]}>User Management</Text>
578
- <Text style={[styles.headerSubtitle, { color: colors.textSecondary }]}>
579
- {users.length} {users.length === 1 ? 'user' : 'users'} • Manage users and permissions
580
- </Text>
581
- </View>
580
+ <AppHeader
581
+ variant="back"
582
+ title="User Management"
583
+ subtitle={`${users.length} ${users.length === 1 ? 'user' : 'users'} • Manage users and permissions`}
584
+ onBackPress={() => router.back()}
585
+ colors={colors}
586
+ />
582
587
 
583
588
  {/* Search and Actions */}
584
589
  <View style={styles.searchSection}>
@@ -892,7 +897,7 @@ export default function UserManagementScreen() {
892
897
  </View>
893
898
  </UserModal>
894
899
  )}
895
- </View>
900
+ </SafeAreaView>
896
901
  );
897
902
  }
898
903
 
@@ -2,12 +2,14 @@ import React from 'react';
2
2
  import { View, Text, TouchableOpacity, StyleSheet, Platform } from 'react-native';
3
3
  import { Tabs, Redirect } from 'expo-router';
4
4
  import { Home, User, Settings } from 'lucide-react-native';
5
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
6
  import { useTheme } from '@/hooks/useTheme';
6
7
  import { useAuth } from '@/hooks/useAuth';
7
8
 
8
9
  export default function MainLayout() {
9
10
  const { colors } = useTheme();
10
11
  const { user, isAuthenticated } = useAuth();
12
+ const insets = useSafeAreaInsets();
11
13
 
12
14
  // Redirect to login if not authenticated
13
15
  if (!isAuthenticated) {
@@ -25,18 +27,23 @@ export default function MainLayout() {
25
27
  backgroundColor: colors.card || colors.background,
26
28
  borderTopColor: colors.border,
27
29
  borderTopWidth: 1,
28
- height: Platform.OS === 'ios' ? 88 : 65,
29
- paddingBottom: Platform.OS === 'ios' ? 28 : 8,
30
- paddingTop: 8,
30
+ height: Platform.OS === 'ios'
31
+ ? 50 + insets.bottom
32
+ : 50 + (insets.bottom > 0 ? insets.bottom : 8),
33
+ paddingBottom: insets.bottom > 0 ? insets.bottom : 8,
34
+ paddingTop: 0,
31
35
  elevation: 0,
32
36
  shadowOpacity: 0,
33
37
  },
34
38
  tabBarActiveTintColor: colors.primary,
35
39
  tabBarInactiveTintColor: colors.textSecondary,
36
40
  tabBarLabelStyle: {
37
- fontSize: 12,
41
+ fontSize: 10,
38
42
  fontWeight: '600',
39
- marginTop: 4,
43
+ marginTop: 2,
44
+ },
45
+ tabBarItemStyle: {
46
+ paddingVertical: 2,
40
47
  },
41
48
  }}
42
49
  >
@@ -70,7 +77,7 @@ export default function MainLayout() {
70
77
  }}
71
78
  />
72
79
 
73
- {/* {isAdmin && ( */}
80
+ {isAdmin && (
74
81
  <Tabs.Screen
75
82
  name="settings"
76
83
  options={{
@@ -83,19 +90,18 @@ export default function MainLayout() {
83
90
  <Settings size={24} color={color} strokeWidth={focused ? 2.5 : 2} />
84
91
  </View>
85
92
  ),
86
- // Hide settings tab if not admin
87
- // href: isAdmin ? '/(protected)/settings' : null,
88
93
  }}
89
94
  />
95
+ )}
90
96
  </Tabs>
91
97
  );
92
98
  }
93
99
 
94
100
  const styles = StyleSheet.create({
95
101
  iconContainer: {
96
- width: 48,
97
- height: 32,
98
- borderRadius: 12,
102
+ width: 40,
103
+ height: 26,
104
+ borderRadius: 8,
99
105
  justifyContent: 'center',
100
106
  alignItems: 'center',
101
107
  },
@@ -14,6 +14,10 @@ import {
14
14
  import { Search, Filter, Heart, Eye, Calendar, ChevronLeft, ChevronRight } from 'lucide-react-native';
15
15
  import { useTheme } from '@/hooks/useTheme';
16
16
  import { useAuth } from '@/hooks/useAuth';
17
+ // import { AppHeader } from '@/components/layout/AppHeader';
18
+ import { useRouter } from 'expo-router';
19
+ import { SafeAreaView } from 'react-native-safe-area-context';
20
+ import { AppHeader } from '@/components/header/AppHeader';
17
21
 
18
22
  const { width } = Dimensions.get('window');
19
23
  const ITEMS_PER_PAGE = 6;
@@ -45,6 +49,7 @@ const categories = [
45
49
  export default function HomeScreen() {
46
50
  const { colors } = useTheme();
47
51
  const { user } = useAuth();
52
+ const router = useRouter();
48
53
  const [newsList, setNewsList] = useState<News[]>([]);
49
54
  const [filteredNews, setFilteredNews] = useState<News[]>([]);
50
55
  const [loading, setLoading] = useState(true);
@@ -219,10 +224,10 @@ export default function HomeScreen() {
219
224
 
220
225
  const formatDate = (dateString: string) => {
221
226
  const date = new Date(dateString);
222
- return date.toLocaleDateString('en-US', {
223
- month: 'short',
224
- day: 'numeric',
225
- year: 'numeric'
227
+ return date.toLocaleDateString('en-US', {
228
+ month: 'short',
229
+ day: 'numeric',
230
+ year: 'numeric'
226
231
  });
227
232
  };
228
233
 
@@ -245,6 +250,10 @@ export default function HomeScreen() {
245
250
  }
246
251
  };
247
252
 
253
+ const handleProfilePress = () => {
254
+ router.push('/(protected)/profile')
255
+ };
256
+
248
257
  if (loading) {
249
258
  return (
250
259
  <View style={[styles.loadingContainer, { backgroundColor: colors.background }]}>
@@ -255,41 +264,23 @@ export default function HomeScreen() {
255
264
  }
256
265
 
257
266
  return (
258
- <View style={[styles.container, { backgroundColor: colors.background }]}>
267
+ <SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
268
+ {/* Header */}
269
+ <AppHeader
270
+ variant="search"
271
+ searchQuery={searchQuery}
272
+ onSearchChange={setSearchQuery}
273
+ searchPlaceholder="Search..."
274
+ user={{ ...user }} // Pass user object langsung
275
+ onProfilePress={handleProfilePress}
276
+ colors={colors}
277
+ />
259
278
  <ScrollView
260
279
  showsVerticalScrollIndicator={false}
261
280
  refreshControl={
262
281
  <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
263
282
  }
264
283
  >
265
- {/* Header */}
266
- <View style={styles.header}>
267
- <View>
268
- <Text style={[styles.greeting, { color: colors.textSecondary }]}>
269
- Welcome back,
270
- </Text>
271
- <Text style={[styles.username, { color: colors.text }]}>
272
- {user?.name || 'User'}
273
- </Text>
274
- </View>
275
- </View>
276
-
277
- {/* Search Bar */}
278
- <View style={[styles.searchContainer, { backgroundColor: `${colors.primary}08` }]}>
279
- <Search size={20} color={colors.textSecondary} />
280
- <TextInput
281
- style={[styles.searchInput, { color: colors.text }]}
282
- placeholder="Search news..."
283
- placeholderTextColor={colors.textSecondary}
284
- value={searchQuery}
285
- onChangeText={setSearchQuery}
286
- />
287
- {searchQuery.length > 0 && (
288
- <TouchableOpacity onPress={() => setSearchQuery('')}>
289
- <Text style={{ color: colors.primary, fontWeight: '600' }}>Clear</Text>
290
- </TouchableOpacity>
291
- )}
292
- </View>
293
284
 
294
285
  {/* Category Filter */}
295
286
  <ScrollView
@@ -346,7 +337,7 @@ export default function HomeScreen() {
346
337
  {currentNews.map((news) => (
347
338
  <TouchableOpacity
348
339
  key={news.id}
349
- style={[styles.newsCard, {
340
+ style={[styles.newsCard, {
350
341
  backgroundColor: colors.card || colors.background,
351
342
  shadowColor: colors.text,
352
343
  }]}
@@ -364,7 +355,7 @@ export default function HomeScreen() {
364
355
  <Text style={[styles.newsExcerpt, { color: colors.textSecondary }]} numberOfLines={2}>
365
356
  {news.excerpt}
366
357
  </Text>
367
-
358
+
368
359
  <View style={styles.newsFooter}>
369
360
  <View style={styles.newsStats}>
370
361
  <View style={styles.statItem}>
@@ -459,7 +450,7 @@ export default function HomeScreen() {
459
450
 
460
451
  <View style={{ height: 20 }} />
461
452
  </ScrollView>
462
- </View>
453
+ </SafeAreaView>
463
454
  );
464
455
  }
465
456
 
@@ -13,10 +13,14 @@ import { User, Mail, Phone, MapPin, Calendar, Edit2, Save, X } from 'lucide-reac
13
13
  import { useTheme } from '@/hooks/useTheme';
14
14
  import { useAuth } from '@/hooks/useAuth';
15
15
  import { Button } from '@/components/common/Button';
16
+ import { SafeAreaView } from 'react-native-safe-area-context';
17
+ import { AppHeader } from '@/components/header/AppHeader';
18
+ import { useRouter } from 'expo-router';
16
19
 
17
20
  export default function ProfileScreen() {
18
21
  const { colors } = useTheme();
19
22
  const { user } = useAuth();
23
+ const router = useRouter();
20
24
  const [isEditing, setIsEditing] = useState(false);
21
25
  const [loading, setLoading] = useState(false);
22
26
 
@@ -30,6 +34,7 @@ export default function ProfileScreen() {
30
34
  joinDate: 'January 2024',
31
35
  });
32
36
 
37
+
33
38
  const handleSave = async () => {
34
39
  setLoading(true);
35
40
  try {
@@ -79,8 +84,17 @@ export default function ProfileScreen() {
79
84
  );
80
85
 
81
86
  return (
82
- <View style={[styles.container, { backgroundColor: colors.background }]}>
87
+ <SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
88
+ <AppHeader
89
+ variant="back"
90
+ title="Profile"
91
+ subtitle="Manage your personal information"
92
+ onBackPress={() => router.back()}
93
+ colors={colors}
94
+ />
83
95
  <ScrollView showsVerticalScrollIndicator={false}>
96
+
97
+
84
98
  {/* Header with Avatar */}
85
99
  <View style={styles.header}>
86
100
  <View style={[styles.avatarContainer, { backgroundColor: colors.primary }]}>
@@ -140,7 +154,7 @@ export default function ProfileScreen() {
140
154
  {/* Profile Information */}
141
155
  <View style={styles.section}>
142
156
  <Text style={[styles.sectionTitle, { color: colors.text }]}>Profile Information</Text>
143
-
157
+
144
158
  <InfoCard icon={User} label="Name" value={profileData.name} editable />
145
159
  <InfoCard icon={Mail} label="Email" value={profileData.email} editable />
146
160
  <InfoCard icon={Phone} label="Phone" value={profileData.phone} editable />
@@ -171,7 +185,7 @@ export default function ProfileScreen() {
171
185
 
172
186
  <View style={{ height: 40 }} />
173
187
  </ScrollView>
174
- </View>
188
+ </SafeAreaView>
175
189
  );
176
190
  }
177
191
 
@@ -23,6 +23,8 @@ import {
23
23
  import { useTheme } from '@hooks/useTheme';
24
24
  import { useAuth } from '@hooks/useAuth';
25
25
  import { useRouter } from 'expo-router';
26
+ import { AppHeader } from '@/components/header/AppHeader';
27
+ import { SafeAreaView } from 'react-native-safe-area-context';
26
28
 
27
29
  export default function SettingsScreen() {
28
30
  const { colors, theme, themeMode, setThemeMode } = useTheme();
@@ -74,7 +76,7 @@ export default function SettingsScreen() {
74
76
  const SettingSection = ({ title, children }: any) => (
75
77
  <View style={styles.section}>
76
78
  <Text style={[styles.sectionTitle, { color: colors.textSecondary }]}>{title}</Text>
77
- <View style={[styles.sectionContent, {
79
+ <View style={[styles.sectionContent, {
78
80
  backgroundColor: colors.card || colors.background,
79
81
  shadowColor: colors.text,
80
82
  }]}>
@@ -112,15 +114,17 @@ export default function SettingsScreen() {
112
114
  );
113
115
 
114
116
  return (
115
- <View style={[styles.container, { backgroundColor: colors.background }]}>
116
- <ScrollView showsVerticalScrollIndicator={false}>
117
- {/* Header */}
118
- <View style={styles.header}>
119
- <Text style={[styles.headerTitle, { color: colors.text }]}>Settings</Text>
120
- <Text style={[styles.headerSubtitle, { color: colors.textSecondary }]}>
121
- Manage your account and preferences
122
- </Text>
123
- </View>
117
+ <SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
118
+
119
+ <AppHeader
120
+ variant="back"
121
+ title="Settings"
122
+ subtitle="Manage your account and preferences"
123
+ onBackPress={() => router.back()}
124
+ colors={colors}
125
+ />
126
+
127
+ <ScrollView style={styles.scroll} showsVerticalScrollIndicator={false}>
124
128
 
125
129
  {/* Appearance */}
126
130
  <SettingSection title="APPEARANCE">
@@ -229,7 +233,7 @@ export default function SettingsScreen() {
229
233
 
230
234
  <View style={{ height: 40 }} />
231
235
  </ScrollView>
232
- </View>
236
+ </SafeAreaView>
233
237
  );
234
238
  }
235
239
 
@@ -237,9 +241,13 @@ const styles = StyleSheet.create({
237
241
  container: {
238
242
  flex: 1,
239
243
  },
244
+ scroll: {
245
+ paddingTop: 16,
246
+ paddingBottom: 24,
247
+ },
240
248
  header: {
241
249
  paddingHorizontal: 24,
242
- paddingTop: 60,
250
+ paddingTop: 16,
243
251
  paddingBottom: 24,
244
252
  },
245
253
  headerTitle: {
@@ -0,0 +1,160 @@
1
+ // components/AppHeader.tsx
2
+ import React from 'react';
3
+ import { View, TextInput, TouchableOpacity, Image, StyleSheet, Text } from 'react-native';
4
+ import { Search, X, ArrowLeft } from 'lucide-react-native';
5
+
6
+ interface AppHeaderProps {
7
+ variant?: 'search' | 'back';
8
+ searchQuery?: string;
9
+ onSearchChange?: (query: string) => void;
10
+ searchPlaceholder?: string;
11
+ title?: string;
12
+ subtitle?: string; // Tambahkan subtitle
13
+ onBackPress?: () => void;
14
+ user?: {
15
+ name?: string;
16
+ avatar?: string;
17
+ avatarId?: string;
18
+ };
19
+ onProfilePress?: () => void;
20
+ colors: {
21
+ primary: string;
22
+ text: string;
23
+ textSecondary: string;
24
+ background: string;
25
+ };
26
+ }
27
+
28
+ export const AppHeader: React.FC<AppHeaderProps> = ({
29
+ variant = 'search',
30
+ searchQuery = '',
31
+ onSearchChange,
32
+ searchPlaceholder = 'Search...',
33
+ title,
34
+ subtitle,
35
+ onBackPress,
36
+ user,
37
+ onProfilePress,
38
+ colors,
39
+ }) => {
40
+ const avatarUrl = user?.avatar || `https://i.pravatar.cc/150?img=${user?.avatarId || 1}`;
41
+
42
+ // Render header dengan search dan profile
43
+ if (variant === 'search') {
44
+ return (
45
+ <View style={styles.header}>
46
+ <View style={[styles.searchContainer, { backgroundColor: `${colors.primary}08` }]}>
47
+ <Search size={20} color={colors.textSecondary} />
48
+ <TextInput
49
+ style={[styles.searchInput, { color: colors.text }]}
50
+ placeholder={searchPlaceholder}
51
+ placeholderTextColor={colors.textSecondary}
52
+ value={searchQuery}
53
+ onChangeText={onSearchChange}
54
+ />
55
+ {searchQuery.length > 0 && (
56
+ <TouchableOpacity onPress={() => onSearchChange?.('')}>
57
+ <X size={18} color={colors.textSecondary} />
58
+ </TouchableOpacity>
59
+ )}
60
+ </View>
61
+
62
+ <TouchableOpacity
63
+ style={styles.profileContainer}
64
+ onPress={onProfilePress}
65
+ activeOpacity={0.7}
66
+ >
67
+ <Image
68
+ source={{ uri: avatarUrl }}
69
+ style={styles.profileImage}
70
+ />
71
+ </TouchableOpacity>
72
+ </View>
73
+ );
74
+ }
75
+
76
+ // Render header dengan back button, title, dan subtitle
77
+ return (
78
+ <View style={styles.backHeader}>
79
+ <TouchableOpacity
80
+ onPress={onBackPress}
81
+ style={styles.backButton}
82
+ activeOpacity={0.7}
83
+ >
84
+ <ArrowLeft size={24} color={colors.text} />
85
+ </TouchableOpacity>
86
+
87
+ <View style={styles.headerTextContainer}>
88
+ <Text style={[styles.headerTitle, { color: colors.text }]}>
89
+ {title}
90
+ </Text>
91
+ {subtitle && (
92
+ <Text style={[styles.headerSubtitle, { color: colors.textSecondary }]}>
93
+ {subtitle}
94
+ </Text>
95
+ )}
96
+ </View>
97
+ </View>
98
+ );
99
+ };
100
+
101
+ const styles = StyleSheet.create({
102
+ header: {
103
+ flexDirection: 'row',
104
+ alignItems: 'center',
105
+ paddingHorizontal: 16,
106
+ paddingVertical: 12,
107
+ gap: 12,
108
+ },
109
+ searchContainer: {
110
+ flex: 1,
111
+ flexDirection: 'row',
112
+ alignItems: 'center',
113
+ paddingHorizontal: 12,
114
+ paddingVertical: 10,
115
+ borderRadius: 12,
116
+ gap: 8,
117
+ },
118
+ searchInput: {
119
+ flex: 1,
120
+ fontSize: 15,
121
+ padding: 0,
122
+ },
123
+ profileContainer: {
124
+ width: 40,
125
+ height: 40,
126
+ borderRadius: 20,
127
+ overflow: 'hidden',
128
+ borderWidth: 2,
129
+ borderColor: '#E5E7EB',
130
+ },
131
+ profileImage: {
132
+ width: '100%',
133
+ height: '100%',
134
+ },
135
+ backHeader: {
136
+ flexDirection: 'row',
137
+ alignItems: 'center',
138
+ paddingHorizontal: 16,
139
+ paddingVertical: 12,
140
+ gap: 12,
141
+ },
142
+ backButton: {
143
+ width: 40,
144
+ height: 40,
145
+ justifyContent: 'center',
146
+ alignItems: 'flex-start',
147
+ },
148
+ headerTextContainer: {
149
+ flex: 1,
150
+ },
151
+ headerTitle: {
152
+ fontSize: 24,
153
+ fontWeight: '700',
154
+ marginBottom: 4,
155
+ },
156
+ headerSubtitle: {
157
+ fontSize: 14,
158
+ lineHeight: 20,
159
+ },
160
+ });