diff --git a/app/(application)/announcement/index.tsx b/app/(application)/announcement/index.tsx index c62c9d7..0963911 100644 --- a/app/(application)/announcement/index.tsx +++ b/app/(application)/announcement/index.tsx @@ -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 ( + diff --git a/app/(application)/banner/index.tsx b/app/(application)/banner/index.tsx index c5560ab..4340b47 100644 --- a/app/(application)/banner/index.tsx +++ b/app/(application)/banner/index.tsx @@ -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() { ) }} /> + (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 ( + {/* Header controls */} {entityUser.role != "user" && entityUser.role != "coadmin" && ( diff --git a/app/(application)/division/[id]/(fitur-division)/calendar/index.tsx b/app/(application)/division/[id]/(fitur-division)/calendar/index.tsx index 7664f62..7baa0f8 100644 --- a/app/(application)/division/[id]/(fitur-division)/calendar/index.tsx +++ b/app/(application)/division/[id]/(fitur-division)/calendar/index.tsx @@ -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() { ) }} /> + state.user) + const { visible: guideVisible, dismiss: dismissGuide } = useGuide('division-discussion') async function handleCheckMember() { try { @@ -96,6 +100,7 @@ export default function DiscussionDivision() { return ( + {((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision) && ( diff --git a/app/(application)/division/[id]/(fitur-division)/document/index.tsx b/app/(application)/division/[id]/(fitur-division)/document/index.tsx index 5104754..ca528a1 100644 --- a/app/(application)/division/[id]/(fitur-division)/document/index.tsx +++ b/app/(application)/division/[id]/(fitur-division)/document/index.tsx @@ -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() { ) }} /> + 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() { ) }} /> + - + } text="Divisi" onPress={() => { router.push('/division?active=true') }} /> } text="Kegiatan" onPress={() => { router.push('/project?status=0') }} /> } text="Pengumuman" onPress={() => { router.push('/announcement') }} /> } text="Diskusi" onPress={() => { router.push('/discussion?active=true') }} /> - - + } text="Kalender" onPress={() => { router.push('/village-calendar') }} /> } text="Anggota" onPress={() => { router.push('/member') }} /> } text="Jabatan" onPress={() => { router.push('/position') }} /> { @@ -39,7 +38,6 @@ export default function Feature() { (entityUser.role == "supadmin" || entityUser.role == "developer") && <> } text="Lembaga Desa" onPress={() => { router.push('/group') }} /> - {/* } text="Tema" onPress={() => { }} /> */} } text="Banner" onPress={() => { router.push('/banner') }} /> } diff --git a/app/(application)/group/index.tsx b/app/(application)/group/index.tsx index 995d4b1..e20f81b 100644 --- a/app/(application)/group/index.tsx +++ b/app/(application)/group/index.tsx @@ -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 ( + (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 ( + state.positionUpdate) @@ -160,6 +164,7 @@ export default function Index() { return ( + 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() { ) }} /> + void }) { + const { activeTheme, colors } = useTheme(); + const palette = TYPE_COLORS[item.type][activeTheme === 'dark' ? 'dark' : 'light']; + + return ( + + + + {item.timeStart ? ( + + {item.timeStart} - {item.timeEnd} + + ) : ( + + {moment(item.dateStart).format('D MMM YYYY')} – {moment(item.dateEnd).format('D MMM YYYY')} + + )} + + {item.title} + + + + + {item.type === 'calendar' ? 'Divisi' : 'Kegiatan'} + + + + {item.type === 'calendar' ? item.divisionName : item.projectName} + + + + + ); +} + + +export default function VillageCalendar() { + const { colors, activeTheme } = useTheme(); + const { token, decryptToken } = useAuthSession(); + const { visible: guideVisible, dismiss: dismissGuide } = useGuide('village-calendar'); + const [selected, setSelected] = useState(new Date()); + const [data, setData] = useState([]); + const [dataIndicator, setDataIndicator] = useState<{ calendar: string[], task: string[] }>({ calendar: [], task: [] }); + const [month, setMonth] = useState(new Date().getMonth()); + const [loading, setLoading] = useState(true); + const [loadingBtn, setLoadingBtn] = useState(false); + const [refreshing, setRefreshing] = useState(false); + + async function handleLoad(showLoading: boolean) { + try { + setLoading(showLoading); + const hasil = await decryptToken(String(token?.current)); + const response = await apiGetVillageCalendarByDate({ + user: hasil, + date: moment(selected).format("YYYY-MM-DD"), + }); + setData(response.data); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + } + + async function handleLoadIndicator() { + try { + setLoadingBtn(true); + const newDate = new Date(selected?.getFullYear(), month, 1); + const hasil = await decryptToken(String(token?.current)); + const response = await apiGetVillageCalendarIndicator({ + user: hasil, + date: moment(newDate).format("YYYY-MM-DD"), + }); + setDataIndicator(response.data ?? { calendar: [], task: [] }); + } catch (error) { + console.error(error); + } finally { + setTimeout(() => setLoadingBtn(false), 500); + } + } + + useEffect(() => { + handleLoad(true); + }, [selected]); + + useEffect(() => { + handleLoadIndicator(); + }, [month]); + + const handleRefresh = async () => { + setRefreshing(true); + handleLoad(false); + handleLoadIndicator(); + await new Promise(resolve => setTimeout(resolve, 1500)); + setRefreshing(false); + }; + + function handlePressEvent(item: EventItem) { + if (item.type === 'calendar') { + router.push(`/division/${item.idDivision}/calendar/${item.id}` as any); + } else { + router.push(`/project/${item.idRef}` as any); + } + } + + const calendarEvents = data.filter(e => e.type === 'calendar'); + const taskEvents = data.filter(e => e.type === 'task'); + + const components: CalendarComponents = { + Day: (day: CalendarDay) => { + const today = moment(String(day.date)).format("YYYY-MM-DD"); + return ( + setSelected(new Date(today))} + /> + ); + }, + IconNext: ( + !loadingBtn && setMonth(month + 1)}> + + + ), + IconPrev: ( + !loadingBtn && setMonth(month - 1)}> + + + ), + }; + + return ( + + ( + router.back()} + /> + ), + }} + /> + + + } + style={[Styles.h100]} + > + + {/* Calendar grid */} + + setMonth(m)} + styles={{ + selected: Styles.selectedDate, + month_label: { color: colors.text }, + month_selector_label: { color: colors.text }, + year_label: { color: colors.text }, + year_selector_label: { color: colors.text }, + day_label: { color: colors.text }, + time_label: { color: colors.text }, + weekday_label: { color: colors.text }, + }} + /> + + + {/* Legend */} + + + + Acara Divisi + + + + Kegiatan + + + + {/* Calendar events */} + + Acara Divisi + + {loading ? ( + <> + + + + ) : calendarEvents.length > 0 ? ( + calendarEvents.map((item, index) => ( + handlePressEvent(item)} + /> + )) + ) : ( + + Tidak ada acara + + )} + + + + {/* Task events */} + + Kegiatan + + {loading ? ( + <> + + + + ) : taskEvents.length > 0 ? ( + taskEvents.map((item, index) => ( + handlePressEvent(item)} + /> + )) + ) : ( + + Tidak ada kegiatan + + )} + + + + + + ); +} diff --git a/bun.lock b/bun.lock index 952f9ff..613e278 100644 --- a/bun.lock +++ b/bun.lock @@ -46,6 +46,7 @@ "expo-modules-core": "^2.5.0", "expo-notifications": "~0.31.3", "expo-router": "~5.1.4", + "expo-secure-store": "~14.2.4", "expo-sharing": "^13.1.5", "expo-splash-screen": "~0.30.8", "expo-status-bar": "~2.2.3", @@ -1103,6 +1104,8 @@ "expo-router": ["expo-router@5.1.11", "", { "dependencies": { "@expo/metro-runtime": "5.0.5", "@expo/schema-utils": "^0.1.0", "@expo/server": "^0.6.3", "@radix-ui/react-slot": "1.2.0", "@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/native": "^7.1.6", "@react-navigation/native-stack": "^7.3.10", "client-only": "^0.0.1", "invariant": "^2.2.4", "react-fast-compare": "^3.2.2", "react-native-is-edge-to-edge": "^1.1.6", "semver": "~7.6.3", "server-only": "^0.0.1", "shallowequal": "^1.1.0" }, "peerDependencies": { "@react-navigation/drawer": "^7.3.9", "expo": "*", "expo-constants": "*", "expo-linking": "*", "react-native-reanimated": "*", "react-native-safe-area-context": "*", "react-native-screens": "*", "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4" }, "optionalPeers": ["@react-navigation/drawer", "react-native-reanimated", "react-server-dom-webpack"] }, "sha512-6YQGqQM2rviVSiU6++hrJDPMByHZ7Oiux4XmgoSaGdaHku5QOn9911f2puEUZh2H9ALKBipw5v3ZkrECBd6Zbw=="], + "expo-secure-store": ["expo-secure-store@14.2.4", "", { "peerDependencies": { "expo": "*" } }, "sha512-ePaz4fnTitJJZjAiybaVYGfLWWyaEtepZC+vs9ZBMhQMfG5HUotIcVsDaSo3FnwpHmgwsLVPY2qFeryI6AtULw=="], + "expo-sharing": ["expo-sharing@13.1.5", "", { "peerDependencies": { "expo": "*" } }, "sha512-X/5sAEiWXL2kdoGE3NO5KmbfcmaCWuWVZXHu8OQef7Yig4ZgHFkGD11HKJ5KqDrDg+SRZe4ISd6MxE7vGUgm4w=="], "expo-splash-screen": ["expo-splash-screen@0.30.10", "", { "dependencies": { "@expo/prebuild-config": "^9.0.10" }, "peerDependencies": { "expo": "*" } }, "sha512-Tt9va/sLENQDQYeOQ6cdLdGvTZ644KR3YG9aRlnpcs2/beYjOX1LHT510EGzVN9ljUTg+1ebEo5GGt2arYtPjw=="], diff --git a/components/GuideOverlay.tsx b/components/GuideOverlay.tsx new file mode 100644 index 0000000..899a7df --- /dev/null +++ b/components/GuideOverlay.tsx @@ -0,0 +1,159 @@ +import Text from '@/components/Text'; +import Styles from '@/constants/Styles'; +import { useTheme } from '@/providers/ThemeProvider'; +import { Feather } from '@expo/vector-icons'; +import React, { useEffect, useRef, useState } from 'react'; +import { + Animated, + Dimensions, + Modal, + TouchableOpacity, + View, +} from 'react-native'; + +export type GuideStep = { + title: string; + description: string; + // posisi card: pixel absolut atau rasio layar (0.0-1.0), rasio lebih aman untuk layout dinamis + cardTop?: number; + cardTopRatio?: number; + // arrow menunjuk ke atas (elemen ada di atas card) atau ke bawah (elemen ada di bawah card) + arrowDirection?: 'up' | 'down' | 'none'; + // offset horizontal arrow dari kiri card (0.0-1.0), default 0.5 = tengah + arrowOffset?: number; +}; + +type Props = { + visible: boolean; + steps: GuideStep[]; + onDismiss: () => void; +}; + +const { height: SCREEN_H } = Dimensions.get('window'); +const CARD_MARGIN = 24; + +export default function GuideOverlay({ visible, steps, onDismiss }: Props) { + const { colors } = useTheme(); + const [currentStep, setCurrentStep] = useState(0); + const fadeAnim = useRef(new Animated.Value(0)).current; + const slideAnim = useRef(new Animated.Value(20)).current; + + const step = steps[currentStep]; + const isLast = currentStep === steps.length - 1; + + const arrowDirection = step?.arrowDirection ?? 'none'; + const arrowOffset = step?.arrowOffset ?? 0.5; + const cardTop = step?.cardTopRatio != null + ? SCREEN_H * step.cardTopRatio + : (step?.cardTop ?? SCREEN_H * 0.35); + + const cardPositionStyle = { top: cardTop }; + + useEffect(() => { + if (visible) { + setCurrentStep(0); + Animated.parallel([ + Animated.timing(fadeAnim, { toValue: 1, duration: 300, useNativeDriver: true }), + Animated.timing(slideAnim, { toValue: 0, duration: 300, useNativeDriver: true }), + ]).start(); + } else { + fadeAnim.setValue(0); + slideAnim.setValue(20); + } + }, [visible]); + + function animateStep(next: number) { + Animated.sequence([ + Animated.timing(slideAnim, { toValue: 12, duration: 120, useNativeDriver: true }), + Animated.timing(slideAnim, { toValue: 0, duration: 180, useNativeDriver: true }), + ]).start(); + setCurrentStep(next); + } + + if (!visible) return null; + + return ( + + + + {/* Arrow atas — menunjuk ke elemen di atas card */} + {arrowDirection === 'up' && ( + + )} + + {/* Header */} + + + + {currentStep + 1} / {steps.length} + + + + + + + + {/* Content */} + + {step.title} + + + {step.description} + + + {/* Dot indicator */} + + {steps.map((_, i) => ( + + ))} + + + {/* Footer */} + + {currentStep > 0 ? ( + animateStep(currentStep - 1)} style={Styles.guideButtonSecondary}> + Kembali + + ) : ( + + Lewati + + )} + + isLast ? onDismiss() : animateStep(currentStep + 1)} + style={[Styles.guideButtonPrimary, { backgroundColor: colors.icon }]} + > + + {isLast ? 'Selesai' : 'Lanjut'} + + {!isLast && } + + + + {/* Arrow bawah — menunjuk ke elemen di bawah card */} + {arrowDirection === 'down' && ( + + )} + + + + ); +} diff --git a/components/buttonFiturMenu.tsx b/components/buttonFiturMenu.tsx index d9c3533..ba224e6 100644 --- a/components/buttonFiturMenu.tsx +++ b/components/buttonFiturMenu.tsx @@ -14,11 +14,11 @@ export function ButtonFiturMenu({ onPress, icon, text }: Props) { const { colors } = useTheme(); return ( - + {icon} - {text} + {text} ) diff --git a/components/calendar/itemDateCalendar.tsx b/components/calendar/itemDateCalendar.tsx index a932783..a1354f4 100644 --- a/components/calendar/itemDateCalendar.tsx +++ b/components/calendar/itemDateCalendar.tsx @@ -6,19 +6,28 @@ import Text from "../Text"; type Props = { text: string; isSelected: boolean; - isSign: boolean; + isSign?: boolean; + isSignCalendar?: boolean; + isSignTask?: boolean; onPress?: () => void; } - -export default function ItemDateCalendar({ text, isSelected, isSign, onPress }: Props) { +export default function ItemDateCalendar({ text, isSelected, isSign, isSignCalendar, isSignTask, onPress }: Props) { const { colors } = useTheme() + + const showMulti = isSignCalendar !== undefined || isSignTask !== undefined; + return ( - <> - - {text} - - - + + {text} + {showMulti ? ( + + + + + ) : ( + + )} + ) } \ No newline at end of file diff --git a/components/eventItem.tsx b/components/eventItem.tsx index 10117cc..51af2f4 100644 --- a/components/eventItem.tsx +++ b/components/eventItem.tsx @@ -17,16 +17,16 @@ export default function EventItem({ category, title, user, jamAwal, jamAkhir, on const getBackgroundColor = (cat: 'purple' | 'orange') => { if (activeTheme === 'dark') { - return cat === 'orange' ? '#547792' : '#1D546D'; + return cat === 'purple' ? '#2D2B5E' : '#1C3347'; } - return cat === 'orange' ? '#D6E6F2' : '#A9B5DF'; + return cat === 'purple' ? '#A9B5DF' : '#D6E6F2'; }; const getStickColor = (cat: 'purple' | 'orange') => { if (activeTheme === 'dark') { - return cat === 'orange' ? '#94B4C1' : '#5F9598'; + return cat === 'purple' ? '#7886C7' : '#94B4C1'; } - return cat === 'orange' ? '#F5F5F5' : '#7886C7'; + return cat === 'purple' ? '#7886C7' : '#94B4C1'; }; return ( diff --git a/constants/Styles.ts b/constants/Styles.ts index e3b2462..ada35aa 100644 --- a/constants/Styles.ts +++ b/constants/Styles.ts @@ -1169,6 +1169,108 @@ const Styles = StyleSheet.create({ fontSize: 10, fontStyle: 'italic', }, + + // guide overlay + guideOverlay: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.6)', + justifyContent: 'center', + alignItems: 'center', + }, + guideCard: { + position: 'absolute', + left: 24, + right: 24, + borderRadius: 16, + padding: 20, + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.2, + shadowRadius: 12, + elevation: 8, + }, + guideBadge: { + paddingHorizontal: 8, + paddingVertical: 3, + borderRadius: 20, + }, + guideDotRow: { + marginTop: 16, + gap: 6, + justifyContent: 'center', + }, + guideDot: { + width: 6, + height: 6, + borderRadius: 3, + }, + guideButtonPrimary: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 20, + }, + guideButtonSecondary: { + paddingHorizontal: 4, + paddingVertical: 8, + }, + guideArrowUp: { + position: 'absolute', + top: -10, + marginLeft: -8, + width: 0, + height: 0, + borderLeftWidth: 8, + borderRightWidth: 8, + borderBottomWidth: 10, + borderLeftColor: 'transparent', + borderRightColor: 'transparent', + }, + guideArrowDown: { + position: 'absolute', + bottom: -10, + marginLeft: -8, + width: 0, + height: 0, + borderLeftWidth: 8, + borderRightWidth: 8, + borderTopWidth: 10, + borderLeftColor: 'transparent', + borderRightColor: 'transparent', + }, + + // village calendar & itemDateCalendar + calendarDotRow: { + flexDirection: 'row', + gap: 2, + height: 6, + marginTop: 1, + }, + calendarDot: { + width: 5, + height: 5, + borderRadius: 3, + }, + villageEventLegendRow: { + marginTop: 10, + marginBottom: 4, + gap: 16, + }, + villageEventLegendItem: { + gap: 6, + }, + villageEventLegendDot: { + width: 10, + height: 10, + borderRadius: 5, + }, + villageEventBadge: { + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 4, + marginRight: 6, + }, }) export default Styles; \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c1a059c..000d729 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -318,6 +318,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - ExpoSecureStore (14.2.4): + - ExpoModulesCore - ExpoSharing (13.1.5): - ExpoModulesCore - ExpoSplashScreen (0.30.10): @@ -2247,6 +2249,7 @@ DEPENDENCIES: - ExpoLinking (from `../node_modules/expo-linking/ios`) - ExpoMediaLibrary (from `../node_modules/expo-media-library/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) + - ExpoSecureStore (from `../node_modules/expo-secure-store/ios`) - ExpoSharing (from `../node_modules/expo-sharing/ios`) - ExpoSplashScreen (from `../node_modules/expo-splash-screen/ios`) - ExpoSymbols (from `../node_modules/expo-symbols/ios`) @@ -2420,6 +2423,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-media-library/ios" ExpoModulesCore: :path: "../node_modules/expo-modules-core" + ExpoSecureStore: + :path: "../node_modules/expo-secure-store/ios" ExpoSharing: :path: "../node_modules/expo-sharing/ios" ExpoSplashScreen: @@ -2630,6 +2635,7 @@ SPEC CHECKSUMS: ExpoLinking: d5c183998ca6ada66ff45e407e0f965b398a8902 ExpoMediaLibrary: 0daf5e811e00daa47690f5da2989e71ff7de56e0 ExpoModulesCore: 272bc6c06ddd9c4bee2048acc57891cab3700627 + ExpoSecureStore: 3f1b632d6d40bcc62b4983ef9199cd079592a50a ExpoSharing: b0377be82430d07398c6a4cd60b5a15696accbd3 ExpoSplashScreen: 1c22c5d37647106e42d4ae1582bb6d0dda3b2385 ExpoSymbols: c5612a90fb9179cdaebcd19bea9d8c69e5d3b859 diff --git a/lib/api.ts b/lib/api.ts index cf3e82a..a2a84f0 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -619,6 +619,16 @@ export const apiGetCalendarHistory = async ({ user, search, division, page }: { return response.data; }; +export const apiGetVillageCalendarByDate = async ({ user, date }: { user: string, date: string }) => { + const response = await api.get(`mobile/village-calendar?user=${user}&date=${date}`); + return response.data; +}; + +export const apiGetVillageCalendarIndicator = async ({ user, date }: { user: string, date: string }) => { + const response = await api.get(`mobile/village-calendar/indicator?user=${user}&date=${date}`); + return response.data; +}; + export const apiCreateCalendar = async ({ data }: { data: { idDivision: string, title: string, desc: string, timeStart: string, timeEnd: string, dateStart: string, repeatEventTyper: string, repeatValue: string, linkMeet: string, member: any[], user: string } }) => { const response = await api.post(`/mobile/calendar`, data) return response.data; diff --git a/lib/guideSteps.ts b/lib/guideSteps.ts new file mode 100644 index 0000000..64358f4 --- /dev/null +++ b/lib/guideSteps.ts @@ -0,0 +1,217 @@ +import { GuideStep } from "@/components/GuideOverlay"; + +export const GUIDE_VILLAGE_CALENDAR: GuideStep[] = [ + { + title: 'Kalender Umum', + description: 'Halaman ini menampilkan semua acara divisi dan kegiatan yang ada di desa kamu dalam satu kalender.', + cardTopRatio: 0.58, + arrowDirection: 'up', + arrowOffset: 0.5, + }, + { + title: 'Pilih Tanggal', + description: 'Ketuk tanggal pada kalender untuk melihat acara dan kegiatan yang berlangsung pada hari tersebut.', + cardTopRatio: 0.58, + arrowDirection: 'up', + arrowOffset: 0.5, + }, + { + title: 'Indikator Dot', + description: 'Titik ungu menandakan ada acara divisi, titik biru-abu menandakan ada kegiatan pada tanggal tersebut.', + cardTopRatio: 0.63, + arrowDirection: 'up', + arrowOffset: 0.12, + }, + { + title: 'Detail Event', + description: 'Ketuk salah satu acara atau kegiatan di bawah untuk melihat detail lengkapnya.', + cardTopRatio: 0.35, + arrowDirection: 'down', + arrowOffset: 0.5, + }, +]; + +export const GUIDE_DIVISION_DISCUSSION: GuideStep[] = [ + { + title: 'Diskusi Divisi', + description: 'Halaman ini menampilkan semua topik diskusi dalam divisi. Setiap card menunjukkan pembuat, deskripsi singkat, jumlah komentar, dan status diskusi.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, + { + title: 'Buka Diskusi', + description: 'Ketuk salah satu diskusi untuk membaca detail dan ikut berkomentar.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, +]; + +export const GUIDE_DIVISION_DOCUMENT: GuideStep[] = [ + { + title: 'Dokumen Divisi', + description: 'Halaman ini menampilkan semua file dan folder dokumen milik divisi kamu.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, + { + title: 'Buka File atau Folder', + description: 'Ketuk folder untuk masuk ke dalamnya, atau ketuk file untuk langsung membukanya.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, + { + title: 'Pilih & Kelola', + description: 'Centang file atau folder untuk memilihnya, lalu gunakan menu di bawah untuk menghapus, mengganti nama, atau membagikan ke divisi lain.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, +]; + +export const GUIDE_DIVISION_CALENDAR: GuideStep[] = [ + { + title: 'Kalender Divisi', + description: 'Halaman ini menampilkan semua acara yang ada di divisi kamu dalam satu kalender.', + cardTopRatio: 0.50, + arrowDirection: 'none', + arrowOffset: 0.5, + }, + { + title: 'Indikator Acara', + description: 'Garis di bawah tanggal menandakan ada acara pada hari tersebut. Ketuk tanggal untuk melihat daftarnya.', + cardTopRatio: 0.50, + arrowDirection: 'none', + arrowOffset: 0.55, + }, + { + title: 'Detail Acara', + description: 'Ketuk salah satu acara di bawah kalender untuk melihat detail lengkapnya.', + cardTopRatio: 0.35, + arrowDirection: 'down', + arrowOffset: 0.55, + }, +]; + +export const GUIDE_DISCUSSION: GuideStep[] = [ + { + title: 'Daftar Diskusi', + description: 'Halaman ini menampilkan semua topik diskusi. Setiap card menunjukkan judul, deskripsi singkat, jumlah komentar, dan status diskusi.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, + { + title: 'Buka Diskusi', + description: 'Ketuk salah satu diskusi untuk membaca detail dan ikut berkomentar.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, +]; + +export const GUIDE_ANNOUNCEMENT: GuideStep[] = [ + { + title: 'Daftar Pengumuman', + description: 'Halaman ini menampilkan semua pengumuman yang ada di desamu.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, + { + title: 'Detail Pengumuman', + description: 'Ketuk salah satu pengumuman untuk membaca isi lengkapnya.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, +]; + +export const GUIDE_MEMBER: GuideStep[] = [ + { + title: 'Daftar Anggota', + description: 'Halaman ini menampilkan semua anggota desa. Gunakan tab untuk memfilter anggota aktif atau tidak aktif.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, + { + title: 'Detail Anggota', + description: 'Ketuk salah satu anggota untuk melihat informasi lengkapnya seperti NIK, jabatan, lembaga, dan kontak.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, +]; + +export const GUIDE_POSITION: GuideStep[] = [ + { + title: 'Daftar Jabatan', + description: 'Halaman ini menampilkan semua jabatan yang terdaftar. Gunakan tab untuk memfilter jabatan aktif atau tidak aktif.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, + { + title: 'Menu Aksi', + description: 'Ketuk salah satu jabatan untuk membuka menu aksi — kamu bisa mengaktifkan/menonaktifkan atau mengedit nama jabatan.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, +]; + +export const GUIDE_GROUP: GuideStep[] = [ + { + title: 'Daftar Lembaga Desa', + description: 'Halaman ini menampilkan semua lembaga desa yang terdaftar. Gunakan tab untuk memfilter yang aktif atau tidak aktif.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, + { + title: 'Menu Aksi', + description: 'Ketuk salah satu lembaga untuk membuka menu aksi — kamu bisa mengaktifkan/menonaktifkan atau mengedit nama lembaga.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, +]; + +export const GUIDE_BANNER: GuideStep[] = [ + { + title: 'Daftar Banner', + description: 'Halaman ini menampilkan semua banner yang ada di desa kamu.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, + { + title: 'Buka Menu Aksi', + description: 'Ketuk salah satu banner untuk membuka menu aksi — kamu bisa melihat, mengedit, atau menghapus banner.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, + { + title: 'Tambah Banner', + description: 'Ketuk tombol tambah di pojok kanan atas untuk menambahkan banner baru.', + cardTopRatio: 0.15, + arrowDirection: 'up', + arrowOffset: 1.05, + }, +]; + +export const GUIDE_PROJECT_DETAIL: GuideStep[] = [ + { + title: 'Detail Kegiatan', + description: 'Halaman ini menampilkan informasi lengkap sebuah kegiatan, mulai dari progress, tugas, file, hingga anggota.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, + { + title: 'Progress Kegiatan', + description: 'Bar ini menunjukkan seberapa banyak tugas yang sudah diselesaikan dari total tugas yang ada.', + cardTopRatio: 0.28, + arrowDirection: 'up', + arrowOffset: 0.5, + }, + { + title: 'Daftar Tugas', + description: 'Semua tugas dalam kegiatan ini ditampilkan di sini. Ketuk tugas untuk melihat detail atau mengubah statusnya.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, + { + title: 'File & Tautan', + description: 'File dan tautan pendukung kegiatan tersedia di bagian bawah. Scroll ke bawah untuk mengaksesnya.', + cardTopRatio: 0.35, + arrowDirection: 'none', + }, +]; diff --git a/lib/useGuide.ts b/lib/useGuide.ts new file mode 100644 index 0000000..5d67382 --- /dev/null +++ b/lib/useGuide.ts @@ -0,0 +1,28 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { useEffect, useState } from 'react'; + +const KEY_PREFIX = '@guide:'; + +export function useGuide(featureKey: string) { + const [visible, setVisible] = useState(false); + const [checked, setChecked] = useState(false); + + useEffect(() => { + AsyncStorage.getItem(KEY_PREFIX + featureKey).then((val) => { + if (!val) setVisible(true); + setChecked(true); + }); + }, [featureKey]); + + async function dismiss() { + setVisible(false); + await AsyncStorage.setItem(KEY_PREFIX + featureKey, 'done'); + } + + async function reset() { + await AsyncStorage.removeItem(KEY_PREFIX + featureKey); + setVisible(true); + } + + return { visible: checked && visible, dismiss, reset }; +} diff --git a/package.json b/package.json index bb9c889..94f34bf 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "expo-modules-core": "^2.5.0", "expo-notifications": "~0.31.3", "expo-router": "~5.1.4", + "expo-secure-store": "~14.2.4", "expo-sharing": "^13.1.5", "expo-splash-screen": "~0.30.8", "expo-status-bar": "~2.2.3",