User search:
Fix:
-  api get all user
- searching by username

Portofolio:
Fix:
- dot button hanya muncul jika user yang memiliki portofolio tersebut yang melihat

Profile:
Fix:
- dot button muncul hanya untuk user yang memiliki akunnya

### No Issue
This commit is contained in:
2025-09-10 16:33:39 +08:00
parent fb822d20b6
commit fc181bda7c
7 changed files with 174 additions and 107 deletions

View File

@@ -1,4 +1,5 @@
import { DrawerCustom, Spacing, StackCustom } from "@/components"; /* eslint-disable react-hooks/exhaustive-deps */
import { DrawerCustom, LoaderCustom, Spacing, StackCustom } from "@/components";
import LeftButtonCustom from "@/components/Button/BackButton"; import LeftButtonCustom from "@/components/Button/BackButton";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
@@ -10,18 +11,21 @@ import { drawerItemsPortofolio } from "@/screens/Portofolio/ListPage";
import Portofolio_MenuDrawerSection from "@/screens/Portofolio/MenuDrawer"; import Portofolio_MenuDrawerSection from "@/screens/Portofolio/MenuDrawer";
import Portofolio_SocialMediaSection from "@/screens/Portofolio/SocialMediaSection"; import Portofolio_SocialMediaSection from "@/screens/Portofolio/SocialMediaSection";
import { apiGetOnePortofolio } from "@/service/api-client/api-portofolio"; import { apiGetOnePortofolio } from "@/service/api-client/api-portofolio";
import { apiUser } from "@/service/api-client/api-user";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router"; import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { TouchableOpacity } from "react-native"; import { TouchableOpacity } from "react-native";
export default function Portofolio() { export default function Portofolio() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const { user } = useAuth();
const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [isLoadingDelete, setIsLoadingDelete] = useState(false); const [isLoadingDelete, setIsLoadingDelete] = useState(false);
const [data, setData] = useState<any>(); const [data, setData] = useState<any>();
const [profileId, setProfileId] = useState<any>();
const { user } = useAuth();
const openDrawer = () => { const openDrawer = () => {
setIsDrawerOpen(true); setIsDrawerOpen(true);
@@ -33,24 +37,27 @@ export default function Portofolio() {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
onLoadData(id as string); onLoadData(id as string);
onLoadUserByToken();
}, [id]) }, [id])
); );
async function onLoadData(id: string) { async function onLoadData(id: string) {
const response = await apiGetOnePortofolio({ id: id }); const response = await apiGetOnePortofolio({ id: id });
// console.log( console.log(
// "Response portofolio >>", "[PROFILE ID]>>",
// JSON.stringify(response.data, null, 2) JSON.stringify(response.data.Profile.id, null, 2)
// ); );
setData(response.data); setData(response.data);
} }
const userId = user?.id; const onLoadUserByToken = async () => {
const userLoginId = data?.Profile?.userId const response = await apiUser(user?.id as string);
console.log(
console.log("User ID >>", userId); "[PROFILE LOGIN]>>",
console.log("User Login ID >>", userLoginId); JSON.stringify(response.data?.Profile.id, null, 2)
);
setProfileId(response?.data?.Profile?.id);
};
return ( return (
<> <>
@@ -59,32 +66,41 @@ export default function Portofolio() {
options={{ options={{
title: "Portofolio", title: "Portofolio",
headerLeft: () => <LeftButtonCustom />, headerLeft: () => <LeftButtonCustom />,
headerRight: () => ( headerRight: () =>
<TouchableOpacity onPress={openDrawer}> data?.Profile?.id !== profileId ? null : (
<Ionicons <TouchableOpacity onPress={openDrawer}>
name="ellipsis-vertical" <Ionicons
size={20} name="ellipsis-vertical"
color={MainColor.yellow} size={20}
/> color={MainColor.yellow}
</TouchableOpacity> />
), </TouchableOpacity>
),
headerStyle: GStyles.headerStyle, headerStyle: GStyles.headerStyle,
headerTitleStyle: GStyles.headerTitleStyle, headerTitleStyle: GStyles.headerTitleStyle,
}} }}
/> />
<ViewWrapper> <ViewWrapper>
{/* <PorfofolioSection setShowDeleteAlert={setDeleteAlert} data={data} /> */} {!data || !profileId ? (
<StackCustom> <LoaderCustom />
<Portofolio_Data data={data} listSubBidang={data?.Portofolio_BidangDanSubBidangBisnis as any []} /> ) : (
<Portofolio_BusinessLocation /> <StackCustom>
<Portofolio_SocialMediaSection data={data?.Portofolio_MediaSosial} /> <Portofolio_Data
<Portofolio_ButtonDelete data={data}
id={id as string} listSubBidang={data?.Portofolio_BidangDanSubBidangBisnis as any[]}
isLoadingDelete={isLoadingDelete} />
setIsLoadingDelete={setIsLoadingDelete} <Portofolio_BusinessLocation />
/> <Portofolio_SocialMediaSection
<Spacing /> data={data?.Portofolio_MediaSosial}
</StackCustom> />
<Portofolio_ButtonDelete
id={id as string}
isLoadingDelete={isLoadingDelete}
setIsLoadingDelete={setIsLoadingDelete}
/>
<Spacing />
</StackCustom>
)}
</ViewWrapper> </ViewWrapper>
{/* Drawer Komponen Eksternal */} {/* Drawer Komponen Eksternal */}

View File

@@ -1,3 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { LoaderCustom } from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import LeftButtonCustom from "@/components/Button/BackButton"; import LeftButtonCustom from "@/components/Button/BackButton";
import DrawerCustom from "@/components/Drawer/DrawerCustom"; import DrawerCustom from "@/components/Drawer/DrawerCustom";
@@ -9,6 +11,7 @@ import Profile_PortofolioSection from "@/screens/Profile/PortofolioSection";
import ProfileSection from "@/screens/Profile/ProfileSection"; import ProfileSection from "@/screens/Profile/ProfileSection";
import { apiGetPortofolio } from "@/service/api-client/api-portofolio"; import { apiGetPortofolio } from "@/service/api-client/api-portofolio";
import { apiProfile } from "@/service/api-client/api-profile"; import { apiProfile } from "@/service/api-client/api-profile";
import { apiUser } from "@/service/api-client/api-user";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
import { IProfile } from "@/types/Type-Profile"; import { IProfile } from "@/types/Type-Profile";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
@@ -20,9 +23,10 @@ export default function Profile() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [data, setData] = useState<IProfile>(); const [data, setData] = useState<IProfile>();
const [dataPortofolio, setDataPortofolio] = useState<any[]>(); const [dataToken, setDataToken] = useState<IProfile>();
const [listPortofolio, setListPortofolio] = useState<any[]>();
const { logout, isAdmin } = useAuth(); const { logout, isAdmin, user } = useAuth();
const openDrawer = () => { const openDrawer = () => {
setIsDrawerOpen(true); setIsDrawerOpen(true);
@@ -36,14 +40,28 @@ export default function Profile() {
useCallback(() => { useCallback(() => {
onLoadData(id as string); onLoadData(id as string);
onLoadPortofolio(id as string); onLoadPortofolio(id as string);
onLoadUserByToken();
isUserCheck();
}, [id]) }, [id])
); );
const isUserCheck = () => {
const userId = id;
const userLoginId = dataToken?.id;
return userId === userLoginId;
};
const onLoadData = async (id: string) => { const onLoadData = async (id: string) => {
const response = await apiProfile({ id: id }); const response = await apiProfile({ id: id });
setData(response.data); setData(response.data);
}; };
const onLoadUserByToken = async () => {
const response = await apiUser(user?.id as string);
setDataToken(response?.data?.Profile);
};
const onLoadPortofolio = async (id: string) => { const onLoadPortofolio = async (id: string) => {
const response = await apiGetPortofolio({ id: id }); const response = await apiGetPortofolio({ id: id });
const lastTwoByDate = response.data const lastTwoByDate = response.data
@@ -52,7 +70,7 @@ export default function Profile() {
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
) // urut desc ) // urut desc
.slice(0, 2); .slice(0, 2);
setDataPortofolio(lastTwoByDate); setListPortofolio(lastTwoByDate);
}; };
return ( return (
@@ -61,27 +79,34 @@ export default function Profile() {
options={{ options={{
title: "Profile", title: "Profile",
headerLeft: () => <LeftButtonCustom />, headerLeft: () => <LeftButtonCustom />,
headerRight: () => ( headerRight: () =>
<TouchableOpacity onPress={openDrawer}> isUserCheck() && (
<Ionicons <TouchableOpacity onPress={openDrawer}>
name="ellipsis-vertical" <Ionicons
size={20} name="ellipsis-vertical"
color={MainColor.yellow} size={20}
/> color={MainColor.yellow}
</TouchableOpacity> />
), </TouchableOpacity>
),
headerStyle: GStyles.headerStyle, headerStyle: GStyles.headerStyle,
headerTitleStyle: GStyles.headerTitleStyle, headerTitleStyle: GStyles.headerTitleStyle,
}} }}
/> />
{/* Main View */} {/* Main View */}
<ViewWrapper> <ViewWrapper>
<ProfileSection data={data as any} /> {!data || !dataToken ? (
<LoaderCustom />
) : (
<>
<ProfileSection data={data as any} />
<Profile_PortofolioSection <Profile_PortofolioSection
data={dataPortofolio as any} data={listPortofolio as any}
profileId={id as string} profileId={id as string}
/> />
</>
)}
</ViewWrapper> </ViewWrapper>
{/* Drawer Komponen Eksternal */} {/* Drawer Komponen Eksternal */}

View File

@@ -1,5 +1,5 @@
import { import {
AvatarCustom, AvatarComp,
ClickableCustom, ClickableCustom,
Grid, Grid,
Spacing, Spacing,
@@ -10,39 +10,42 @@ import {
} from "@/components"; } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value"; import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { apiAllUser } from "@/service/api-client/api-user";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router"; import { router } from "expo-router";
import _ from "lodash";
import { useEffect, useState } from "react";
export default function UserSearch() { export default function UserSearch() {
function generateRandomPhoneNumber(index: number) { const [data, setData] = useState<any[]>([]);
let prefix; const [search, setSearch] = useState<string>("");
// Menentukan prefix berdasarkan index genap atau ganjil useEffect(() => {
if (index % 2 === 0) { onLoadData(search);
const evenPrefixes = ["6288", "6289", "6281"]; }, [search]);
prefix = evenPrefixes[Math.floor(Math.random() * evenPrefixes.length)];
} else { const onLoadData = async (search: string) => {
const oddPrefixes = ["6285", "6283"]; try {
prefix = oddPrefixes[Math.floor(Math.random() * oddPrefixes.length)]; const response = await apiAllUser({ search: search });
console.log("[DATA USER] >", JSON.stringify(response.data, null, 2));
setData(response.data);
} catch (error) {
console.log("Error fetching data", error);
} }
};
// Menghitung panjang sisa nomor acak (antara 10 - 12 digit) const handleSearch = (search: string) => {
const remainingLength = Math.floor(Math.random() * 3) + 10; // 10, 11, atau 12 setSearch(search);
onLoadData(search);
};
// Membuat sisa nomor acak
let randomNumber = "";
for (let i = 0; i < remainingLength; i++) {
randomNumber += Math.floor(Math.random() * 10); // Digit acak antara 0-9
}
// Menggabungkan prefix dan sisa nomor
return prefix + randomNumber;
}
return ( return (
<> <>
<ViewWrapper <ViewWrapper
headerComponent={ headerComponent={
<TextInputCustom <TextInputCustom
value={search}
onChangeText={handleSearch}
iconLeft={ iconLeft={
<Ionicons <Ionicons
name="search" name="search"
@@ -57,41 +60,46 @@ export default function UserSearch() {
} }
> >
<StackCustom> <StackCustom>
{Array.from({ length: 20 }).map((e, index) => { {!_.isEmpty(data) ? (
return ( data?.map((e, index) => {
<Grid key={index}> return (
<Grid.Col span={2}> <ClickableCustom
<AvatarCustom href={`/profile/${index}`}/> key={index}
</Grid.Col> onPress={() => {
<Grid.Col span={9}> console.log("Ke Profile");
<TextCustom size="large">Nama user {index}</TextCustom> router.push(`/profile/${e?.Profile?.id}`);
<TextCustom size="small">
+{generateRandomPhoneNumber(index)}
</TextCustom>
</Grid.Col>
<Grid.Col
span={1}
style={{
justifyContent: "center",
alignItems: "flex-end",
}} }}
> >
<ClickableCustom <Grid>
onPress={() => { <Grid.Col span={2}>
console.log("Ke Profile"); <AvatarComp fileId={e?.Profile?.imageId} size="base" />
router.push(`/profile/${index}`); </Grid.Col>
}} <Grid.Col span={9}>
> <StackCustom gap={"sm"}>
<Ionicons <TextCustom size="large">{e?.username}</TextCustom>
name="chevron-forward" <TextCustom size="small">+{e?.nomor}</TextCustom>
size={ICON_SIZE_SMALL} </StackCustom>
color={MainColor.white} </Grid.Col>
/> <Grid.Col
</ClickableCustom> span={1}
</Grid.Col> style={{
</Grid> justifyContent: "center",
); alignItems: "flex-end",
})} }}
>
<Ionicons
name="chevron-forward"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
</Grid.Col>
</Grid>
</ClickableCustom>
);
})
) : (
<TextCustom align="center">Tidak ditemukan</TextCustom>
)}
</StackCustom> </StackCustom>
<Spacing height={50} /> <Spacing height={50} />
</ViewWrapper> </ViewWrapper>

View File

@@ -0,0 +1,8 @@
import { MainColor } from "@/constants/color-palet";
import { ActivityIndicator } from "react-native";
export default function LoaderCustom({ size }: { size?: "small" | "large" }) {
return (
<ActivityIndicator size={size ? size : "small"} color={MainColor.yellow} />
);
}

View File

@@ -61,6 +61,8 @@ import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage";
import GridComponentView from "./_ShareComponent/GridSectionView"; import GridComponentView from "./_ShareComponent/GridSectionView";
// Progress // Progress
import ProgressCustom from "./Progress/ProgressCustom"; import ProgressCustom from "./Progress/ProgressCustom";
// Loader
import LoaderCustom from "./Loader/LoaderCustom";
export { export {
// ActionIcon // ActionIcon
@@ -128,4 +130,6 @@ export {
TextInputCustom, TextInputCustom,
// ViewWrapper // ViewWrapper
ViewWrapper, ViewWrapper,
// Loader
LoaderCustom,
}; };

View File

@@ -92,7 +92,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
await AsyncStorage.setItem("authToken", token); await AsyncStorage.setItem("authToken", token);
const responseUser = await apiConfig.get( const responseUser = await apiConfig.get(
`/mobile/user?token=${token}`, `/mobile?token=${token}`,
{ {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,

View File

@@ -1,6 +1,12 @@
import { apiConfig } from "../api-config"; import { apiConfig } from "../api-config";
export async function apiUser(id: string) { export async function apiUser(id: string) {
const response = await apiConfig.get(`/user/${id}`); const response = await apiConfig.get(`/mobile/user/${id}`);
return response.data; return response.data;
} }
export async function apiAllUser({ search }: { search: string }) {
const response = await apiConfig.get(`/mobile/user?search=${search}`);
return response.data;
}