feat: tambah sistem guide onboarding per fitur dengan GuideOverlay

- Buat komponen GuideOverlay dengan animasi fade+slide, arrow tooltip, dan dot indicator
- Buat hook useGuide untuk menyimpan state guide per fitur via AsyncStorage
- Sentralisasi semua step guide di lib/guideSteps.ts
- Pasang guide pada 12 halaman: village-calendar, project detail, banner, group, position, member, announcement, discussion, division calendar/document/discussion, dan division task detail
- Posisi card menggunakan cardTopRatio (rasio layar) untuk kompatibilitas berbagai ukuran device
- Tambah styles guide dan village calendar di constants/Styles.ts
This commit is contained in:
2026-05-11 16:34:46 +08:00
parent 84935e8188
commit 7341f378dd
16 changed files with 539 additions and 1 deletions

View File

@@ -1,9 +1,12 @@
import GuideOverlay from "@/components/GuideOverlay";
import BorderBottomItem from "@/components/borderBottomItem";
import InputSearch from "@/components/inputSearch";
import SkeletonContent from "@/components/skeletonContent";
import Text from '@/components/Text';
import Styles from "@/constants/Styles";
import { apiGetAnnouncement } from "@/lib/api";
import { GUIDE_ANNOUNCEMENT } from "@/lib/guideSteps";
import { useGuide } from "@/lib/useGuide";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import { MaterialIcons } from "@expo/vector-icons";
@@ -25,6 +28,7 @@ export default function Announcement() {
const { colors } = useTheme();
const [search, setSearch] = useState('')
const update = useSelector((state: any) => state.announcementUpdate)
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('announcement')
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
// TanStack Query Infinite Query
@@ -78,6 +82,7 @@ export default function Announcement() {
return (
<View style={[Styles.p15, Styles.flex1, { backgroundColor: colors.background }]}>
<GuideOverlay visible={guideVisible} steps={GUIDE_ANNOUNCEMENT} onDismiss={dismissGuide} />
<View>
<InputSearch onChange={setSearch} />
</View>

View File

@@ -1,5 +1,5 @@
import styles from "@/components/AppHeader"
import AppHeader from "@/components/AppHeader"
import GuideOverlay from "@/components/GuideOverlay"
import HeaderRightBannerList from "@/components/banner/headerBannerList"
import BorderBottomItem from "@/components/borderBottomItem"
import DrawerBottom from "@/components/drawerBottom"
@@ -12,6 +12,8 @@ import { ConstEnv } from "@/constants/ConstEnv"
import Styles from "@/constants/Styles"
import { apiDeleteBanner, apiGetBanner } from "@/lib/api"
import { setEntities } from "@/lib/bannerSlice"
import { GUIDE_BANNER } from "@/lib/guideSteps"
import { useGuide } from "@/lib/useGuide"
import { useAuthSession } from "@/providers/AuthProvider"
import { useTheme } from "@/providers/ThemeProvider"
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"
@@ -44,6 +46,7 @@ export default function BannerList() {
const dispatch = useDispatch()
const [refreshing, setRefreshing] = useState(false)
const [loadingOpen, setLoadingOpen] = useState(false)
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('banner')
const [viewImg, setViewImg] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const queryClient = useQueryClient()
@@ -147,6 +150,7 @@ export default function BannerList() {
)
}}
/>
<GuideOverlay visible={guideVisible} steps={GUIDE_BANNER} onDismiss={dismissGuide} />
<ModalLoading isVisible={loadingOpen} setVisible={setLoadingOpen} />
<ScrollView
refreshControl={

View File

@@ -1,3 +1,4 @@
import GuideOverlay from "@/components/GuideOverlay";
import ButtonTab from "@/components/buttonTab";
import InputSearch from "@/components/inputSearch";
import LabelStatus from "@/components/labelStatus";
@@ -6,6 +7,8 @@ import Text from "@/components/Text";
import WrapTab from "@/components/wrapTab";
import Styles from "@/constants/Styles";
import { apiGetDiscussionGeneral } from "@/lib/api";
import { GUIDE_DISCUSSION } from "@/lib/guideSteps";
import { useGuide } from "@/lib/useGuide";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import { AntDesign, Feather } from "@expo/vector-icons";
@@ -34,6 +37,7 @@ export default function Discussion() {
const queryClient = useQueryClient()
const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true')
const [refreshing, setRefreshing] = useState(false)
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('discussion')
const {
data,
@@ -85,6 +89,7 @@ export default function Discussion() {
return (
<View style={[Styles.flex1, { backgroundColor: colors.background }]}>
<GuideOverlay visible={guideVisible} steps={GUIDE_DISCUSSION} onDismiss={dismissGuide} />
{/* Header controls */}
<View style={[Styles.ph15, { paddingTop: 12 }]}>
{entityUser.role != "user" && entityUser.role != "coadmin" && (

View File

@@ -1,4 +1,5 @@
import AppHeader from "@/components/AppHeader";
import GuideOverlay from "@/components/GuideOverlay";
import HeaderRightCalendarList from "@/components/calendar/headerCalendarList";
import ItemDateCalendar from "@/components/calendar/itemDateCalendar";
import EventItem from "@/components/eventItem";
@@ -6,6 +7,8 @@ import Skeleton from "@/components/skeleton";
import Text from "@/components/Text";
import Styles from "@/constants/Styles";
import { apiGetCalendarByDateDivision, apiGetIndicatorCalendar } from "@/lib/api";
import { GUIDE_DIVISION_CALENDAR } from "@/lib/guideSteps";
import { useGuide } from "@/lib/useGuide";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import { Feather } from "@expo/vector-icons";
@@ -46,6 +49,7 @@ export default function CalendarDivision() {
const [loading, setLoading] = useState(true)
const [loadingBtn, setLoadingBtn] = useState(false)
const [refreshing, setRefreshing] = useState(false)
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('division-calendar')
async function handleLoad(loading: boolean) {
@@ -150,6 +154,7 @@ export default function CalendarDivision() {
)
}}
/>
<GuideOverlay visible={guideVisible} steps={GUIDE_DIVISION_CALENDAR} onDismiss={dismissGuide} />
<ScrollView
refreshControl={
<RefreshControl

View File

@@ -1,3 +1,4 @@
import GuideOverlay from "@/components/GuideOverlay";
import ButtonTab from "@/components/buttonTab";
import ImageUser from "@/components/imageNew";
import InputSearch from "@/components/inputSearch";
@@ -7,6 +8,8 @@ import WrapTab from "@/components/wrapTab";
import { ConstEnv } from "@/constants/ConstEnv";
import Styles from "@/constants/Styles";
import { apiGetDiscussion, apiGetDivisionOneFeature } from "@/lib/api";
import { GUIDE_DIVISION_DISCUSSION } from "@/lib/guideSteps";
import { useGuide } from "@/lib/useGuide";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import { AntDesign, Feather } from "@expo/vector-icons";
@@ -43,6 +46,7 @@ export default function DiscussionDivision() {
const [isMemberDivision, setIsMemberDivision] = useState(false)
const [isAdminDivision, setIsAdminDivision] = useState(false)
const entityUser = useSelector((state: any) => state.user)
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('division-discussion')
async function handleCheckMember() {
try {
@@ -96,6 +100,7 @@ export default function DiscussionDivision() {
return (
<View style={[Styles.flex1, { backgroundColor: colors.background }]}>
<GuideOverlay visible={guideVisible} steps={GUIDE_DIVISION_DISCUSSION} onDismiss={dismissGuide} />
{((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision) && (
<View style={[Styles.ph15, { paddingTop: 12 }]}>
<WrapTab>

View File

@@ -1,3 +1,4 @@
import GuideOverlay from "@/components/GuideOverlay";
import ModalConfirmation from "@/components/ModalConfirmation";
import AppHeader from "@/components/AppHeader";
import { ButtonHeader } from "@/components/buttonHeader";
@@ -22,6 +23,8 @@ import {
apiShareDocument,
} from "@/lib/api";
import { setUpdateDokumen } from "@/lib/dokumenUpdate";
import { GUIDE_DIVISION_DOCUMENT } from "@/lib/guideSteps";
import { useGuide } from "@/lib/useGuide";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import {
@@ -90,6 +93,7 @@ export default function DocumentDivision() {
const [isMemberDivision, setIsMemberDivision] = useState(false)
const entityUser = useSelector((state: any) => state.user)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('division-document')
const [bodyRename, setBodyRename] = useState({
id: "",
name: "",
@@ -415,6 +419,7 @@ export default function DocumentDivision() {
)
}}
/>
<GuideOverlay visible={guideVisible} steps={GUIDE_DIVISION_DOCUMENT} onDismiss={dismissGuide} />
<ModalLoading isVisible={loadingOpen} setVisible={setLoadingOpen} />
<ScrollView
refreshControl={

View File

@@ -1,4 +1,5 @@
import AppHeader from "@/components/AppHeader";
import GuideOverlay from "@/components/GuideOverlay";
import SectionCancel from "@/components/sectionCancel";
import SectionProgress from "@/components/sectionProgress";
import HeaderRightTaskDetail from "@/components/task/headerTaskDetail";
@@ -9,6 +10,8 @@ import SectionReportTask from "@/components/task/sectionReportTask";
import SectionTanggalTugasTask from "@/components/task/sectionTanggalTugasTask";
import Styles from "@/constants/Styles";
import { apiGetDivisionOneFeature, apiGetTaskOne } from "@/lib/api";
import { GUIDE_PROJECT_DETAIL } from "@/lib/guideSteps";
import { useGuide } from "@/lib/useGuide";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import { router, Stack, useLocalSearchParams } from "expo-router";
@@ -36,6 +39,7 @@ export default function DetailTaskDivision() {
const update = useSelector((state: any) => state.taskUpdate)
const [refreshing, setRefreshing] = useState(false)
const [isMemberDivision, setIsMemberDivision] = useState(false);
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('division-task-detail')
const [isAdminDivision, setIsAdminDivision] = useState(false);
const entityUser = useSelector((state: any) => state.user);
@@ -139,6 +143,7 @@ export default function DetailTaskDivision() {
)
}}
/>
<GuideOverlay visible={guideVisible} steps={GUIDE_PROJECT_DETAIL} onDismiss={dismissGuide} />
<ScrollView
refreshControl={
<RefreshControl

View File

@@ -1,3 +1,4 @@
import GuideOverlay from "@/components/GuideOverlay";
import ModalConfirmation from "@/components/ModalConfirmation";
import BorderBottomItem from "@/components/borderBottomItem";
import { ButtonForm } from "@/components/buttonForm";
@@ -12,6 +13,8 @@ import WrapTab from "@/components/wrapTab";
import Styles from "@/constants/Styles";
import { apiDeleteGroup, apiEditGroup, apiGetGroup } from "@/lib/api";
import { setUpdateGroup } from "@/lib/groupSlice";
import { GUIDE_GROUP } from "@/lib/guideSteps";
import { useGuide } from "@/lib/useGuide";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import { AntDesign, Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
@@ -41,6 +44,7 @@ export default function Index() {
const [titleChoose, setTitleChoose] = useState('')
const queryClient = useQueryClient()
const [refreshing, setRefreshing] = useState(false)
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('group')
const dispatch = useDispatch()
const update = useSelector((state: any) => state.groupUpdate)
@@ -136,6 +140,7 @@ export default function Index() {
return (
<View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}>
<GuideOverlay visible={guideVisible} steps={GUIDE_GROUP} onDismiss={dismissGuide} />
<View style={[Styles.mb10]}>
<WrapTab>
<ButtonTab

View File

@@ -1,3 +1,4 @@
import GuideOverlay from "@/components/GuideOverlay";
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonTab from "@/components/buttonTab";
import ImageUser from "@/components/imageNew";
@@ -9,6 +10,8 @@ import WrapTab from "@/components/wrapTab";
import { ConstEnv } from "@/constants/ConstEnv";
import Styles from "@/constants/Styles";
import { apiGetUser } from "@/lib/api";
import { GUIDE_MEMBER } from "@/lib/guideSteps";
import { useGuide } from "@/lib/useGuide";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import { AntDesign, Feather } from "@expo/vector-icons";
@@ -42,6 +45,7 @@ export default function Index() {
const queryClient = useQueryClient()
const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true')
const [refreshing, setRefreshing] = useState(false)
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('member')
// TanStack Query for Members with Infinite Scroll
const {
@@ -117,6 +121,7 @@ export default function Index() {
return (
<View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}>
<GuideOverlay visible={guideVisible} steps={GUIDE_MEMBER} onDismiss={dismissGuide} />
<View>
<WrapTab>
<ButtonTab

View File

@@ -1,3 +1,4 @@
import GuideOverlay from "@/components/GuideOverlay";
import BorderBottomItem from "@/components/borderBottomItem";
import { ButtonForm } from "@/components/buttonForm";
import ButtonTab from "@/components/buttonTab";
@@ -13,6 +14,8 @@ import WrapTab from "@/components/wrapTab";
import Styles from "@/constants/Styles";
import { apiDeletePosition, apiEditPosition, apiGetPosition } from "@/lib/api";
import { setUpdatePosition } from "@/lib/positionSlice";
import { GUIDE_POSITION } from "@/lib/guideSteps";
import { useGuide } from "@/lib/useGuide";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import { AntDesign, Feather, MaterialCommunityIcons } from "@expo/vector-icons";
@@ -48,6 +51,7 @@ export default function Index() {
const queryClient = useQueryClient()
const [refreshing, setRefreshing] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('position')
const dispatch = useDispatch()
const update = useSelector((state: any) => state.positionUpdate)
@@ -160,6 +164,7 @@ export default function Index() {
return (
<View style={[Styles.p15, Styles.flex1, { backgroundColor: colors.background }]}>
<GuideOverlay visible={guideVisible} steps={GUIDE_POSITION} onDismiss={dismissGuide} />
<View>
<WrapTab>
<ButtonTab

View File

@@ -1,4 +1,5 @@
import AppHeader from "@/components/AppHeader";
import GuideOverlay from "@/components/GuideOverlay";
import HeaderRightProjectDetail from "@/components/project/headerProjectDetail";
import SectionFile from "@/components/project/sectionFile";
import SectionLink from "@/components/project/sectionLink";
@@ -9,6 +10,8 @@ import SectionCancel from "@/components/sectionCancel";
import SectionProgress from "@/components/sectionProgress";
import Styles from "@/constants/Styles";
import { apiGetProjectOne } from "@/lib/api";
import { GUIDE_PROJECT_DETAIL } from "@/lib/guideSteps";
import { useGuide } from "@/lib/useGuide";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import { router, Stack, useLocalSearchParams } from "expo-router";
@@ -43,6 +46,7 @@ export default function DetailProject() {
const [isMember, setIsMember] = useState(false)
const entityUser = useSelector((state: any) => state.user)
const [refreshing, setRefreshing] = useState(false)
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('project-detail')
async function handleLoad(cat: 'data' | 'progress') {
try {
@@ -129,6 +133,7 @@ export default function DetailProject() {
)
}}
/>
<GuideOverlay visible={guideVisible} steps={GUIDE_PROJECT_DETAIL} onDismiss={dismissGuide} />
<ScrollView
style={[Styles.h100, { backgroundColor: colors.background }]}
refreshControl={

View File

@@ -1,9 +1,12 @@
import AppHeader from "@/components/AppHeader";
import ItemDateCalendar from "@/components/calendar/itemDateCalendar";
import GuideOverlay from "@/components/GuideOverlay";
import Skeleton from "@/components/skeleton";
import Text from "@/components/Text";
import Styles from "@/constants/Styles";
import { apiGetVillageCalendarByDate, apiGetVillageCalendarIndicator } from "@/lib/api";
import { GUIDE_VILLAGE_CALENDAR } from "@/lib/guideSteps";
import { useGuide } from "@/lib/useGuide";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import { Feather } from "@expo/vector-icons";
@@ -84,9 +87,11 @@ function VillageEventItem({ item, onPress }: { item: EventItem; onPress: () => v
);
}
export default function VillageCalendar() {
const { colors, activeTheme } = useTheme();
const { token, decryptToken } = useAuthSession();
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('village-calendar');
const [selected, setSelected] = useState<any>(new Date());
const [data, setData] = useState<EventItem[]>([]);
const [dataIndicator, setDataIndicator] = useState<{ calendar: string[], task: string[] }>({ calendar: [], task: [] });
@@ -194,6 +199,11 @@ export default function VillageCalendar() {
),
}}
/>
<GuideOverlay
visible={guideVisible}
steps={GUIDE_VILLAGE_CALENDAR}
onDismiss={dismissGuide}
/>
<ScrollView
refreshControl={
<RefreshControl