diff --git a/app/(application)/(user)/forum/[id]/forumku.tsx b/app/(application)/(user)/forum/[id]/forumku.tsx index c51d8ed..d82962f 100644 --- a/app/(application)/(user)/forum/[id]/forumku.tsx +++ b/app/(application)/(user)/forum/[id]/forumku.tsx @@ -1,142 +1,12 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { - AvatarComp, - ButtonCustom, - CenterCustom, - DrawerCustom, - FloatingButton, - Grid, - LoaderCustom, - StackCustom, - TextCustom, - ViewWrapper, -} from "@/components"; -import { useAuth } from "@/hooks/use-auth"; -import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection"; -import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda"; -import { apiForumGetAll } from "@/service/api-client/api-forum"; -import { apiUser } from "@/service/api-client/api-user"; -import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; -import _ from "lodash"; -import { useCallback, useState } from "react"; +import View_Forumku from "@/screens/Forum/ViewForumku"; +import View_Forumku2 from "@/screens/Forum/ViewForumku2"; export default function Forumku() { - const { id } = useLocalSearchParams(); - const { user } = useAuth(); - const [openDrawer, setOpenDrawer] = useState(false); - const [status, setStatus] = useState(""); - const [listData, setListData] = useState(null); - const [dataUser, setDataUser] = useState(null); - const [loadingGetList, setLoadingGetList] = useState(false); - - useFocusEffect( - useCallback(() => { - onLoadData(); - onLoadDataProfile(id as string); - }, [id]) - ); - - const onLoadDataProfile = async (id: string) => { - try { - const response = await apiUser(id); - - setDataUser(response.data); - } catch (error) { - console.log("[ERROR]", error); - } finally { - } - }; - - const onLoadData = async () => { - try { - setLoadingGetList(true); - const response = await apiForumGetAll({ - search: "", - authorId: id as string, - }); - - setListData(response.data); - } catch (error) { - console.log("[ERROR]", error); - } finally { - setLoadingGetList(false); - } - }; - return ( <> - - router.navigate("/(application)/(user)/forum/create") - } - /> - ) - } - > - - - - - - - - - @{dataUser?.username || "-"} - - {listData?.length || "0"} postingan - - - - Kunjungi Profile - - - - {loadingGetList ? ( - - ) : _.isEmpty(listData) ? ( - Tidak ada diskusi - ) : ( - <> - {listData?.map((item: any, index: number) => ( - { - setOpenDrawer(value.setOpenDrawer); - setStatus(value.setStatus); - }} - /> - ))} - - )} - - - - {/* Drawer Komponen Eksternal */} - setOpenDrawer(false)} - > - { - setOpenDrawer(false); - }} - authorId={id as string} - /> - + {/* */} + ); } diff --git a/app/(application)/(user)/forum/index.tsx b/app/(application)/(user)/forum/index.tsx index e006a53..a4fe51e 100644 --- a/app/(application)/(user)/forum/index.tsx +++ b/app/(application)/(user)/forum/index.tsx @@ -1,129 +1,12 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { - AvatarComp, - BackButton, - DrawerCustom, - LoaderCustom, - SearchInput, - TextCustom, - ViewWrapper, -} from "@/components"; -import FloatingButton from "@/components/Button/FloatingButton"; -import { useAuth } from "@/hooks/use-auth"; -import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection"; -import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda"; -import { apiForumGetAll } from "@/service/api-client/api-forum"; -import { apiUser } from "@/service/api-client/api-user"; -import { router, Stack, useFocusEffect } from "expo-router"; -import _ from "lodash"; -import { useCallback, useState } from "react"; +import Forum_ViewBeranda from "@/screens/Forum/ViewBeranda"; +import Forum_ViewBeranda2 from "@/screens/Forum/ViewBeranda2"; export default function Forum() { - const [openDrawer, setOpenDrawer] = useState(false); - const [status, setStatus] = useState(""); - const { user } = useAuth(); - const [dataUser, setDataUser] = useState(); - const [listData, setListData] = useState(); - const [loadingGetList, setLoadingGetList] = useState(false); - const [search, setSearch] = useState(""); - const [dataId, setDataId] = useState(""); - const [authorId, setAuthorId] = useState(""); - - useFocusEffect( - useCallback(() => { - onLoadData(); - onLoadDataProfile(user?.id as string); - }, [user?.id, search]) - ); - - const onLoadDataProfile = async (id: string) => { - const response = await apiUser(id); - setDataUser(response.data); - }; - - const onLoadData = async () => { - try { - setLoadingGetList(true); - const response = await apiForumGetAll({ search: search }); - - setListData(response.data); - } catch (error) { - console.log("[ERROR]", error); - } finally { - setLoadingGetList(false); - } - }; - return ( <> - , - headerRight: () => ( - - ), - }} - /> - - setSearch(e)} - /> - } - floatingButton={ - - router.navigate("/(application)/(user)/forum/create") - } - /> - } - > - {loadingGetList ? ( - - ) : _.isEmpty(listData) ? ( - - Tidak ada diskusi - - ) : ( - listData?.map((e: any, i: number) => ( - { - setDataId(e.id); - setOpenDrawer(true); - setStatus(e.ForumMaster_StatusPosting?.status); - setAuthorId(e.Author?.id); - }} - isTruncate={true} - href={`/forum/${e.id}`} - isRightComponent={false} - /> - )) - )} - - - setOpenDrawer(false)} - > - { - setOpenDrawer(false); - }} - /> - + {/* */} + ); } diff --git a/bun.lock b/bun.lock index 4419dcc..ff0b3c3 100644 --- a/bun.lock +++ b/bun.lock @@ -29,6 +29,7 @@ "expo-haptics": "~15.0.7", "expo-image": "~3.0.8", "expo-image-picker": "~17.0.8", + "expo-linear-gradient": "~15.0.7", "expo-linking": "~8.0.8", "expo-location": "~19.0.7", "expo-notifications": "^0.32.13", @@ -39,6 +40,7 @@ "expo-system-ui": "~6.0.7", "expo-web-browser": "~15.0.9", "lodash": "^4.17.21", + "moti": "^0.30.0", "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.4", @@ -340,6 +342,10 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], + "@emotion/is-prop-valid": ["@emotion/is-prop-valid@0.8.8", "", { "dependencies": { "@emotion/memoize": "0.7.4" } }, "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA=="], + + "@emotion/memoize": ["@emotion/memoize@0.7.4", "", {}, "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], @@ -478,6 +484,18 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + "@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="], + + "@motionone/dom": ["@motionone/dom@10.12.0", "", { "dependencies": { "@motionone/animation": "^10.12.0", "@motionone/generators": "^10.12.0", "@motionone/types": "^10.12.0", "@motionone/utils": "^10.12.0", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw=="], + + "@motionone/easing": ["@motionone/easing@10.18.0", "", { "dependencies": { "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg=="], + + "@motionone/generators": ["@motionone/generators@10.18.0", "", { "dependencies": { "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg=="], + + "@motionone/types": ["@motionone/types@10.17.1", "", {}, "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A=="], + + "@motionone/utils": ["@motionone/utils@10.18.0", "", { "dependencies": { "@motionone/types": "^10.17.1", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" } }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="], "@nicolo-ribaudo/chokidar-2": ["@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3", "", {}, "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ=="], @@ -1226,6 +1244,8 @@ "expo-keep-awake": ["expo-keep-awake@15.0.7", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-CgBNcWVPnrIVII5G54QDqoE125l+zmqR4HR8q+MQaCfHet+dYpS5vX5zii/RMayzGN4jPgA4XYIQ28ePKFjHoA=="], + "expo-linear-gradient": ["expo-linear-gradient@15.0.7", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-yF+y+9Shpr/OQFfy/wglB/0bykFMbwHBTuMRa5Of/r2P1wbkcacx8rg0JsUWkXH/rn2i2iWdubyqlxSJa3ggZA=="], + "expo-linking": ["expo-linking@8.0.8", "", { "dependencies": { "expo-constants": "~18.0.8", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MyeMcbFDKhXh4sDD1EHwd0uxFQNAc6VCrwBkNvvvufUsTYFq3glTA9Y8a+x78CPpjNqwNAamu74yIaIz7IEJyg=="], "expo-location": ["expo-location@19.0.7", "", { "peerDependencies": { "expo": "*" } }, "sha512-YNkh4r9E6ECbPkBCAMG5A5yHDgS0pw+Rzyd0l2ZQlCtjkhlODB55nMCKr5CZnUI0mXTkaSm8CwfoCO8n2MpYfg=="], @@ -1304,6 +1324,10 @@ "form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="], + "framer-motion": ["framer-motion@6.5.1", "", { "dependencies": { "@motionone/dom": "10.12.0", "framesync": "6.0.1", "hey-listen": "^1.0.8", "popmotion": "11.0.3", "style-value-types": "5.0.0", "tslib": "^2.1.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": ">=16.8 || ^17.0.0 || ^18.0.0", "react-dom": ">=16.8 || ^17.0.0 || ^18.0.0" } }, "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw=="], + + "framesync": ["framesync@6.0.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA=="], + "freeport-async": ["freeport-async@2.0.0", "", {}, "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ=="], "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], @@ -1380,6 +1404,8 @@ "hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="], + "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="], + "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], "hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], @@ -1734,6 +1760,8 @@ "mkdirp": ["mkdirp@3.0.1", "", { "bin": "dist/cjs/src/bin.js" }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], + "moti": ["moti@0.30.0", "", { "dependencies": { "framer-motion": "^6.5.1" }, "peerDependencies": { "react-native-reanimated": "*" } }, "sha512-YN78mcefo8kvJaL+TZNyusq6YA2aMFvBPl8WiLPy4eb4wqgOFggJOjP9bUr2YO8PrAt0uusmRG8K4RPL4OhCsA=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], @@ -1850,6 +1878,8 @@ "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], + "popmotion": ["popmotion@11.0.3", "", { "dependencies": { "framesync": "6.0.1", "hey-listen": "^1.0.8", "style-value-types": "5.0.0", "tslib": "^2.1.0" } }, "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA=="], + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], "postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="], @@ -2162,6 +2192,8 @@ "structured-headers": ["structured-headers@0.4.1", "", {}, "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg=="], + "style-value-types": ["style-value-types@5.0.0", "", { "dependencies": { "hey-listen": "^1.0.8", "tslib": "^2.1.0" } }, "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA=="], + "styleq": ["styleq@0.1.3", "", {}, "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA=="], "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], diff --git a/components/_ShareComponent/NewWrapper.tsx b/components/_ShareComponent/NewWrapper.tsx new file mode 100644 index 0000000..9f0586d --- /dev/null +++ b/components/_ShareComponent/NewWrapper.tsx @@ -0,0 +1,188 @@ +// @/components/NewWrapper.tsx +import { MainColor } from "@/constants/color-palet"; +import { OS_HEIGHT } from "@/constants/constans-value"; +import { GStyles } from "@/styles/global-styles"; +import { + ImageBackground, + Keyboard, + KeyboardAvoidingView, + Platform, + ScrollView, + FlatList, + TouchableWithoutFeedback, + View, + StyleProp, + ViewStyle, +} from "react-native"; +import { + NativeSafeAreaViewProps, + SafeAreaView, +} from "react-native-safe-area-context"; +import type { ScrollViewProps, FlatListProps } from "react-native"; + +// --- ✅ Tambahkan refreshControl ke BaseProps --- +interface BaseProps { + withBackground?: boolean; + headerComponent?: React.ReactNode; + footerComponent?: React.ReactNode; + floatingButton?: React.ReactNode; + hideFooter?: boolean; + edgesFooter?: NativeSafeAreaViewProps["edges"]; + style?: StyleProp; + refreshControl?: ScrollViewProps["refreshControl"]; // ✅ dipakai di kedua mode +} + +interface StaticModeProps extends BaseProps { + children: React.ReactNode; + listData?: never; + renderItem?: never; +} + +interface ListModeProps extends BaseProps { + children?: never; + listData: any[]; + renderItem: FlatListProps["renderItem"]; + onEndReached?: () => void; + // ✅ Gunakan tipe yang kompatibel dengan FlatList + ListHeaderComponent?: React.ReactElement | null; + ListFooterComponent?: React.ReactElement | null; + ListEmptyComponent?: React.ReactElement | null; + keyExtractor?: FlatListProps["keyExtractor"]; +} + +type NewWrapperProps = StaticModeProps | ListModeProps; + +const NewWrapper = (props: NewWrapperProps) => { + const { + withBackground = false, + headerComponent, + footerComponent, + floatingButton, + hideFooter = false, + edgesFooter = [], + style, + refreshControl, // ✅ sekarang ada di BaseProps + } = props; + + const assetBackground = require("../../assets/images/main-background.png"); + + const renderContainer = (content: React.ReactNode) => { + if (withBackground) { + return ( + + + {content} + + + ); + } + return {content}; + }; + + // 🔹 Mode Dinamis + if ("listData" in props) { + const listProps = props as ListModeProps; + + return ( + + {headerComponent && ( + {headerComponent} + )} + + { + if (item.id == null) { + console.warn("Item tanpa 'id':", item); + return `fallback-${JSON.stringify(item)}`; + } + return String(item.id); + }) + } + + refreshControl={refreshControl} // ✅ dari BaseProps + onEndReached={listProps.onEndReached} + onEndReachedThreshold={0.5} + ListHeaderComponent={listProps.ListHeaderComponent} + ListFooterComponent={listProps.ListFooterComponent} + ListEmptyComponent={listProps.ListEmptyComponent} + contentContainerStyle={{ flexGrow: 1 }} + keyboardShouldPersistTaps="handled" + /> + + + {footerComponent ? ( + + {footerComponent} + + ) : hideFooter ? null : ( + + )} + + {floatingButton && ( + {floatingButton} + )} + + ); + } + + // 🔹 Mode Statis + const staticProps = props as StaticModeProps; + + return ( + + {headerComponent && ( + {headerComponent} + )} + + + + {renderContainer(staticProps.children)} + + + + {footerComponent ? ( + + {footerComponent} + + ) : hideFooter ? null : ( + + )} + + {floatingButton && ( + {floatingButton} + )} + + ); +}; + +export default NewWrapper; diff --git a/components/_ShareComponent/SkeletonCustom.tsx b/components/_ShareComponent/SkeletonCustom.tsx new file mode 100644 index 0000000..89b8ee3 --- /dev/null +++ b/components/_ShareComponent/SkeletonCustom.tsx @@ -0,0 +1,59 @@ +// components/CustomSkeleton.tsx +import React from "react"; +import { View, StyleProp, ViewStyle, DimensionValue } from "react-native"; +import { MotiView } from "moti"; +import { AccentColor, MainColor } from "@/constants/color-palet"; + +interface CustomSkeletonProps { + isLoading?: boolean; + style?: StyleProp; + width?: DimensionValue; + height?: DimensionValue; + radius?: number; +} + +const CustomSkeleton: React.FC = ({ + isLoading = true, + style, + width = "100%", + height = 16, + radius = 8, +}) => { + if (!isLoading) return null; + + return ( + + + + ); +}; + +export default CustomSkeleton; diff --git a/components/_ShareComponent/ViewWrapper.tsx b/components/_ShareComponent/ViewWrapper.tsx index afd2d1d..1c189e7 100644 --- a/components/_ShareComponent/ViewWrapper.tsx +++ b/components/_ShareComponent/ViewWrapper.tsx @@ -11,6 +11,7 @@ import { View, StyleProp, ViewStyle, + ScrollViewProps, } from "react-native"; import { NativeSafeAreaViewProps, SafeAreaView } from "react-native-safe-area-context"; @@ -23,6 +24,7 @@ interface ViewWrapperProps { hideFooter?: boolean; edgesFooter?: NativeSafeAreaViewProps["edges"]; style?: StyleProp; + refreshControl?: ScrollViewProps["refreshControl"]; } /** @@ -40,6 +42,7 @@ const ViewWrapper = ({ hideFooter = false, edgesFooter =[], style, + refreshControl, }: ViewWrapperProps) => { const assetBackground = require("../../assets/images/main-background.png"); @@ -57,6 +60,7 @@ const ViewWrapper = ({ diff --git a/package.json b/package.json index 4d0106a..549397c 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "expo-haptics": "~15.0.7", "expo-image": "~3.0.8", "expo-image-picker": "~17.0.8", + "expo-linear-gradient": "~15.0.7", "expo-linking": "~8.0.8", "expo-location": "~19.0.7", "expo-notifications": "^0.32.13", @@ -46,6 +47,7 @@ "expo-system-ui": "~6.0.7", "expo-web-browser": "~15.0.9", "lodash": "^4.17.21", + "moti": "^0.30.0", "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.4", diff --git a/screens/Forum/ViewBeranda.tsx b/screens/Forum/ViewBeranda.tsx new file mode 100644 index 0000000..85bcb36 --- /dev/null +++ b/screens/Forum/ViewBeranda.tsx @@ -0,0 +1,108 @@ +import { + BackButton, + AvatarComp, + ViewWrapper, + SearchInput, + FloatingButton, + LoaderCustom, + TextCustom, +} from "@/components"; +import { useAuth } from "@/hooks/use-auth"; +import { apiForumGetAll } from "@/service/api-client/api-forum"; +import { apiUser } from "@/service/api-client/api-user"; +import { Stack, router } from "expo-router"; +import _ from "lodash"; +import { useState, useEffect } from "react"; +import { RefreshControl } from "react-native"; +import Forum_BoxDetailSection from "./DiscussionBoxSection"; + +export default function Forum_ViewBeranda() { + const { user } = useAuth(); + const [dataUser, setDataUser] = useState(); + const [listData, setListData] = useState(); + const [loadingGetList, setLoadingGetList] = useState(false); + const [search, setSearch] = useState(""); + + useEffect(() => { + onLoadData(); + onLoadDataProfile(user?.id as string); + }, [user?.id, search]); + + const onLoadDataProfile = async (id: string) => { + const response = await apiUser(id); + setDataUser(response.data); + }; + + const onLoadData = async () => { + try { + setLoadingGetList(true); + const response = await apiForumGetAll({ + category: "beranda", + search: search, + userLoginId: user?.id, + }); + + setListData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingGetList(false); + } + }; + + return ( + <> + , + headerRight: () => ( + + ), + }} + /> + + setSearch(e)} + /> + } + floatingButton={ + + router.navigate("/(application)/(user)/forum/create") + } + /> + } + refreshControl={ + + } + > + {loadingGetList ? ( + + ) : _.isEmpty(listData) ? ( + + Tidak ada diskusi + + ) : ( + listData?.map((e: any, i: number) => ( + {}} + isTruncate={true} + href={`/forum/${e.id}`} + isRightComponent={false} + /> + )) + )} + + + ); +} diff --git a/screens/Forum/ViewBeranda2.tsx b/screens/Forum/ViewBeranda2.tsx new file mode 100644 index 0000000..3a76141 --- /dev/null +++ b/screens/Forum/ViewBeranda2.tsx @@ -0,0 +1,215 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + AvatarComp, + BackButton, + FloatingButton, + LoaderCustom, + SearchInput, + StackCustom, + TextCustom, // ← gunakan NewWrapper yang sudah diperbaiki +} from "@/components"; +import SkeletonCustom from "@/components/_ShareComponent/SkeletonCustom"; +import NewWrapper from "@/components/_ShareComponent/NewWrapper"; +import { useAuth } from "@/hooks/use-auth"; +import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection"; +import { apiForumGetAll } from "@/service/api-client/api-forum"; +import { apiUser } from "@/service/api-client/api-user"; +import { router, Stack } from "expo-router"; +import _ from "lodash"; +import { useCallback, useEffect, useState } from "react"; +import { RefreshControl, View } from "react-native"; +import { MainColor } from "@/constants/color-palet"; + +// Sesuai dengan `takeData = 5` di API-mu +const PAGE_SIZE = 5; + +export default function Forum_ViewBeranda2() { + const { user } = useAuth(); + const [dataUser, setDataUser] = useState(null); + const [listData, setListData] = useState([]); + const [loading, setLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [page, setPage] = useState(1); + const [search, setSearch] = useState(""); + + // 🔹 Load data profil user sekali + useEffect(() => { + if (user?.id) { + apiUser(user.id).then((res) => setDataUser(res.data)); + } + }, [user?.id]); + + // 🔹 Reset dan muat ulang saat search atau user berubah + useEffect(() => { + setPage(1); + setListData([]); + setHasMore(true); + fetchData(1, true); + }, [search, user?.id]); + + // 🔹 Fungsi fetch data + const fetchData = async (pageNumber: number, clear: boolean) => { + if (!user?.id) return; + + // Cegah multiple call + if (!clear && (loading || refreshing)) return; + + const isRefresh = clear; + if (isRefresh) setRefreshing(true); + if (!isRefresh) setLoading(true); + + try { + const response = await apiForumGetAll({ + category: "beranda", + search: search || "", + userLoginId: user.id, + page: String(pageNumber), // API terima string + }); + + const newData = response.data || []; + setListData((prev) => { + const current = Array.isArray(prev) ? prev : []; + return clear ? newData : [...current, ...newData]; + }); + setHasMore(newData.length === PAGE_SIZE); + setPage(pageNumber); + } catch (error) { + console.error("[ERROR] Fetch forum:", error); + setHasMore(false); + } finally { + setRefreshing(false); + setLoading(false); + } + }; + + // 🔹 Pull-to-refresh + const onRefresh = useCallback(() => { + fetchData(1, true); + }, [search, user?.id]); + + // 🔹 Infinite scroll + const loadMore = useCallback(() => { + if (hasMore && !loading && !refreshing) { + fetchData(page + 1, false); + } + }, [hasMore, loading, refreshing, page, search, user?.id]); + + // 🔹 Render item forum + const renderForumItem = ({ item }: { item: any }) => ( + {}} + isTruncate={true} + href={`/forum/${item.id}`} + isRightComponent={false} + /> + ); + + // 🔹 Komponen Header List (di dalam FlatList) + const ListHeaderComponent = ( + + Diskusi Terbaru + + ); + + // 🔹 Komponen Footer List (loading indicator) + const ListFooterComponent = + loading && !refreshing && listData.length > 0 ? ( + + {/* Memuat diskusi... */} + + + ) : null; + + // Skeleton List (untuk initial load) + const SkeletonListComponent = () => ( + + + {Array.from({ length: 5 }).map((_, i) => ( + + ))} + + + ); + + // Komponen Empty + const EmptyComponent = () => ( + + + {search ? "Tidak ada hasil pencarian" : "Tidak ada diskusi"} + + + ); + + return ( + <> + {/* 🔹 Header Navigation */} + , + headerRight: () => ( + + ), + }} + /> + + {/* 🔹 NewWrapper dalam mode list */} + + setSearch(text), 500)} + // value={search} + /> + + } + // Floating action button + floatingButton={ + + router.navigate("/(application)/(user)/forum/create") + } + /> + } + // --- Mode List Props --- + listData={listData} + renderItem={renderForumItem} + refreshControl={ + + } + onEndReached={loadMore} + // ListHeaderComponent={ListHeaderComponent} + ListFooterComponent={ListFooterComponent} + ListEmptyComponent={ + _.isEmpty(listData) ? : + } + // ------------------------ + /> + + ); +} diff --git a/screens/Forum/ViewForumku.tsx b/screens/Forum/ViewForumku.tsx new file mode 100644 index 0000000..db35228 --- /dev/null +++ b/screens/Forum/ViewForumku.tsx @@ -0,0 +1,144 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + AvatarComp, + ButtonCustom, + CenterCustom, + DrawerCustom, + FloatingButton, + Grid, + LoaderCustom, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { useAuth } from "@/hooks/use-auth"; +import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection"; +import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda"; +import { apiForumGetAll } from "@/service/api-client/api-forum"; +import { apiUser } from "@/service/api-client/api-user"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function View_Forumku() { + const { id } = useLocalSearchParams(); + const { user } = useAuth(); + const [openDrawer, setOpenDrawer] = useState(false); + const [status, setStatus] = useState(""); + const [listData, setListData] = useState(null); + const [dataUser, setDataUser] = useState(null); + const [loadingGetList, setLoadingGetList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + onLoadDataProfile(id as string); + }, [id]) + ); + + const onLoadDataProfile = async (id: string) => { + try { + const response = await apiUser(id); + + setDataUser(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + } + }; + + const onLoadData = async () => { + try { + setLoadingGetList(true); + const response = await apiForumGetAll({ + search: "", + authorId: id as string, + category: "forumku", + }); + + setListData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingGetList(false); + } + }; + + return ( + <> + + router.navigate("/(application)/(user)/forum/create") + } + /> + ) + } + > + + + + + + + + + @{dataUser?.username || "-"} + + {listData?.length || "0"} postingan + + + + Kunjungi Profile + + + + {loadingGetList ? ( + + ) : _.isEmpty(listData) ? ( + Tidak ada diskusi + ) : ( + <> + {listData?.map((item: any, index: number) => ( + { + setOpenDrawer(value.setOpenDrawer); + setStatus(value.setStatus); + }} + /> + ))} + + )} + + + + {/* Drawer Komponen Eksternal */} + setOpenDrawer(false)} + > + { + setOpenDrawer(false); + }} + authorId={id as string} + authorUsername={dataUser?.username} + /> + + + ); +} diff --git a/screens/Forum/ViewForumku2.tsx b/screens/Forum/ViewForumku2.tsx new file mode 100644 index 0000000..5c758e2 --- /dev/null +++ b/screens/Forum/ViewForumku2.tsx @@ -0,0 +1,215 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + AvatarComp, + ButtonCustom, + CenterCustom, + FloatingButton, + Grid, + LoaderCustom, + Spacing, + StackCustom, + TextCustom, +} from "@/components"; +import NewWrapper from "@/components/_ShareComponent/NewWrapper"; +import NoDataText from "@/components/_ShareComponent/NoDataText"; +import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom"; +import { MainColor } from "@/constants/color-palet"; +import { useAuth } from "@/hooks/use-auth"; +import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection"; +import { apiForumGetAll } from "@/service/api-client/api-forum"; +import { apiUser } from "@/service/api-client/api-user"; +import { router, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useCallback, useEffect, useState } from "react"; +import { RefreshControl, View } from "react-native"; + +const PAGE_SIZE = 5; + +export default function View_Forumku2() { + const { id } = useLocalSearchParams(); + const { user } = useAuth(); + const [listData, setListData] = useState([]); + const [dataUser, setDataUser] = useState(null); + const [loading, setLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [page, setPage] = useState(1); + const [count, setCount] = useState(0); + + useEffect(() => { + onLoadDataProfile(id as string); + }, [id]); + + useEffect(() => { + setPage(1); + setListData([]); + setHasMore(true); + fetchData(1, true); + }, [user?.id]); + + const onLoadDataProfile = async (id: string) => { + try { + const response = await apiUser(id); + + setDataUser(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + } + }; + + // 🔹 Fungsi fetch data + const fetchData = async (pageNumber: number, clear: boolean) => { + if (!user?.id) return; + + // Cegah multiple call + if (!clear && (loading || refreshing)) return; + + const isRefresh = clear; + if (isRefresh) setRefreshing(true); + if (!isRefresh) setLoading(true); + + try { + const response = await apiForumGetAll({ + category: "forumku", + authorId: id as string, + userLoginId: user.id, + page: String(pageNumber), // API terima string + }); + + const newData = response.data.data || []; + setListData((prev) => { + const current = Array.isArray(prev) ? prev : []; + return clear ? newData : [...current, ...newData]; + }); + setHasMore(newData.length === PAGE_SIZE); + setPage(pageNumber); + setCount(response.data.count); + } catch (error) { + console.error("[ERROR] Fetch forum:", error); + setHasMore(false); + } finally { + setRefreshing(false); + setLoading(false); + } + }; + + // 🔹 Pull-to-refresh + const onRefresh = useCallback(() => { + fetchData(1, true); + }, [user?.id]); + + // 🔹 Infinite scroll + const loadMore = useCallback(() => { + if (hasMore && !loading && !refreshing) { + fetchData(page + 1, false); + } + }, [hasMore, loading, refreshing, page, user?.id]); + + const randerHeaderComponent = () => ( + <> + + + + + + + + @{dataUser?.username || "-"} + + {count || "0"} postingan + + + + Kunjungi Profile + + + + + + ); + + const renderList = ({ item }: { item: any }) => ( + {}} + isTruncate={true} + href={`/forum/${item.id}`} + isRightComponent={false} + /> + ); + + // Skeleton List (untuk initial load) + const SkeletonListComponent = () => ( + + + {Array.from({ length: 5 }).map((_, i) => ( + + ))} + + + ); + + // Komponen Empty + const EmptyComponent = () => ( + + + + ); + + // 🔹 Komponen Footer List (loading indicator) + const ListFooterComponent = + loading && !refreshing && listData.length > 0 ? ( + + {/* Memuat diskusi... */} + + + ) : null; + + return ( + <> + + router.navigate("/(application)/(user)/forum/create") + } + /> + ) + } + listData={listData} + renderItem={renderList} + refreshControl={ + + } + onEndReached={loadMore} + ListHeaderComponent={randerHeaderComponent()} + ListFooterComponent={ListFooterComponent} + ListEmptyComponent={ + _.isEmpty(listData) ? : + } + /> + + ); +} diff --git a/screens/Home/homeViewStyle.tsx b/screens/Home/homeViewStyle.tsx index 9732a74..dac67ff 100644 --- a/screens/Home/homeViewStyle.tsx +++ b/screens/Home/homeViewStyle.tsx @@ -59,11 +59,30 @@ export const stylesHome = StyleSheet.create({ borderWidth: 2, borderColor: AccentColor.blue, }, + gridItemInactive: { + width: "46%", + height: "100%", + aspectRatio: 1, + backgroundColor: MainColor.darkblue, + borderRadius: 8, + padding: 16, + alignItems: "center", + justifyContent: "center", + marginVertical: 8, + borderWidth: 2, + borderColor: AccentColor.blue, + opacity: 0.7, + }, gridLabel: { marginTop: 8, color: "white", fontWeight: "bold", }, + gridLabelInactive: { + marginTop: 8, + color: "gray", + fontWeight: "bold", + }, jobVacancyContainer: { backgroundColor: MainColor.darkblue, borderRadius: 8, diff --git a/screens/Home/topFeatureSection.tsx b/screens/Home/topFeatureSection.tsx index 8d265b0..8befc82 100644 --- a/screens/Home/topFeatureSection.tsx +++ b/screens/Home/topFeatureSection.tsx @@ -9,21 +9,25 @@ export default function Home_FeatureSection() { name: "Event", icon: , onPress: () => router.push("/(application)/(user)/event/(tabs)"), + status: "active", }, { name: "Collaboration", - icon: , + icon: , onPress: () => router.push("/(application)/(user)/collaboration/(tabs)"), + status: "inactive", }, { name: "Voting", icon: , onPress: () => router.push("/(application)/(user)/voting/(tabs)"), + status: "active", }, { name: "Crowdfunding", icon: , onPress: () => router.push("/(application)/(user)/crowdfunding"), + status: "active", }, ]; @@ -33,11 +37,12 @@ export default function Home_FeatureSection() { {listFeature.map((item, index) => ( {item.icon} - {item.name} + {item.name} ))} diff --git a/service/api-client/api-forum.ts b/service/api-client/api-forum.ts index fc1eb9c..9e3c4e3 100644 --- a/service/api-client/api-forum.ts +++ b/service/api-client/api-forum.ts @@ -14,13 +14,22 @@ export async function apiForumCreate({ data }: { data: any }) { export async function apiForumGetAll({ search, authorId, + userLoginId, + category, + page, }: { - search: string; + search?: string; authorId?: string; + userLoginId?: string; + category: "beranda" | "forumku"; + page?: string; }) { - const authorQuery = authorId ? `?authorId=${authorId}` : ""; - const searchQuery = search ? `?search=${search}` : ""; - const query = search ? searchQuery : authorQuery; + const categoryQuery = `?category=${category}`; + const authorQuery = authorId ? `&authorId=${authorId}` : ""; + const userLoginQuery = userLoginId ? `&userLoginId=${userLoginId}` : ""; + const searchQuery = search ? `&search=${search}` : ""; + const pageQuery = page ? `&page=${page}` : ""; + const query = `${categoryQuery}${authorQuery}${userLoginQuery}${searchQuery}${pageQuery}`; try { const response = await apiConfig.get(`/mobile/forum${query}`);