Refresh control dan Blockir user di forum
### No Issue
This commit is contained in:
@@ -1,142 +1,12 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import View_Forumku from "@/screens/Forum/ViewForumku";
|
||||||
AvatarComp,
|
import View_Forumku2 from "@/screens/Forum/ViewForumku2";
|
||||||
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 Forumku() {
|
export default function Forumku() {
|
||||||
const { id } = useLocalSearchParams();
|
|
||||||
const { user } = useAuth();
|
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
|
||||||
const [status, setStatus] = useState("");
|
|
||||||
const [listData, setListData] = useState<any | null>(null);
|
|
||||||
const [dataUser, setDataUser] = useState<any | null>(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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewWrapper
|
{/* <View_Forumku /> */}
|
||||||
floatingButton={
|
<View_Forumku2 />
|
||||||
user?.id === id && (
|
|
||||||
<FloatingButton
|
|
||||||
onPress={() =>
|
|
||||||
router.navigate("/(application)/(user)/forum/create")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StackCustom>
|
|
||||||
<CenterCustom>
|
|
||||||
<AvatarComp
|
|
||||||
fileId={dataUser?.Profile?.imageId}
|
|
||||||
href={`/(application)/(image)/preview-image/${dataUser?.Profile?.imageId}`}
|
|
||||||
size="xl"
|
|
||||||
/>
|
|
||||||
</CenterCustom>
|
|
||||||
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<TextCustom bold truncate>
|
|
||||||
@{dataUser?.username || "-"}
|
|
||||||
</TextCustom>
|
|
||||||
<TextCustom>{listData?.length || "0"} postingan</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6} style={{ alignItems: "flex-end" }}>
|
|
||||||
<ButtonCustom href={`/profile/${dataUser?.Profile?.id}`}>
|
|
||||||
Kunjungi Profile
|
|
||||||
</ButtonCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
{loadingGetList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom> Tidak ada diskusi</TextCustom>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{listData?.map((item: any, index: number) => (
|
|
||||||
<Forum_BoxDetailSection
|
|
||||||
isRightComponent={false}
|
|
||||||
key={index}
|
|
||||||
data={item}
|
|
||||||
isTruncate={true}
|
|
||||||
href={`/forum/${item.id}`}
|
|
||||||
onSetData={(value) => {
|
|
||||||
setOpenDrawer(value.setOpenDrawer);
|
|
||||||
setStatus(value.setStatus);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</StackCustom>
|
|
||||||
</ViewWrapper>
|
|
||||||
|
|
||||||
{/* Drawer Komponen Eksternal */}
|
|
||||||
<DrawerCustom
|
|
||||||
height={"auto"}
|
|
||||||
isVisible={openDrawer}
|
|
||||||
closeDrawer={() => setOpenDrawer(false)}
|
|
||||||
>
|
|
||||||
<Forum_MenuDrawerBerandaSection
|
|
||||||
id={id as string}
|
|
||||||
status={status}
|
|
||||||
setIsDrawerOpen={() => {
|
|
||||||
setOpenDrawer(false);
|
|
||||||
}}
|
|
||||||
authorId={id as string}
|
|
||||||
/>
|
|
||||||
</DrawerCustom>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,129 +1,12 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {
|
import Forum_ViewBeranda from "@/screens/Forum/ViewBeranda";
|
||||||
AvatarComp,
|
import Forum_ViewBeranda2 from "@/screens/Forum/ViewBeranda2";
|
||||||
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";
|
|
||||||
|
|
||||||
export default function Forum() {
|
export default function Forum() {
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
|
||||||
const [status, setStatus] = useState("");
|
|
||||||
const { user } = useAuth();
|
|
||||||
const [dataUser, setDataUser] = useState<any>();
|
|
||||||
const [listData, setListData] = useState<any[]>();
|
|
||||||
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
{/* <Forum_ViewBeranda /> */}
|
||||||
options={{
|
<Forum_ViewBeranda2 />
|
||||||
title: "Forum",
|
|
||||||
headerLeft: () => <BackButton />,
|
|
||||||
headerRight: () => (
|
|
||||||
<AvatarComp
|
|
||||||
fileId={dataUser?.Profile?.imageId}
|
|
||||||
size="base"
|
|
||||||
href={`/forum/${user?.id}/forumku`}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ViewWrapper
|
|
||||||
headerComponent={
|
|
||||||
<SearchInput
|
|
||||||
placeholder="Cari topik diskusi"
|
|
||||||
onChangeText={(e) => setSearch(e)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
floatingButton={
|
|
||||||
<FloatingButton
|
|
||||||
onPress={() =>
|
|
||||||
router.navigate("/(application)/(user)/forum/create")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{loadingGetList ? (
|
|
||||||
<LoaderCustom />
|
|
||||||
) : _.isEmpty(listData) ? (
|
|
||||||
<TextCustom align="center" color="gray">
|
|
||||||
Tidak ada diskusi
|
|
||||||
</TextCustom>
|
|
||||||
) : (
|
|
||||||
listData?.map((e: any, i: number) => (
|
|
||||||
<Forum_BoxDetailSection
|
|
||||||
key={i}
|
|
||||||
data={e}
|
|
||||||
onSetData={() => {
|
|
||||||
setDataId(e.id);
|
|
||||||
setOpenDrawer(true);
|
|
||||||
setStatus(e.ForumMaster_StatusPosting?.status);
|
|
||||||
setAuthorId(e.Author?.id);
|
|
||||||
}}
|
|
||||||
isTruncate={true}
|
|
||||||
href={`/forum/${e.id}`}
|
|
||||||
isRightComponent={false}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ViewWrapper>
|
|
||||||
|
|
||||||
<DrawerCustom
|
|
||||||
height={"auto"}
|
|
||||||
isVisible={openDrawer}
|
|
||||||
closeDrawer={() => setOpenDrawer(false)}
|
|
||||||
>
|
|
||||||
<Forum_MenuDrawerBerandaSection
|
|
||||||
id={dataId}
|
|
||||||
authorId={authorId}
|
|
||||||
status={status}
|
|
||||||
setIsDrawerOpen={() => {
|
|
||||||
setOpenDrawer(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DrawerCustom>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
32
bun.lock
32
bun.lock
@@ -29,6 +29,7 @@
|
|||||||
"expo-haptics": "~15.0.7",
|
"expo-haptics": "~15.0.7",
|
||||||
"expo-image": "~3.0.8",
|
"expo-image": "~3.0.8",
|
||||||
"expo-image-picker": "~17.0.8",
|
"expo-image-picker": "~17.0.8",
|
||||||
|
"expo-linear-gradient": "~15.0.7",
|
||||||
"expo-linking": "~8.0.8",
|
"expo-linking": "~8.0.8",
|
||||||
"expo-location": "~19.0.7",
|
"expo-location": "~19.0.7",
|
||||||
"expo-notifications": "^0.32.13",
|
"expo-notifications": "^0.32.13",
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
"expo-system-ui": "~6.0.7",
|
"expo-system-ui": "~6.0.7",
|
||||||
"expo-web-browser": "~15.0.9",
|
"expo-web-browser": "~15.0.9",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"moti": "^0.30.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.4",
|
"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=="],
|
"@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/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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"@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-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-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=="],
|
"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=="],
|
"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=="],
|
"freeport-async": ["freeport-async@2.0.0", "", {}, "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ=="],
|
||||||
|
|
||||||
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
||||||
|
|||||||
188
components/_ShareComponent/NewWrapper.tsx
Normal file
188
components/_ShareComponent/NewWrapper.tsx
Normal file
@@ -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<ViewStyle>;
|
||||||
|
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<any>["renderItem"];
|
||||||
|
onEndReached?: () => void;
|
||||||
|
// ✅ Gunakan tipe yang kompatibel dengan FlatList
|
||||||
|
ListHeaderComponent?: React.ReactElement | null;
|
||||||
|
ListFooterComponent?: React.ReactElement | null;
|
||||||
|
ListEmptyComponent?: React.ReactElement | null;
|
||||||
|
keyExtractor?: FlatListProps<any>["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 (
|
||||||
|
<ImageBackground
|
||||||
|
source={assetBackground}
|
||||||
|
resizeMode="cover"
|
||||||
|
style={GStyles.imageBackground}
|
||||||
|
>
|
||||||
|
<View style={[GStyles.containerWithBackground, style]}>
|
||||||
|
{content}
|
||||||
|
</View>
|
||||||
|
</ImageBackground>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <View style={[GStyles.container, style]}>{content}</View>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🔹 Mode Dinamis
|
||||||
|
if ("listData" in props) {
|
||||||
|
const listProps = props as ListModeProps;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
|
style={{ flex: 1, backgroundColor: MainColor.darkblue }}
|
||||||
|
>
|
||||||
|
{headerComponent && (
|
||||||
|
<View style={GStyles.stickyHeader}>{headerComponent}</View>
|
||||||
|
)}
|
||||||
|
<View style={[GStyles.container, style]}>
|
||||||
|
<FlatList
|
||||||
|
data={listProps.listData}
|
||||||
|
renderItem={listProps.renderItem}
|
||||||
|
keyExtractor={
|
||||||
|
listProps.keyExtractor ||
|
||||||
|
((item) => {
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{footerComponent ? (
|
||||||
|
<SafeAreaView
|
||||||
|
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]}
|
||||||
|
style={{ backgroundColor: MainColor.darkblue, height: OS_HEIGHT }}
|
||||||
|
>
|
||||||
|
{footerComponent}
|
||||||
|
</SafeAreaView>
|
||||||
|
) : hideFooter ? null : (
|
||||||
|
<SafeAreaView
|
||||||
|
edges={["bottom"]}
|
||||||
|
style={{ backgroundColor: MainColor.darkblue }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{floatingButton && (
|
||||||
|
<View style={GStyles.floatingContainer}>{floatingButton}</View>
|
||||||
|
)}
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔹 Mode Statis
|
||||||
|
const staticProps = props as StaticModeProps;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
|
style={{ flex: 1, backgroundColor: MainColor.darkblue }}
|
||||||
|
>
|
||||||
|
{headerComponent && (
|
||||||
|
<View style={GStyles.stickyHeader}>{headerComponent}</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={{ flexGrow: 1 }}
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
refreshControl={refreshControl} // ✅ sekarang valid
|
||||||
|
>
|
||||||
|
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||||
|
{renderContainer(staticProps.children)}
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{footerComponent ? (
|
||||||
|
<SafeAreaView
|
||||||
|
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]}
|
||||||
|
style={{ backgroundColor: MainColor.darkblue, height: OS_HEIGHT }}
|
||||||
|
>
|
||||||
|
{footerComponent}
|
||||||
|
</SafeAreaView>
|
||||||
|
) : hideFooter ? null : (
|
||||||
|
<SafeAreaView
|
||||||
|
edges={["bottom"]}
|
||||||
|
style={{ backgroundColor: MainColor.darkblue }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{floatingButton && (
|
||||||
|
<View style={GStyles.floatingContainer}>{floatingButton}</View>
|
||||||
|
)}
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewWrapper;
|
||||||
59
components/_ShareComponent/SkeletonCustom.tsx
Normal file
59
components/_ShareComponent/SkeletonCustom.tsx
Normal file
@@ -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<ViewStyle>;
|
||||||
|
width?: DimensionValue;
|
||||||
|
height?: DimensionValue;
|
||||||
|
radius?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomSkeleton: React.FC<CustomSkeletonProps> = ({
|
||||||
|
isLoading = true,
|
||||||
|
style,
|
||||||
|
width = "100%",
|
||||||
|
height = 16,
|
||||||
|
radius = 8,
|
||||||
|
}) => {
|
||||||
|
if (!isLoading) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
borderRadius: radius,
|
||||||
|
backgroundColor: AccentColor.darkblue,
|
||||||
|
overflow: "hidden",
|
||||||
|
position: "relative",
|
||||||
|
},
|
||||||
|
style,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<MotiView
|
||||||
|
from={{ translateY: -100 }}
|
||||||
|
animate={{ translateY: 100 }}
|
||||||
|
transition={{
|
||||||
|
duration: 1200,
|
||||||
|
repeat: Infinity,
|
||||||
|
type: "timing",
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: 100,
|
||||||
|
backgroundColor: MainColor.soft_darkblue,
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomSkeleton;
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
View,
|
View,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
|
ScrollViewProps,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { NativeSafeAreaViewProps, SafeAreaView } from "react-native-safe-area-context";
|
import { NativeSafeAreaViewProps, SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ interface ViewWrapperProps {
|
|||||||
hideFooter?: boolean;
|
hideFooter?: boolean;
|
||||||
edgesFooter?: NativeSafeAreaViewProps["edges"];
|
edgesFooter?: NativeSafeAreaViewProps["edges"];
|
||||||
style?: StyleProp<ViewStyle>;
|
style?: StyleProp<ViewStyle>;
|
||||||
|
refreshControl?: ScrollViewProps["refreshControl"];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +42,7 @@ const ViewWrapper = ({
|
|||||||
hideFooter = false,
|
hideFooter = false,
|
||||||
edgesFooter =[],
|
edgesFooter =[],
|
||||||
style,
|
style,
|
||||||
|
refreshControl,
|
||||||
}: ViewWrapperProps) => {
|
}: ViewWrapperProps) => {
|
||||||
const assetBackground = require("../../assets/images/main-background.png");
|
const assetBackground = require("../../assets/images/main-background.png");
|
||||||
|
|
||||||
@@ -57,6 +60,7 @@ const ViewWrapper = ({
|
|||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={{ flexGrow: 1 }}
|
contentContainerStyle={{ flexGrow: 1 }}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
|
refreshControl={refreshControl}
|
||||||
>
|
>
|
||||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"expo-haptics": "~15.0.7",
|
"expo-haptics": "~15.0.7",
|
||||||
"expo-image": "~3.0.8",
|
"expo-image": "~3.0.8",
|
||||||
"expo-image-picker": "~17.0.8",
|
"expo-image-picker": "~17.0.8",
|
||||||
|
"expo-linear-gradient": "~15.0.7",
|
||||||
"expo-linking": "~8.0.8",
|
"expo-linking": "~8.0.8",
|
||||||
"expo-location": "~19.0.7",
|
"expo-location": "~19.0.7",
|
||||||
"expo-notifications": "^0.32.13",
|
"expo-notifications": "^0.32.13",
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"expo-system-ui": "~6.0.7",
|
"expo-system-ui": "~6.0.7",
|
||||||
"expo-web-browser": "~15.0.9",
|
"expo-web-browser": "~15.0.9",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"moti": "^0.30.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.4",
|
"react-native": "0.81.4",
|
||||||
|
|||||||
108
screens/Forum/ViewBeranda.tsx
Normal file
108
screens/Forum/ViewBeranda.tsx
Normal file
@@ -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<any>();
|
||||||
|
const [listData, setListData] = useState<any[]>();
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
title: "Forum",
|
||||||
|
headerLeft: () => <BackButton />,
|
||||||
|
headerRight: () => (
|
||||||
|
<AvatarComp
|
||||||
|
fileId={dataUser?.Profile?.imageId}
|
||||||
|
size="base"
|
||||||
|
href={`/forum/${user?.id}/forumku`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ViewWrapper
|
||||||
|
headerComponent={
|
||||||
|
<SearchInput
|
||||||
|
placeholder="Cari topik diskusi"
|
||||||
|
onChangeText={(e) => setSearch(e)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
floatingButton={
|
||||||
|
<FloatingButton
|
||||||
|
onPress={() =>
|
||||||
|
router.navigate("/(application)/(user)/forum/create")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl refreshing={loadingGetList} onRefresh={onLoadData} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{loadingGetList ? (
|
||||||
|
<LoaderCustom />
|
||||||
|
) : _.isEmpty(listData) ? (
|
||||||
|
<TextCustom align="center" color="gray">
|
||||||
|
Tidak ada diskusi
|
||||||
|
</TextCustom>
|
||||||
|
) : (
|
||||||
|
listData?.map((e: any, i: number) => (
|
||||||
|
<Forum_BoxDetailSection
|
||||||
|
key={i}
|
||||||
|
data={e}
|
||||||
|
onSetData={() => {}}
|
||||||
|
isTruncate={true}
|
||||||
|
href={`/forum/${e.id}`}
|
||||||
|
isRightComponent={false}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</ViewWrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
215
screens/Forum/ViewBeranda2.tsx
Normal file
215
screens/Forum/ViewBeranda2.tsx
Normal file
@@ -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<any>(null);
|
||||||
|
const [listData, setListData] = useState<any[]>([]);
|
||||||
|
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 }) => (
|
||||||
|
<Forum_BoxDetailSection
|
||||||
|
key={item.id}
|
||||||
|
data={item}
|
||||||
|
onSetData={() => {}}
|
||||||
|
isTruncate={true}
|
||||||
|
href={`/forum/${item.id}`}
|
||||||
|
isRightComponent={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔹 Komponen Header List (di dalam FlatList)
|
||||||
|
const ListHeaderComponent = (
|
||||||
|
<View style={{ paddingVertical: 8, alignItems: "center" }}>
|
||||||
|
<TextCustom>Diskusi Terbaru</TextCustom>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔹 Komponen Footer List (loading indicator)
|
||||||
|
const ListFooterComponent =
|
||||||
|
loading && !refreshing && listData.length > 0 ? (
|
||||||
|
<View style={{ paddingVertical: 16, alignItems: "center" }}>
|
||||||
|
{/* <Text style={{ color: "#aaa", fontSize: 14 }}>Memuat diskusi...</Text> */}
|
||||||
|
<LoaderCustom />
|
||||||
|
</View>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
// Skeleton List (untuk initial load)
|
||||||
|
const SkeletonListComponent = () => (
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<StackCustom>
|
||||||
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<SkeletonCustom height={200} key={i} />
|
||||||
|
))}
|
||||||
|
</StackCustom>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Komponen Empty
|
||||||
|
const EmptyComponent = () => (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextCustom align="center" color="gray">
|
||||||
|
{search ? "Tidak ada hasil pencarian" : "Tidak ada diskusi"}
|
||||||
|
</TextCustom>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* 🔹 Header Navigation */}
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
title: "Forum",
|
||||||
|
headerLeft: () => <BackButton />,
|
||||||
|
headerRight: () => (
|
||||||
|
<AvatarComp
|
||||||
|
fileId={dataUser?.Profile?.imageId}
|
||||||
|
size="base"
|
||||||
|
href={`/forum/${user?.id}/forumku`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 🔹 NewWrapper dalam mode list */}
|
||||||
|
<NewWrapper
|
||||||
|
// Header global (di atas FlatList, sticky)
|
||||||
|
headerComponent={
|
||||||
|
<View style={{ paddingHorizontal: 16, paddingTop: 8 }}>
|
||||||
|
<SearchInput
|
||||||
|
placeholder="Cari topik diskusi"
|
||||||
|
onChangeText={_.debounce((text) => setSearch(text), 500)}
|
||||||
|
// value={search}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
// Floating action button
|
||||||
|
floatingButton={
|
||||||
|
<FloatingButton
|
||||||
|
onPress={() =>
|
||||||
|
router.navigate("/(application)/(user)/forum/create")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
// --- Mode List Props ---
|
||||||
|
listData={listData}
|
||||||
|
renderItem={renderForumItem}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
// IOS
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
// Android
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
progressBackgroundColor={MainColor.yellow}
|
||||||
|
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onEndReached={loadMore}
|
||||||
|
// ListHeaderComponent={ListHeaderComponent}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
ListEmptyComponent={
|
||||||
|
_.isEmpty(listData) ? <SkeletonListComponent /> : <EmptyComponent />
|
||||||
|
}
|
||||||
|
// ------------------------
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
144
screens/Forum/ViewForumku.tsx
Normal file
144
screens/Forum/ViewForumku.tsx
Normal file
@@ -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<any | null>(null);
|
||||||
|
const [dataUser, setDataUser] = useState<any | null>(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 (
|
||||||
|
<>
|
||||||
|
<ViewWrapper
|
||||||
|
floatingButton={
|
||||||
|
user?.id === id && (
|
||||||
|
<FloatingButton
|
||||||
|
onPress={() =>
|
||||||
|
router.navigate("/(application)/(user)/forum/create")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackCustom>
|
||||||
|
<CenterCustom>
|
||||||
|
<AvatarComp
|
||||||
|
fileId={dataUser?.Profile?.imageId}
|
||||||
|
href={`/(application)/(image)/preview-image/${dataUser?.Profile?.imageId}`}
|
||||||
|
size="xl"
|
||||||
|
/>
|
||||||
|
</CenterCustom>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextCustom bold truncate>
|
||||||
|
@{dataUser?.username || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
<TextCustom>{listData?.length || "0"} postingan</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6} style={{ alignItems: "flex-end" }}>
|
||||||
|
<ButtonCustom href={`/profile/${dataUser?.Profile?.id}`}>
|
||||||
|
Kunjungi Profile
|
||||||
|
</ButtonCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
{loadingGetList ? (
|
||||||
|
<LoaderCustom />
|
||||||
|
) : _.isEmpty(listData) ? (
|
||||||
|
<TextCustom> Tidak ada diskusi</TextCustom>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{listData?.map((item: any, index: number) => (
|
||||||
|
<Forum_BoxDetailSection
|
||||||
|
isRightComponent={false}
|
||||||
|
key={index}
|
||||||
|
data={item}
|
||||||
|
isTruncate={true}
|
||||||
|
href={`/forum/${item.id}`}
|
||||||
|
onSetData={(value) => {
|
||||||
|
setOpenDrawer(value.setOpenDrawer);
|
||||||
|
setStatus(value.setStatus);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</StackCustom>
|
||||||
|
</ViewWrapper>
|
||||||
|
|
||||||
|
{/* Drawer Komponen Eksternal */}
|
||||||
|
<DrawerCustom
|
||||||
|
height={"auto"}
|
||||||
|
isVisible={openDrawer}
|
||||||
|
closeDrawer={() => setOpenDrawer(false)}
|
||||||
|
>
|
||||||
|
<Forum_MenuDrawerBerandaSection
|
||||||
|
id={id as string}
|
||||||
|
status={status}
|
||||||
|
setIsDrawerOpen={() => {
|
||||||
|
setOpenDrawer(false);
|
||||||
|
}}
|
||||||
|
authorId={id as string}
|
||||||
|
authorUsername={dataUser?.username}
|
||||||
|
/>
|
||||||
|
</DrawerCustom>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
215
screens/Forum/ViewForumku2.tsx
Normal file
215
screens/Forum/ViewForumku2.tsx
Normal file
@@ -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<any[]>([]);
|
||||||
|
const [dataUser, setDataUser] = useState<any | null>(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 = () => (
|
||||||
|
<>
|
||||||
|
<CenterCustom>
|
||||||
|
<AvatarComp
|
||||||
|
fileId={dataUser?.Profile?.imageId}
|
||||||
|
href={`/(application)/(image)/preview-image/${dataUser?.Profile?.imageId}`}
|
||||||
|
size="xl"
|
||||||
|
/>
|
||||||
|
</CenterCustom>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col style={{ paddingLeft: 8 }} span={6}>
|
||||||
|
<TextCustom bold truncate>
|
||||||
|
@{dataUser?.username || "-"}
|
||||||
|
</TextCustom>
|
||||||
|
<TextCustom>{count || "0"} postingan</TextCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6} style={{ alignItems: "flex-end", paddingRight: 8 }}>
|
||||||
|
<ButtonCustom href={`/profile/${dataUser?.Profile?.id}`}>
|
||||||
|
Kunjungi Profile
|
||||||
|
</ButtonCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
<Spacing />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderList = ({ item }: { item: any }) => (
|
||||||
|
<Forum_BoxDetailSection
|
||||||
|
key={item.id}
|
||||||
|
data={item}
|
||||||
|
onSetData={() => {}}
|
||||||
|
isTruncate={true}
|
||||||
|
href={`/forum/${item.id}`}
|
||||||
|
isRightComponent={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Skeleton List (untuk initial load)
|
||||||
|
const SkeletonListComponent = () => (
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<StackCustom>
|
||||||
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<CustomSkeleton height={200} key={i} />
|
||||||
|
))}
|
||||||
|
</StackCustom>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Komponen Empty
|
||||||
|
const EmptyComponent = () => (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NoDataText />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔹 Komponen Footer List (loading indicator)
|
||||||
|
const ListFooterComponent =
|
||||||
|
loading && !refreshing && listData.length > 0 ? (
|
||||||
|
<View style={{ paddingVertical: 16, alignItems: "center" }}>
|
||||||
|
{/* <Text style={{ color: "#aaa", fontSize: 14 }}>Memuat diskusi...</Text> */}
|
||||||
|
<LoaderCustom />
|
||||||
|
</View>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NewWrapper
|
||||||
|
floatingButton={
|
||||||
|
user?.id === id && (
|
||||||
|
<FloatingButton
|
||||||
|
onPress={() =>
|
||||||
|
router.navigate("/(application)/(user)/forum/create")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
listData={listData}
|
||||||
|
renderItem={renderList}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
// IOS
|
||||||
|
tintColor={MainColor.yellow}
|
||||||
|
// Android
|
||||||
|
colors={[MainColor.yellow]}
|
||||||
|
progressBackgroundColor={MainColor.yellow}
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onEndReached={loadMore}
|
||||||
|
ListHeaderComponent={randerHeaderComponent()}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
ListEmptyComponent={
|
||||||
|
_.isEmpty(listData) ? <SkeletonListComponent /> : <EmptyComponent />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -59,11 +59,30 @@ export const stylesHome = StyleSheet.create({
|
|||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: AccentColor.blue,
|
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: {
|
gridLabel: {
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
color: "white",
|
color: "white",
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
},
|
},
|
||||||
|
gridLabelInactive: {
|
||||||
|
marginTop: 8,
|
||||||
|
color: "gray",
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
jobVacancyContainer: {
|
jobVacancyContainer: {
|
||||||
backgroundColor: MainColor.darkblue,
|
backgroundColor: MainColor.darkblue,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
|
|||||||
@@ -9,21 +9,25 @@ export default function Home_FeatureSection() {
|
|||||||
name: "Event",
|
name: "Event",
|
||||||
icon: <Ionicons name="analytics" size={48} color="white" />,
|
icon: <Ionicons name="analytics" size={48} color="white" />,
|
||||||
onPress: () => router.push("/(application)/(user)/event/(tabs)"),
|
onPress: () => router.push("/(application)/(user)/event/(tabs)"),
|
||||||
|
status: "active",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Collaboration",
|
name: "Collaboration",
|
||||||
icon: <Ionicons name="share" size={48} color="white" />,
|
icon: <Ionicons name="share" size={48} color="gray" />,
|
||||||
onPress: () => router.push("/(application)/(user)/collaboration/(tabs)"),
|
onPress: () => router.push("/(application)/(user)/collaboration/(tabs)"),
|
||||||
|
status: "inactive",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Voting",
|
name: "Voting",
|
||||||
icon: <Ionicons name="cube" size={48} color="white" />,
|
icon: <Ionicons name="cube" size={48} color="white" />,
|
||||||
onPress: () => router.push("/(application)/(user)/voting/(tabs)"),
|
onPress: () => router.push("/(application)/(user)/voting/(tabs)"),
|
||||||
|
status: "active",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Crowdfunding",
|
name: "Crowdfunding",
|
||||||
icon: <Ionicons name="heart" size={48} color="white" />,
|
icon: <Ionicons name="heart" size={48} color="white" />,
|
||||||
onPress: () => router.push("/(application)/(user)/crowdfunding"),
|
onPress: () => router.push("/(application)/(user)/crowdfunding"),
|
||||||
|
status: "active",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -33,11 +37,12 @@ export default function Home_FeatureSection() {
|
|||||||
{listFeature.map((item, index) => (
|
{listFeature.map((item, index) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={index}
|
key={index}
|
||||||
style={stylesHome.gridItem}
|
style={item.status === "inactive" ? stylesHome.gridItemInactive : stylesHome.gridItem}
|
||||||
onPress={item.onPress}
|
onPress={item.onPress}
|
||||||
|
disabled={item.status === "inactive"}
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
<Text style={stylesHome.gridLabel}>{item.name}</Text>
|
<Text style={item.status === "inactive" ? stylesHome.gridLabelInactive : stylesHome.gridLabel}>{item.name}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -14,13 +14,22 @@ export async function apiForumCreate({ data }: { data: any }) {
|
|||||||
export async function apiForumGetAll({
|
export async function apiForumGetAll({
|
||||||
search,
|
search,
|
||||||
authorId,
|
authorId,
|
||||||
|
userLoginId,
|
||||||
|
category,
|
||||||
|
page,
|
||||||
}: {
|
}: {
|
||||||
search: string;
|
search?: string;
|
||||||
authorId?: string;
|
authorId?: string;
|
||||||
|
userLoginId?: string;
|
||||||
|
category: "beranda" | "forumku";
|
||||||
|
page?: string;
|
||||||
}) {
|
}) {
|
||||||
const authorQuery = authorId ? `?authorId=${authorId}` : "";
|
const categoryQuery = `?category=${category}`;
|
||||||
const searchQuery = search ? `?search=${search}` : "";
|
const authorQuery = authorId ? `&authorId=${authorId}` : "";
|
||||||
const query = search ? searchQuery : authorQuery;
|
const userLoginQuery = userLoginId ? `&userLoginId=${userLoginId}` : "";
|
||||||
|
const searchQuery = search ? `&search=${search}` : "";
|
||||||
|
const pageQuery = page ? `&page=${page}` : "";
|
||||||
|
const query = `${categoryQuery}${authorQuery}${userLoginQuery}${searchQuery}${pageQuery}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiConfig.get(`/mobile/forum${query}`);
|
const response = await apiConfig.get(`/mobile/forum${query}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user