Compare commits

...

8 Commits

Author SHA1 Message Date
a43ddaa9d6 Invesment
Fix:
- styles/global-styles.ts : tambah alignSelfFlexEnd
- my-holding: basic ui
- portofolio.: basic ui
- transaction: basic ui

## No Issue
2025-07-30 11:15:57 +08:00
927db87749 Component
Add:
- upload button : masih percobaan

Utils:
Add:
- file validasi untuk upload file

Pakage
Add:
- expo-document-picker
- expo-file-system

## No Issue
2025-07-30 10:39:54 +08:00
8a514d2670 Investasi
Add:
- lib/dummy-data/investment : list master
- app/(application)/(user)/investment/(tabs)

Main Layout:
Fix:
- app/(application)/(user)/investment/create.tsx

Component
Add:
- Progress

## No Issue
2025-07-29 17:22:11 +08:00
554428b7b4 Crowd
Add:
-  assets/images/constants/crowd-hipmi.png
-  app/(application)/(user)/crowdfunding/
-  app/(application)/(user)/investment/
-  app/(application)/(user)/donation/

Fix:
-  screens/Home/topFeatureSection.tsx
-  app/(application)/(user)/_layout.tsx

## No Issue
2025-07-29 11:41:41 +08:00
befbd1a47d Voting:
Add:
- ComponentDetailDataSection : Tampilan judul, deskripsi, batas waktu
- BoxDetailHistorySection
- (user)/voting/[id]/history

Fix:
Perbaikan component detail data pada:
- screens/Voting/BoxDetailSection.tsx
-  screens/Voting/BoxDetailPublishSection.tsx
- screens/Voting/BoxDetailContribution.tsx

# No Issue
2025-07-29 10:50:24 +08:00
c9a1ac1db5 Voting
Add:
- BoxDetailContribution
- app/(application)/(user)/voting/[id]/contribution.tsx

Fix:
- app/(application)/(user)/voting/(tabs)/contribution.tsx
- app/(application)/(user)/voting/[id]/[status]/detail.tsx
- app/(application)/(user)/voting/[id]/list-of-contributor.tsx
- app/(application)/(user)/voting/[id]/index.tsx

# No Issue
2025-07-28 17:29:52 +08:00
4bcfcb5f5a Icon:
Add:
- IconArchive

Voting
Add:
- detail voting
- BoxDetailHasilVotingSection
- BoxDetailPublishSection
- BoxDetailSection
- ButtonStatusSection

Fix:
- BoxPublishSection
- ReportListSection: Hapus import useless

# No Issue
2025-07-28 16:43:54 +08:00
f21ff744d3 Component
Add:
- TabsTwoHeaderCustom

Voting
Fix:
- history

# Np Issue
2025-07-28 14:51:41 +08:00
47 changed files with 1982 additions and 95 deletions

View File

@@ -147,7 +147,6 @@ export default function UserLayout() {
}}
/>
{/* ========== End Collaboration Section ========= */}
{/* ========== Voting Section ========= */}
@@ -165,17 +164,63 @@ export default function UserLayout() {
headerLeft: () => <BackButton path="/home" />,
}}
/>
{/* <Stack.Screen
<Stack.Screen
name="voting/[id]/edit"
options={{
title: "Edit Voting",
headerLeft: () => <BackButton />,
}}
/> */}
/>
<Stack.Screen
name="voting/[id]/list-of-contributor"
options={{
title: "Daftar Kontributor",
headerLeft: () => <BackButton />,
}}
/>
{/* ========== End Voting Section ========= */}
{/* ========== Crowdfunding Section ========= */}
<Stack.Screen
name="crowdfunding/index"
options={{
title: "Crowdfunding",
headerLeft: () => <BackButton />,
}}
/>
{/* ========== End Crowdfunding Section ========= */}
{/* ========== Investment Section ========= */}
<Stack.Screen
name="investment/(tabs)"
options={{
title: "Investasi",
headerLeft: () => <BackButton path="/crowdfunding" />,
}}
/>
<Stack.Screen
name="investment/create"
options={{
title: "Tambah Investasi",
headerLeft: () => <BackButton />,
}}
/>
{/* ========== End Investment Section ========= */}
{/* ========== Donation Section ========= */}
<Stack.Screen
name="donation/create"
options={{
title: "Tambah Donasi",
headerLeft: () => <BackButton />,
}}
/>
{/* ========== End Donation Section ========= */}
{/* ========== Job Section ========= */}
<Stack.Screen
name="job/create"

View File

@@ -0,0 +1,68 @@
import {
BaseBox,
Grid,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { Feather } from "@expo/vector-icons";
import { Image } from "expo-image";
export default function Crowdfunding() {
const listPage = [
{
title: "Investasi",
desc: "Buat investasi dan jual beli saham lebih mudah dengan pengguna lain.",
path: "investment/(tabs)",
},
{
title: "Donasi",
desc: "Berbagi info untuk berdonasi lebih luas dan lebih efisien.",
path: "donation/create",
},
];
return (
<ViewWrapper>
<StackCustom>
<Image
source={require("@/assets/images/constants/crowd-hipmi.png")}
contentFit="cover"
transition={1000}
style={{
width: "100%",
height: 200,
borderRadius: 10,
}}
/>
{listPage.map((item, index) => (
<BaseBox key={index} paddingTop={10} paddingBottom={10} href={item.path as any} marginBottom={0}>
<Grid>
<Grid.Col span={10}>
<StackCustom gap={"xs"}>
<TextCustom bold size="large">
{item.title}
</TextCustom>
<TextCustom>{item.desc}</TextCustom>
</StackCustom>
</Grid.Col>
<Grid.Col
span={2}
style={{ alignItems: "flex-end", justifyContent: "center" }}
>
<Feather
name="chevron-right"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
</Grid.Col>
</Grid>
</BaseBox>
))}
</StackCustom>
</ViewWrapper>
);
}

View File

@@ -0,0 +1,11 @@
import { TextCustom, ViewWrapper } from "@/components";
export default function DonationCreate() {
return (
<ViewWrapper>
<TextCustom bold size="large">
Coming Soon !
</TextCustom>
</ViewWrapper>
);
}

View File

@@ -0,0 +1,59 @@
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { TabsStyles } from "@/styles/tabs-styles";
import { Feather, FontAwesome6, Ionicons } from "@expo/vector-icons";
import { Tabs } from "expo-router";
export default function InvestmentTabsLayout() {
return (
<Tabs screenOptions={TabsStyles}>
<Tabs.Screen
name="index"
options={{
title: "Bursa",
tabBarIcon: ({ color }) => (
<Ionicons
name="bar-chart-outline"
color={color}
size={ICON_SIZE_SMALL}
/>
),
}}
/>
<Tabs.Screen
name="portofolio"
options={{
title: "Portofolio",
tabBarIcon: ({ color }) => (
<Feather name="pie-chart" color={color} size={ICON_SIZE_SMALL} />
),
}}
/>
<Tabs.Screen
name="my-holding"
options={{
title: "Saham Saya",
tabBarIcon: ({ color }) => (
<FontAwesome6
name="hand-holding-dollar"
color={color}
size={ICON_SIZE_SMALL}
/>
),
}}
/>
<Tabs.Screen
name="transaction"
options={{
title: "Transaksi",
tabBarIcon: ({ color }) => (
<FontAwesome6
name="money-bill-transfer"
color={color}
size={ICON_SIZE_SMALL}
/>
),
}}
/>
</Tabs>
);
}

View File

@@ -0,0 +1,81 @@
import {
BaseBox,
FloatingButton,
Grid,
ProgressCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import dayjs from "dayjs";
import { Image } from "expo-image";
import { router } from "expo-router";
import { View } from "react-native";
export default function InvestmentBursa() {
return (
<ViewWrapper
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/investment/create")} />
}
>
{Array.from({ length: 10 }).map((_, index) => (
<BaseBox key={index} paddingTop={7} paddingBottom={7}>
<Grid>
<Grid.Col span={5}>
<Image
source={DUMMY_IMAGE.background}
style={{ width: "auto", height: 100, borderRadius: 10 }}
/>
</Grid.Col>
<Grid.Col span={1}>
<View />
</Grid.Col>
<Grid.Col span={6}>
<StackCustom>
<TextCustom truncate={2}>
Title here : Lorem ipsum dolor sit amet consectetur
adipisicing elit. Omnis, exercitationem, sequi enim quod
distinctio maiores laudantium amet, quidem atque repellat sit
vitae qui aliquam est veritatis laborum eum voluptatum totam!
</TextCustom>
<ProgressCustom value={index % 5 * 20} size="lg" />
<TextCustom>
Sisa waktu: {dayjs().diff(dayjs(), "day")} hari
</TextCustom>
</StackCustom>
</Grid.Col>
</Grid>
</BaseBox>
))}
</ViewWrapper>
);
}
// <View style={{ padding: 20, gap: 16 }}>
// <TextCustom>Progress 70%</TextCustom>
// <ProgressCustom value={70} color="primary" size="md" />
// <TextCustom>Success Progress</TextCustom>
// <ProgressCustom value={40} color="success" size="lg" />
// <TextCustom>Warning Progress (small)</TextCustom>
// <ProgressCustom value={90} color="warning" size="sm" />
// <TextCustom>Error Indeterminate</TextCustom>
// <ProgressCustom value={null} color="error" size="md" />
// <TextCustom>Custom Radius</TextCustom>
// <ProgressCustom value={60} color="info" size="xl" radius={4} />
// <ProgressCustom value={70} color="primary" size="lg" />
// <ProgressCustom value={45} color="success" size="md" label="Halfway!" />
// <ProgressCustom value={90} color="warning" size="lg" showLabel={false} />
// <ProgressCustom value={null} color="error" size="sm" label="Loading..." />
// </View>;

View File

@@ -0,0 +1,49 @@
import {
BaseBox,
Grid,
ProgressCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { View } from "react-native";
export default function InvestmentMyHolding() {
return (
<ViewWrapper hideFooter>
{Array.from({ length: 10 }).map((_, index) => (
<BaseBox key={index} paddingTop={7} paddingBottom={7}>
<Grid>
<Grid.Col span={6}>
<StackCustom gap={"xs"}>
<TextCustom truncate={2}>
Title here : Lorem ipsum dolor sit amet consectetur
adipisicing elit. Omnis, exercitationem, sequi enim quod
distinctio maiores laudantium amet, quidem atque repellat sit
vitae qui aliquam est veritatis laborum eum voluptatum totam!
</TextCustom>
<Spacing height={5} />
<TextCustom size="small">Rp. 7.500.000</TextCustom>
<TextCustom size="small">300 Lembar</TextCustom>
</StackCustom>
</Grid.Col>
<Grid.Col span={1}>
<View />
</Grid.Col>
<Grid.Col
span={5}
style={{
justifyContent: "center",
alignItems: "center",
}}
>
<ProgressCustom value={(index % 5) * 20} size="lg" />
</Grid.Col>
</Grid>
</BaseBox>
))}
</ViewWrapper>
);
}

View File

@@ -0,0 +1,70 @@
import {
BaseBox,
Grid,
ScrollableCustom,
Spacing,
TextCustom,
ViewWrapper
} from "@/components";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { masterStatus } from "@/lib/dummy-data/_master/status";
import { Image } from "expo-image";
import { useState } from "react";
import { View } from "react-native";
export default function InvestmentPortofolio() {
const [activeCategory, setActiveCategory] = useState<string | null>(
"publish"
);
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
const scrollComponent = (
<ScrollableCustom
data={masterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
return (
<ViewWrapper headerComponent={scrollComponent} hideFooter>
{Array.from({ length: 10 }).map((_, index) => (
<BaseBox key={index} paddingTop={7} paddingBottom={7}>
<Grid>
<Grid.Col span={6}>
<TextCustom truncate={2}>
Title here : {activeCategory} Lorem ipsum dolor sit amet consectetur adipisicing
elit. Omnis, exercitationem, sequi enim quod distinctio maiores
laudantium amet, quidem atque repellat sit vitae qui aliquam est
veritatis laborum eum voluptatum totam!
</TextCustom>
<Spacing />
<TextCustom bold size="small">
Target Dana:
</TextCustom>
<TextCustom>Rp. {index + 1 % 3/4 * 1004000}</TextCustom>
</Grid.Col>
<Grid.Col span={1}>
<View />
</Grid.Col>
<Grid.Col span={5}>
<Image
source={DUMMY_IMAGE.background}
style={{ width: "auto", height: 100, borderRadius: 10 }}
/>
</Grid.Col>
</Grid>
</BaseBox>
))}
</ViewWrapper>
);
}

View File

@@ -0,0 +1,53 @@
import {
BadgeCustom,
BaseBox,
Grid,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { GStyles } from "@/styles/global-styles";
import dayjs from "dayjs";
import { View } from "react-native";
export default function InvestmentTransaction() {
return (
<ViewWrapper hideFooter>
{Array.from({ length: 10 }).map((_, i) => (
<BaseBox key={i} paddingTop={7} paddingBottom={7}>
<Grid>
<Grid.Col span={6}>
<StackCustom gap={"xs"}>
<TextCustom truncate>
Title Investment: Lorem ipsum dolor sit amet consectetur
adipisicing elit. Am culpa excepturi deleniti soluta animi
porro amet ducimus.
</TextCustom>
<TextCustom color="gray" size="small">
{dayjs().format("DD/MM/YYYY")}
</TextCustom>
</StackCustom>
</Grid.Col>
<Grid.Col span={1}>
<View />
</Grid.Col>
<Grid.Col span={5} style={{ alignItems: "flex-end" }}>
<StackCustom gap={"xs"}>
<TextCustom bold truncate>
Rp. 7.500.000
</TextCustom>
<BadgeCustom
variant="light"
color="success"
style={GStyles.alignSelfFlexEnd}
>
Berhasil
</BadgeCustom>
</StackCustom>
</Grid.Col>
</Grid>
</BaseBox>
))}
</ViewWrapper>
);
}

View File

@@ -0,0 +1,197 @@
import {
BaseBox,
ButtonCenteredOnly,
ButtonCustom,
CenterCustom,
InformationBox,
LandscapeFrameUploaded,
SelectCustom,
Spacing,
StackCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import dummyPembagianDeviden from "@/lib/dummy-data/investment/pembagian-deviden";
import dummyListPencarianInvestor from "@/lib/dummy-data/investment/pencarian-investor";
import dummyPeriodeDeviden from "@/lib/dummy-data/investment/periode-deviden";
import { FontAwesome5 } from "@expo/vector-icons";
import { router } from "expo-router";
import { useState } from "react";
export default function InvestmentCreate() {
const [data, setData] = useState({
title: "",
targetDana: 0,
hargaPerLembar: 0,
totalLembar: 0,
rasioKeuntungan: 0,
pencarianInvestor: "",
periodeDeviden: "",
pembagianDeviden: "",
});
// const [coba, setCoba] = useState("");
return (
<ViewWrapper>
<StackCustom gap={"xs"}>
{/* <View style={GStyles.inputContainerInput}>
<TextInput
style={{
...GStyles.inputText,
}}
onChangeText={(value) => setCoba(value)}
value={coba}
keyboardType="decimal-pad"
/>
</View> */}
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
<LandscapeFrameUploaded />
<ButtonCenteredOnly
icon="upload"
onPress={() => router.push("/take-picture/1")}
>
Upload
</ButtonCenteredOnly>
<Spacing />
<InformationBox text="File prospektus wajib untuk diupload, agar calon investor paham dengan prospek investasi yang akan anda jalankan kedepannya." />
<BaseBox>
<CenterCustom>
<FontAwesome5
name="file-pdf"
size={30}
color={MainColor.disabled}
/>
</CenterCustom>
</BaseBox>
<ButtonCenteredOnly
icon="upload"
onPress={() => router.push("/take-picture/1")}
>
Upload File
</ButtonCenteredOnly>
<Spacing />
<TextInputCustom
required
placeholder="Judul"
label="Judul"
value={data.title}
onChangeText={(value) => setData({ ...data, title: value })}
/>
<TextInputCustom
required
iconLeft="Rp."
placeholder="0"
label="Target Dana"
keyboardType="numeric"
onChangeText={(value) =>
setData({ ...data, targetDana: Number(value) })
}
value={data.targetDana === 0 ? "" : data.targetDana.toString()}
/>
<TextInputCustom
required
iconLeft="Rp."
placeholder="0"
label="Target Dana"
keyboardType="numeric"
onChangeText={(value) =>
setData({ ...data, targetDana: Number(value) })
}
value={data.targetDana === 0 ? "" : data.targetDana.toString()}
/>
<TextInputCustom
required
iconLeft="Rp."
placeholder="0"
label="Harga Per Lembar"
keyboardType="numeric"
onChangeText={(value) =>
setData({ ...data, targetDana: Number(value) })
}
value={data.targetDana === 0 ? "" : data.targetDana.toString()}
/>
<TextInputCustom
required
placeholder="0"
label="Total Lembar"
keyboardType="numeric"
onChangeText={(value) =>
setData({ ...data, totalLembar: Number(value) })
}
value={data.totalLembar === 0 ? "" : data.totalLembar.toString()}
/>
<TextInputCustom
required
iconRight="%"
label="Rasio Keuntungan / ROI %"
placeholder="0"
keyboardType="numeric"
onChangeText={(value) =>
setData({ ...data, rasioKeuntungan: Number(value) })
}
value={
data.rasioKeuntungan === 0 ? "" : data.rasioKeuntungan.toString()
}
/>
<SelectCustom
required
placeholder="Pilih batas waktu"
label="Pencarian Investor"
data={dummyListPencarianInvestor.map((item) => ({
label: item.name + `${" "}hari`,
value: item.id,
}))}
onChange={(value) =>
setData({ ...data, pencarianInvestor: value as any })
}
value={data.pencarianInvestor}
/>
<SelectCustom
required
placeholder="Pilih batas waktu"
label="Pilih Periode Deviden"
data={dummyPeriodeDeviden.map((item) => ({
label: item.name,
value: item.id,
}))}
onChange={(value) =>
setData({ ...data, periodeDeviden: value as any })
}
value={data.periodeDeviden}
/>
<SelectCustom
required
placeholder="Pilih batas waktu"
label="Pilih Pembagian Deviden"
data={dummyPembagianDeviden.map((item) => ({
label: item.name + `${" "}bulan`,
value: item.id,
}))}
onChange={(value) =>
setData({ ...data, pembagianDeviden: value as any })
}
value={data.pembagianDeviden}
/>
<Spacing />
<ButtonCustom onPress={() => router.replace("/investment/portofolio")}>
Simpan
</ButtonCustom>
</StackCustom>
<Spacing height={50} />
</ViewWrapper>
);
}

View File

@@ -1,25 +1,15 @@
import {
BadgeCustom,
StackCustom,
TextCustom,
ViewWrapper,
ViewWrapper
} from "@/components";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { GStyles } from "@/styles/global-styles";
export default function VotingContribution() {
const bottomComponent = (
<StackCustom>
<TextCustom align="center">Pilihan Anda:</TextCustom>
<BadgeCustom style={[GStyles.alignSelfCenter]}>Pilihan 1</BadgeCustom>
</StackCustom>
);
return (
<ViewWrapper hideFooter>
{Array.from({ length: 5 }).map((_, index) => (
<Voting_BoxPublishSection
key={index}
bottomComponent={bottomComponent}
href={`/voting/${index}/contribution`}
/>
))}
</ViewWrapper>

View File

@@ -1,9 +1,37 @@
import { TextCustom, ViewWrapper } from "@/components";
import { ViewWrapper } from "@/components";
import TabsTwoHeaderCustom from "@/components/_ShareComponent/TabsTwoHeaderCustom";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { useState } from "react";
export default function VotingHistory() {
const [activeCategory, setActiveCategory] = useState<string | null>("all");
const handlePress = (item: any) => {
setActiveCategory(item);
// tambahkan logika lain seperti filter dsb.
};
return (
<ViewWrapper>
<TextCustom>Voting History</TextCustom>
<ViewWrapper
hideFooter
headerComponent={
<TabsTwoHeaderCustom
leftValue="all"
rightValue="main"
leftText="Semua Riwayat"
rightText="Riwayat Saya"
activeCategory={activeCategory}
handlePress={handlePress}
/>
}
>
{Array.from({ length: 10 }).map((_, index) => (
<Voting_BoxPublishSection
key={index}
id={activeCategory as any}
href={`/voting/${index}/history`}
/>
))}
</ViewWrapper>
);
}

View File

@@ -16,7 +16,7 @@ export default function VotingBeranda() {
headerComponent={<SearchInput placeholder="Cari voting" />}
>
{Array.from({ length: 5 }).map((_, index) => (
<Voting_BoxPublishSection key={index} />
<Voting_BoxPublishSection key={index} href={`/voting/${index}`} />
))}
</ViewWrapper>
);

View File

@@ -4,7 +4,7 @@ import {
ScrollableCustom,
StackCustom,
TextCustom,
ViewWrapper
ViewWrapper,
} from "@/components";
import { masterStatus } from "@/lib/dummy-data/_master/status";
import dayjs from "dayjs";
@@ -39,8 +39,7 @@ export default function VotingStatus() {
key={i}
paddingTop={20}
paddingBottom={20}
// href={`/job/${e.id}/${activeCategory}/detail`}
// onPress={() => console.log("pressed")}
href={`/voting/${i}/${activeCategory}/detail`}
>
<StackCustom>
<TextCustom align="center" bold truncate size="large">

View File

@@ -0,0 +1,108 @@
import {
AlertDefaultSystem,
BackButton,
DotButton,
DrawerCustom,
MenuDrawerDynamicGrid,
Spacing,
ViewWrapper,
} from "@/components";
import { IconArchive, IconContribution, IconEdit } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import { Voting_BoxDetailSection } from "@/screens/Voting/BoxDetailSection";
import Voting_ButtonStatusSection from "@/screens/Voting/ButtonStatusSection";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useState } from "react";
export default function VotingDetailStatus() {
const { id, status } = useLocalSearchParams();
const [openDrawerDraft, setOpenDrawerDraft] = useState(false);
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const handlePressDraft = (item: IMenuDrawerItem) => {
console.log("PATH >> ", item.path);
router.navigate(item.path as any);
setOpenDrawerDraft(false);
};
const handlePressPublish = (item: IMenuDrawerItem) => {
if (item.path === "") {
AlertDefaultSystem({
title: "Update Arsip",
message: "Apakah Anda yakin ingin mengarsipkan voting ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
console.log("Hapus");
router.back();
},
});
}
router.navigate(item.path as any);
setOpenDrawerPublish(false);
};
return (
<>
<Stack.Screen
options={{
title: `Detail ${status}`,
headerLeft: () => <BackButton />,
headerRight: () =>
status === "draft" ? (
<DotButton onPress={() => setOpenDrawerDraft(true)} />
) : status === "publish" ? (
<DotButton onPress={() => setOpenDrawerPublish(true)} />
) : null,
}}
/>
<ViewWrapper>
<Voting_BoxDetailSection />
<Voting_ButtonStatusSection status={status as string} />
<Spacing />
</ViewWrapper>
{/* ========= Draft Drawer ========= */}
<DrawerCustom
isVisible={openDrawerDraft}
closeDrawer={() => setOpenDrawerDraft(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <IconEdit />,
label: "Edit",
path: `/voting/${id}/edit`,
},
]}
columns={4}
onPressItem={handlePressDraft as any}
/>
</DrawerCustom>
{/* ========= Publish Drawer ========= */}
<DrawerCustom
isVisible={openDrawerPublish}
closeDrawer={() => setOpenDrawerPublish(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <IconContribution />,
label: "Daftar Kontributor",
path: `/voting/${id}/list-of-contributor`,
},
{
icon: <IconArchive />,
label: "Update Arsip",
path: "" as any,
},
]}
onPressItem={handlePressPublish as any}
/>
</DrawerCustom>
</>
);
}

View File

@@ -0,0 +1,65 @@
import {
AvatarUsernameAndOtherComponent,
BackButton,
DotButton,
DrawerCustom,
MenuDrawerDynamicGrid,
Spacing,
ViewWrapper,
} from "@/components";
import { IconContribution } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import { Voting_BoxDetailContributionSection } from "@/screens/Voting/BoxDetailContribution";
import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useState } from "react";
export default function VotingDetailContribution() {
const { id } = useLocalSearchParams();
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const handlePressPublish = (item: IMenuDrawerItem) => {
router.navigate(item.path as any);
setOpenDrawerPublish(false);
};
return (
<>
<Stack.Screen
options={{
title: "Detail Kontribusi",
headerLeft: () => <BackButton />,
headerRight: () => (
<DotButton onPress={() => setOpenDrawerPublish(true)} />
),
}}
/>
<ViewWrapper>
<Voting_BoxDetailContributionSection
headerAvatar={<AvatarUsernameAndOtherComponent />}
/>
<Voting_BoxDetailHasilVotingSection />
<Spacing />
</ViewWrapper>
{/* ========= Publish Drawer ========= */}
<DrawerCustom
isVisible={openDrawerPublish}
closeDrawer={() => setOpenDrawerPublish(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <IconContribution />,
label: "Daftar Kontributor",
path: `/voting/${id}/list-of-contributor`,
},
]}
onPressItem={handlePressPublish as any}
/>
</DrawerCustom>
</>
);
}

View File

@@ -0,0 +1,78 @@
import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
Grid,
Spacing,
StackCustom,
TextAreaCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import { MainColor } from "@/constants/color-palet";
import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import { TouchableOpacity } from "react-native";
export default function VotingEdit() {
const buttonSubmit = () => {
return (
<>
<BoxButtonOnFooter>
<ButtonCustom
onPress={() =>
router.back()
}
>
Update
</ButtonCustom>
</BoxButtonOnFooter>
</>
);
};
return (
<ViewWrapper footerComponent={buttonSubmit()}>
<StackCustom gap={"xs"}>
<TextInputCustom
label="Judul Voting"
placeholder="MasukanJudul Voting"
required
/>
<TextAreaCustom
label="Deskripsi"
placeholder="Masukan Deskripsi"
required
showCount
maxLength={1000}
/>
<DateTimePickerCustom label="Mulai Voting" required />
<DateTimePickerCustom label="Voting Berakhir" required />
<Grid>
<Grid.Col span={10}>
<TextInputCustom
label="Pilihan"
placeholder="Masukan Pilihan"
required
/>
</Grid.Col>
<Grid.Col
span={2}
style={{ alignItems: "center", justifyContent: "center" }}
>
<TouchableOpacity onPress={() => console.log("delete")}>
<Ionicons name="trash" size={24} color={MainColor.red} />
</TouchableOpacity>
</Grid.Col>
</Grid>
<ButtonCenteredOnly onPress={() => console.log("add")}>
Tambah Pilihan
</ButtonCenteredOnly>
<Spacing />
</StackCustom>
</ViewWrapper>
);
}

View File

@@ -0,0 +1,64 @@
import {
AvatarUsernameAndOtherComponent,
BackButton,
DotButton,
DrawerCustom,
MenuDrawerDynamicGrid,
Spacing,
ViewWrapper,
} from "@/components";
import { IconContribution } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection";
import { Voting_BoxDetailHistorySection } from "@/screens/Voting/BoxDetailHistorySection";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useState } from "react";
export default function VotingDetailHistory() {
const { id } = useLocalSearchParams();
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const handlePressPublish = (item: IMenuDrawerItem) => {
router.navigate(item.path as any);
setOpenDrawerPublish(false);
};
return (
<>
<Stack.Screen
options={{
title: "Riwayat Voting",
headerLeft: () => <BackButton />,
headerRight: () => (
<DotButton onPress={() => setOpenDrawerPublish(true)} />
),
}}
/>
<ViewWrapper>
<Voting_BoxDetailHistorySection
headerAvatar={<AvatarUsernameAndOtherComponent />}
/>
<Voting_BoxDetailHasilVotingSection />
<Spacing />
</ViewWrapper>
{/* ========= Publish Drawer ========= */}
<DrawerCustom
isVisible={openDrawerPublish}
closeDrawer={() => setOpenDrawerPublish(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <IconContribution />,
label: "Daftar Kontributor",
path: `/voting/${id}/list-of-contributor`,
},
]}
onPressItem={handlePressPublish as any}
/>
</DrawerCustom>
</>
);
}

View File

@@ -0,0 +1,87 @@
import {
AlertDefaultSystem,
AvatarUsernameAndOtherComponent,
BackButton,
DotButton,
DrawerCustom,
InformationBox,
MenuDrawerDynamicGrid,
StackCustom,
ViewWrapper,
} from "@/components";
import { IconArchive, IconContribution } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection";
import { Voting_BoxDetailPublishSection } from "@/screens/Voting/BoxDetailPublishSection";
import { router, Stack, useLocalSearchParams } from "expo-router";
import React, { useState } from "react";
export default function VotingDetail() {
const { id } = useLocalSearchParams();
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const handlePressPublish = (item: IMenuDrawerItem) => {
if (item.path === "") {
AlertDefaultSystem({
title: "Update Arsip",
message: "Apakah Anda yakin ingin mengarsipkan voting ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
console.log("Hapus");
router.back();
},
});
}
router.navigate(item.path as any);
setOpenDrawerPublish(false);
};
return (
<>
<Stack.Screen
options={{
title: `Detail Voting`,
headerLeft: () => <BackButton />,
headerRight: () => (
<DotButton onPress={() => setOpenDrawerPublish(true)} />
),
}}
/>
<ViewWrapper>
<StackCustom>
<InformationBox text="Untuk sementara voting ini belum di buka. Voting akan dimulai sesuai dengan tanggal awal pemilihan, dan akan ditutup sesuai dengan tanggal akhir pemilihan." />
<Voting_BoxDetailPublishSection
headerAvatar={<AvatarUsernameAndOtherComponent />}
/>
<Voting_BoxDetailHasilVotingSection />
</StackCustom>
</ViewWrapper>
{/* ========= Publish Drawer ========= */}
<DrawerCustom
isVisible={openDrawerPublish}
closeDrawer={() => setOpenDrawerPublish(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <IconContribution />,
label: "Daftar Kontributor",
path: `/voting/${id}/list-of-contributor`,
},
{
icon: <IconArchive />,
label: "Update Arsip",
path: "" as any,
},
]}
onPressItem={handlePressPublish as any}
/>
</DrawerCustom>
</>
);
}

View File

@@ -0,0 +1,26 @@
import {
AvatarUsernameAndOtherComponent,
BadgeCustom,
BaseBox,
ViewWrapper,
} from "@/components";
export default function Voting_ListOfContributor() {
return (
<ViewWrapper>
{Array.from({ length: 10 }).map((_, index) => (
<BaseBox paddingTop={5} paddingBottom={5} key={index.toString()}>
<AvatarUsernameAndOtherComponent
rightComponent={
<BadgeCustom
style={{alignSelf: "flex-end" }}
>
Pilihan {index + 1}
</BadgeCustom>
}
/>
</BaseBox>
))}
</ViewWrapper>
);
}

View File

@@ -15,7 +15,7 @@ import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import { TouchableOpacity } from "react-native";
export default function CreateVoting() {
export default function VotingCreate() {
const buttonSubmit = () => {
return (
<>

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React from "react";
import {
View,
@@ -11,6 +12,8 @@ import { Ionicons } from "@expo/vector-icons";
import { router, Stack } from "expo-router";
import EventDetailScreen from "./double-scroll";
import LeftButtonCustom from "@/components/Button/BackButton";
import CustomUploadButton from "./upload-button";
import { SafeAreaView } from "react-native-safe-area-context";
const { width } = Dimensions.get("window");
@@ -117,14 +120,38 @@ const CustomTabNavigator = () => {
const ActiveComponent = getActiveComponent();
const handleImageUpload = (file: any) => {
console.log("Gambar dipilih:", file);
// Upload ke server
};
const handlePdfOrPngUpload = (file: any) => {
console.log("PDF atau PNG dipilih:", file);
};
return (
<>
<Stack.Screen
options={{
title: "Custom Tab Navigator",
}}
/>
<EventDetailScreen />
<SafeAreaView edges={["bottom"]} style={styles.container}>
<Stack.Screen
options={{
title: "Custom Tab Navigator",
}}
/>
<EventDetailScreen />
<CustomUploadButton
allowedExtensions={["jpeg", "png"]}
buttonTitle="Unggah Gambar (JPEG/PNG)"
onFileSelected={handleImageUpload}
/>
{/* Hanya PDF atau PNG */}
<CustomUploadButton
allowedExtensions={["pdf"]}
buttonTitle="Unggah PDF atau PNG"
onFileSelected={handlePdfOrPngUpload}
/>
</SafeAreaView>
</>
// <View style={styles.container}>
// {/* Content Area */}

View File

@@ -0,0 +1,99 @@
// components/CustomUploadButton.tsx
import React from 'react';
import { Button, Alert, View, StyleSheet } from 'react-native';
import * as DocumentPicker from 'expo-document-picker';
import { isValidFileType, getMimeType } from '../../../utils/fileValidation';
interface UploadButtonProps {
allowedExtensions: string[];
buttonTitle?: string;
onFileSelected?: (file: {
uri: string;
name: string;
size: number | null;
mimeType: string;
}) => void;
}
const CustomUploadButton: React.FC<UploadButtonProps> = ({
allowedExtensions,
buttonTitle = 'Pilih File',
onFileSelected,
}) => {
const handlePickFile = async () => {
try {
// Coba filter dengan MIME type jika memungkinkan
const typeFilter = getMimeTypeFilter(allowedExtensions);
const result = await DocumentPicker.getDocumentAsync({
type: typeFilter, // Ini membantu memfilter di UI pemilih
copyToCacheDirectory: true,
});
if (result.canceled) {
Alert.alert('Dibatalkan', 'Tidak ada file yang dipilih.');
return;
}
const file = result.assets[0];
const { uri, name, size } = file;
// Validasi ekstensi secara manual (cadangan jika MIME tidak akurat)
if (!isValidFileType(name, allowedExtensions)) {
Alert.alert(
'Format Tidak Didukung',
`Hanya file dengan ekstensi berikut yang diperbolehkan: ${allowedExtensions.join(', ')}`
);
return;
}
const mimeType = getMimeType(name);
// Kirim data file ke komponen induk
if (onFileSelected) {
onFileSelected({ uri, name, size: size || null, mimeType });
}
Alert.alert('Berhasil', `File ${name} berhasil dipilih!`);
} catch (error) {
console.error('Error picking file:', error);
Alert.alert('Error', 'Terjadi kesalahan saat memilih file.');
}
};
return (
<View style={styles.container}>
<Button title={buttonTitle} onPress={handlePickFile} />
</View>
);
};
// Fungsi bantu untuk menghasilkan MIME type filter dari ekstensi
const getMimeTypeFilter = (extensions: string[]): string => {
const mimeTypes: string[] = [];
extensions.forEach((ext) => {
switch (ext.toLowerCase()) {
case 'jpg':
case 'jpeg':
if (!mimeTypes.includes('image/jpeg')) mimeTypes.push('image/jpeg');
break;
case 'png':
if (!mimeTypes.includes('image/png')) mimeTypes.push('image/png');
break;
case 'pdf':
if (!mimeTypes.includes('application/pdf')) mimeTypes.push('application/pdf');
break;
default:
mimeTypes.push('*/*'); // fallback
}
});
return mimeTypes.length > 0 ? mimeTypes.join(',') : '*/*';
};
const styles = StyleSheet.create({
container: {
marginVertical: 10,
},
});
export default CustomUploadButton;

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

View File

@@ -18,6 +18,8 @@
"expo-camera": "~16.1.10",
"expo-clipboard": "~7.1.5",
"expo-constants": "~17.1.7",
"expo-document-picker": "~13.1.6",
"expo-file-system": "~18.1.11",
"expo-font": "~13.3.2",
"expo-haptics": "~14.1.4",
"expo-image": "~2.3.2",
@@ -36,6 +38,7 @@
"react-native-international-phone-number": "^0.9.3",
"react-native-maps": "1.20.1",
"react-native-otp-entry": "^1.8.5",
"react-native-pager-view": "6.7.1",
"react-native-paper": "^5.14.5",
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0",
@@ -832,6 +835,8 @@
"expo-constants": ["expo-constants@17.1.7", "", { "dependencies": { "@expo/config": "~11.0.12", "@expo/env": "~1.0.7" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-byBjGsJ6T6FrLlhOBxw4EaiMXrZEn/MlUYIj/JAd+FS7ll5X/S4qVRbIimSJtdW47hXMq0zxPfJX6njtA56hHA=="],
"expo-document-picker": ["expo-document-picker@13.1.6", "", { "peerDependencies": { "expo": "*" } }, "sha512-8FTQPDOkyCvFN/i4xyqzH7ELW4AsB6B3XBZQjn1FEdqpozo6rpNJRr7sWFU/93WrLgA9FJEKpKbyr6XxczK6BA=="],
"expo-file-system": ["expo-file-system@18.1.11", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-HJw/m0nVOKeqeRjPjGdvm+zBi5/NxcdPf8M8P3G2JFvH5Z8vBWqVDic2O58jnT1OFEy0XXzoH9UqFu7cHg9DTQ=="],
"expo-font": ["expo-font@13.3.2", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-wUlMdpqURmQ/CNKK/+BIHkDA5nGjMqNlYmW0pJFXY/KE/OG80Qcavdu2sHsL4efAIiNGvYdBS10WztuQYU4X0A=="],
@@ -1392,6 +1397,8 @@
"react-native-otp-entry": ["react-native-otp-entry@1.8.5", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-TZNkIuUzZKAAWrC8X/A22ZHJdycLysxUNysrGf0yTmDLRUyf4zLXwVFcDYUcRNe763Hjaf5qvtKGILb6lDGzoA=="],
"react-native-pager-view": ["react-native-pager-view@6.7.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-cBSr6xw4g5N7Kd3VGWcf+kmaH7iBWb0DXAf2bVo3bXkzBcBbTOmYSvc0LVLHhUPW8nEq5WjT9LCIYAzgF++EXw=="],
"react-native-paper": ["react-native-paper@5.14.5", "", { "dependencies": { "@callstack/react-theme-provider": "^3.0.9", "color": "^3.1.2", "use-latest-callback": "^0.2.3" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-safe-area-context": "*" } }, "sha512-eaIH5bUQjJ/mYm4AkI6caaiyc7BcHDwX6CqNDi6RIxfxfWxROsHpll1oBuwn/cFvknvA8uEAkqLk/vzVihI3AQ=="],
"react-native-reanimated": ["react-native-reanimated@3.17.5", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "invariant": "^2.2.4", "react-native-is-edge-to-edge": "1.1.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-SxBK7wQfJ4UoWoJqQnmIC7ZjuNgVb9rcY5Xc67upXAFKftWg0rnkknTw6vgwnjRcvYThrjzUVti66XoZdDJGtw=="],

View File

@@ -0,0 +1,193 @@
import { useTheme } from "@react-navigation/native";
import React from "react";
import { Animated, StyleSheet, Text, View, ViewStyle } from "react-native";
type ProgressColor =
| "primary"
| "success"
| "warning"
| "error"
| "info"
| "dark";
type ProgressSize = "xs" | "sm" | "md" | "lg" | "xl";
interface ProgressProps {
value?: number | null;
color?: ProgressColor;
size?: ProgressSize;
radius?: number;
style?: ViewStyle;
animated?: boolean;
label?: React.ReactNode; // Konten label (bisa string, number, atau elemen)
showLabel?: boolean; // Jika ingin mengontrol visibilitas
}
const getColor = (color: ProgressColor, isDark: boolean) => {
const palette: Record<ProgressColor, string> = {
primary: "#FFC107",
success: "#228B22",
warning: "#FFA500",
error: "#DC3545",
info: "#177DDC",
dark: isDark ? "#DADADA" : "#212121",
};
return palette[color];
};
const getSize = (size: ProgressSize): number => {
const sizes: Record<ProgressSize, number> = {
xs: 6,
sm: 8,
md: 12,
lg: 16,
xl: 20,
};
return sizes[size];
};
const ProgressCustom: React.FC<ProgressProps> = ({
value = 0,
color = "primary",
size = "md",
radius = 999,
style,
animated = true,
label,
showLabel = true,
}) => {
const { dark } = useTheme();
const isDark = dark ?? false;
const barHeight = getSize(size);
const progressColor = getColor(color, isDark);
const displayValue =
typeof value === "number" ? Math.max(0, Math.min(100, value)) : 0;
// Animasi indeterminate
const translateX = React.useRef(new Animated.Value(-1)).current;
React.useEffect(() => {
if (value === null && animated) {
const animation = Animated.loop(
Animated.timing(translateX, {
toValue: 1,
duration: 1200,
useNativeDriver: true,
})
);
animation.start();
return () => animation.stop();
}
}, [value, animated, translateX]);
const isIndeterminate = value === null;
// Tentukan teks label
const labelText =
label !== undefined
? label
: typeof value === "number"
? `${Math.round(value)}%`
: "";
return (
<View
style={[
styles.container,
{
height: barHeight,
borderRadius: radius,
backgroundColor: isDark
? "rgb(255, 255, 255)"
: "rgba(255, 255, 255, 0.84)",
},
style,
]}
>
{/* Progress Fill */}
{isIndeterminate ? (
<Animated.View
style={[
styles.indeterminateBar,
{
width: "50%",
height: barHeight,
borderRadius: radius,
backgroundColor: progressColor,
transform: [
{
translateX: translateX.interpolate({
inputRange: [-1, 1],
outputRange: [-100, 100],
}),
},
],
},
]}
/>
) : (
<View
style={[
styles.progressBar,
{
width: `${displayValue}%`,
height: barHeight,
borderRadius: radius,
backgroundColor: progressColor,
},
]}
/>
)}
{/* Label di tengah */}
{showLabel && labelText ? (
<View style={StyleSheet.absoluteFill}>
<Text
style={[
styles.label,
{
fontSize: barHeight * 0.7,
lineHeight: barHeight,
color: isDark ? "black" : "black", // Warna teks, bisa disesuaikan
fontWeight: "600",
},
]}
numberOfLines={1}
adjustsFontSizeToFit
>
{labelText}
</Text>
</View>
) : null}
</View>
);
};
export default ProgressCustom;
const styles = StyleSheet.create({
container: {
overflow: "hidden",
backgroundColor: "rgba(0,0,0,0.1)",
width: "100%",
justifyContent: "center", // Pusatkan label secara vertikal
},
progressBar: {
backgroundColor: "#007BFF",
},
indeterminateBar: {
position: "absolute",
top: 0,
left: 0,
backgroundColor: "#007BFF",
},
label: {
textAlign: "center",
width: "100%",
// Hindari overlap dengan background — bisa tambahkan shadow atau background jika perlu
textShadowColor: "rgba(255,255,255,0.6)",
textShadowOffset: { width: 1, height: 1 },
textShadowRadius: 1,
},
});

View File

@@ -0,0 +1,13 @@
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { Ionicons } from "@expo/vector-icons";
export default function IconArchive({ color }: { color?: string }) {
return (
<Ionicons
name="archive"
size={ICON_SIZE_SMALL}
color={color || MainColor.white}
/>
);
}

View File

@@ -6,7 +6,7 @@ export default function IconContribution({ color }: { color?: string }) {
<>
<Ionicons
size={ICON_SIZE_SMALL}
name="extension-puzzle"
name="people"
color={color || "white"}
/>
</>

View File

@@ -3,5 +3,13 @@ import IconEdit from "./IconEdit";
import IconHistory from "./IconHistory";
import IconHome from "./IconHome";
import IconStatus from "./IconStatus";
import IconArchive from "./IconArchive";
export { IconContribution, IconEdit, IconHistory, IconHome, IconStatus };
export {
IconContribution,
IconEdit,
IconHistory,
IconHome,
IconStatus,
IconArchive,
};

View File

@@ -1,8 +1,8 @@
import { ImageSourcePropType, View } from "react-native";
import { ImageSourcePropType } from "react-native";
import Divider from "../Divider/Divider";
import Grid from "../Grid/GridCustom";
import AvatarCustom from "../Image/AvatarCustom";
import TextCustom from "../Text/TextCustom";
import Divider from "../Divider/Divider"
const AvatarUsernameAndOtherComponent = ({
avatarHref,
@@ -19,30 +19,28 @@ const AvatarUsernameAndOtherComponent = ({
}) => {
return (
<>
<Grid containerStyle={{ zIndex: 10 }}>
<Grid.Col span={2}>
<AvatarCustom source={avatar} href={avatarHref as any} />
</Grid.Col>
<Grid containerStyle={{ zIndex: 10 }}>
<Grid.Col span={2}>
<AvatarCustom source={avatar} href={avatarHref as any} />
</Grid.Col>
<Grid.Col
span={rightComponent ? 6 : 10}
style={{ justifyContent: "center" }}
>
<TextCustom truncate bold>
{name || "Username"}
</TextCustom>
</Grid.Col>
{rightComponent && (
<Grid.Col
span={rightComponent ? 6 : 10}
style={{ justifyContent: "center" }}
span={4}
style={{ alignItems: "flex-end", justifyContent: "center" }}
>
<TextCustom truncate bold>
{name || "Username"}
</TextCustom>
{rightComponent}
</Grid.Col>
{rightComponent && (
<Grid.Col
span={4}
style={{ alignItems: "flex-end", justifyContent: "center" }}
>
{rightComponent}
</Grid.Col>
)}
</Grid>
{withBottomLine && <Divider marginTop={0} />}
<View>
</View>
)}
</Grid>
{withBottomLine && <Divider marginTop={0} />}
</>
);
};

View File

@@ -0,0 +1,61 @@
import { MainColor, AccentColor } from "@/constants/color-palet";
import { View } from "react-native";
import ButtonCustom from "../Button/ButtonCustom";
import Spacing from "./Spacing";
export default function TabsTwoHeaderCustom ({
leftValue,
rightValue,
leftText,
rightText,
activeCategory,
handlePress,
}: {
leftValue: string;
rightValue: string;
leftText: string;
rightText: string;
activeCategory: string | null;
handlePress: (item: string) => void;
}) {
return (
<>
<View
style={{
flexDirection: "row",
alignItems: "center",
padding: 5,
backgroundColor: MainColor.soft_darkblue,
borderRadius: 50,
width: "100%",
}}
>
<ButtonCustom
backgroundColor={
activeCategory === leftValue ? MainColor.yellow : AccentColor.blue
}
textColor={
activeCategory === leftValue ? MainColor.black : MainColor.white
}
style={{ width: "49%" }}
onPress={() => handlePress(leftValue)}
>
{leftText}
</ButtonCustom>
<Spacing width={"2%"} />
<ButtonCustom
backgroundColor={
activeCategory === rightValue ? MainColor.yellow : AccentColor.blue
}
textColor={
activeCategory === rightValue ? MainColor.black : MainColor.white
}
style={{ width: "49%" }}
onPress={() => handlePress(rightValue)}
>
{rightText}
</ButtonCustom>
</View>
</>
);
}

View File

@@ -55,6 +55,8 @@ import TabBarBackground from "./_ShareComponent/TabBarBackground";
import ViewWrapper from "./_ShareComponent/ViewWrapper";
import SearchInput from "./_ShareComponent/SearchInput";
import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage";
// Progress
import ProgressCustom from "./Progress/ProgressCustom";
export {
AlertCustom,
@@ -96,6 +98,8 @@ export {
// Map
MapCustom,
MenuDrawerDynamicGrid,
// Progress
ProgressCustom,
// Scroll
ScrollableCustom,
// Select

View File

@@ -0,0 +1,20 @@
const dummyPembagianDeviden = [
{
id: "1",
name: "3",
},
{
id: "2",
name: "6",
},
{
id: "3",
name: "9",
},
{
id: "4",
name: "12",
},
];
export default dummyPembagianDeviden;

View File

@@ -0,0 +1,20 @@
const dummyListPencarianInvestor = [
{
id: "1",
name: "30",
},
{
id: "2",
name: "60",
},
{
id: "3",
name: "90",
},
{
id: "4",
name: "120",
},
];
export default dummyListPencarianInvestor;

View File

@@ -0,0 +1,12 @@
const dummyPeriodeDeviden = [
{
id: "1",
name: "Selamanya",
},
{
id: "2",
name: "Satu tahun",
},
];
export default dummyPeriodeDeviden;

View File

@@ -25,6 +25,8 @@
"expo-camera": "~16.1.10",
"expo-clipboard": "~7.1.5",
"expo-constants": "~17.1.7",
"expo-document-picker": "~13.1.6",
"expo-file-system": "~18.1.11",
"expo-font": "~13.3.2",
"expo-haptics": "~14.1.4",
"expo-image": "~2.3.2",
@@ -43,6 +45,7 @@
"react-native-international-phone-number": "^0.9.3",
"react-native-maps": "1.20.1",
"react-native-otp-entry": "^1.8.5",
"react-native-pager-view": "6.7.1",
"react-native-paper": "^5.14.5",
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0",

View File

@@ -1,8 +1,6 @@
import { BaseBox, ButtonCustom, StackCustom, TextCustom } from "@/components";
import { BaseBox, StackCustom, TextCustom } from "@/components";
import { RadioCustom, RadioGroup } from "@/components/Radio/RadioCustom";
import { MainColor } from "@/constants/color-palet";
import { listDummyReportForum } from "@/lib/dummy-data/forum/report-list";
import { router } from "expo-router";
import { useState } from "react";
import { View } from "react-native";

View File

@@ -30,33 +30,14 @@ export default function Home_FeatureSection() {
<Ionicons name="cube" size={48} color="white" />
<Text style={stylesHome.gridLabel}>Voting</Text>
</TouchableOpacity>
<TouchableOpacity style={stylesHome.gridItem}>
<TouchableOpacity
style={stylesHome.gridItem}
onPress={() => router.push("/(application)/(user)/crowdfunding")}
>
<Ionicons name="heart" size={48} color="white" />
<Text style={stylesHome.gridLabel}>Crowdfunding</Text>
</TouchableOpacity>
</View>
{/* <View style={stylesHome.gridContainer}>
<TouchableOpacity
style={stylesHome.gridItem}
onPress={() => router.push("/(application)/event")}
>
<Ionicons name="analytics" size={48} color="white" />
<Text style={stylesHome.gridLabel}>Event</Text>
</TouchableOpacity>
<TouchableOpacity style={stylesHome.gridItem}>
<Ionicons name="share" size={48} color="white" />
<Text style={stylesHome.gridLabel}>Collaboration</Text>
</TouchableOpacity>
<TouchableOpacity style={stylesHome.gridItem}>
<Ionicons name="cube" size={48} color="white" />
<Text style={stylesHome.gridLabel}>Voting</Text>
</TouchableOpacity>
<TouchableOpacity style={stylesHome.gridItem}>
<Ionicons name="heart" size={48} color="white" />
<Text style={stylesHome.gridLabel}>Crowdfunding</Text>
</TouchableOpacity>
</View> */}
</>
);
}

View File

@@ -0,0 +1,35 @@
import {
BadgeCustom,
BoxWithHeaderSection,
Spacing,
StackCustom,
TextCustom,
} from "@/components";
import { GStyles } from "@/styles/global-styles";
import { Voting_ComponentDetailDataSection } from "./ComponentDetailDataSection";
export function Voting_BoxDetailContributionSection({
headerAvatar,
}: {
headerAvatar?: React.ReactNode;
}) {
return (
<>
<BoxWithHeaderSection>
{headerAvatar ? headerAvatar : <Spacing />}
<StackCustom gap={"lg"}>
<Voting_ComponentDetailDataSection />
<StackCustom gap={"xs"}>
<TextCustom bold size="small" align="center">
Pilihan Anda
</TextCustom>
<BadgeCustom style={[GStyles.alignSelfCenter]}>
Pilihan 1
</BadgeCustom>
</StackCustom>
</StackCustom>
</BoxWithHeaderSection>
</>
);
}

View File

@@ -0,0 +1,30 @@
import {
BaseBox,
StackCustom,
TextCustom,
Grid,
CircleContainer,
} from "@/components";
export default function Voting_BoxDetailHasilVotingSection() {
return (
<>
<BaseBox>
<StackCustom>
<TextCustom bold align="center">
Hasil Voting
</TextCustom>
<Grid>
{Array.from({ length: 4 }).map((_, i) => (
<Grid.Col span={3} style={{ alignItems: "center" }} key={i}>
<CircleContainer value={9 % (i + 4)} />
<TextCustom size="small">Pilihan {i + 1}</TextCustom>
</Grid.Col>
))}
</Grid>
</StackCustom>
</BaseBox>
</>
);
}

View File

@@ -0,0 +1,23 @@
import {
BoxWithHeaderSection,
Spacing,
StackCustom
} from "@/components";
import { Voting_ComponentDetailDataSection } from "./ComponentDetailDataSection";
export function Voting_BoxDetailHistorySection({
headerAvatar,
}: {
headerAvatar?: React.ReactNode;
}) {
return (
<>
<BoxWithHeaderSection>
{headerAvatar ? headerAvatar : <Spacing />}
<StackCustom>
<Voting_ComponentDetailDataSection />
</StackCustom>
</BoxWithHeaderSection>
</>
);
}

View File

@@ -0,0 +1,47 @@
import {
BoxWithHeaderSection,
ButtonCustom,
Spacing,
StackCustom,
TextCustom
} from "@/components";
import { RadioCustom, RadioGroup } from "@/components/Radio/RadioCustom";
import { useState } from "react";
import { View } from "react-native";
import { Voting_ComponentDetailDataSection } from "./ComponentDetailDataSection";
export function Voting_BoxDetailPublishSection({
headerAvatar,
}: {
headerAvatar?: React.ReactNode;
}) {
const [value, setValue] = useState<any | number>("");
return (
<>
<BoxWithHeaderSection>
{headerAvatar ? headerAvatar : <Spacing />}
<StackCustom gap={"lg"}>
<Voting_ComponentDetailDataSection />
<View>
<TextCustom bold size="small">
Pilihan :
</TextCustom>
<RadioGroup value={value} onChange={setValue}>
{Array.from({ length: 4 }).map((_, i) => (
<View key={i}>
<RadioCustom
label={`Pilihan ${i + 1}`}
value={`Pilihan ${i + 1}`}
/>
</View>
))}
</RadioGroup>
</View>
<ButtonCustom onPress={() => console.log("vote")}>Vote</ButtonCustom>
</StackCustom>
</BoxWithHeaderSection>
</>
);
}

View File

@@ -0,0 +1,38 @@
import {
BoxWithHeaderSection,
Spacing,
StackCustom,
TextCustom
} from "@/components";
import { View } from "react-native";
import { Voting_ComponentDetailDataSection } from "./ComponentDetailDataSection";
export function Voting_BoxDetailSection({
headerAvatar,
}: {
headerAvatar?: React.ReactNode;
}) {
return (
<>
<BoxWithHeaderSection>
{headerAvatar ? headerAvatar : <Spacing />}
<StackCustom>
<Voting_ComponentDetailDataSection/>
<Spacing/>
<View>
<TextCustom bold size="small">
Pilihan :
</TextCustom>
{Array.from({ length: 3 }).map((_, i) => (
<View key={i}>
<TextCustom>Nama Pilihan {i + 1}</TextCustom>
<Spacing />
</View>
))}
</View>
</StackCustom>
</BoxWithHeaderSection>
</>
);
}

View File

@@ -9,20 +9,25 @@ import {
CircleContainer,
} from "@/components";
import dayjs from "dayjs";
import { Href } from "expo-router";
export default function Voting_BoxPublishSection({
href,
id,
bottomComponent,
}: {
href?: Href
id?: string
bottomComponent?: React.ReactNode;
}) {
return (
<>
<BoxWithHeaderSection>
<BoxWithHeaderSection href={href}>
<AvatarUsernameAndOtherComponent avatarHref="/profile/1" />
<Spacing />
<StackCustom>
<TextCustom align="center" bold truncate size="large">
Voting Title Here
Voting Title {id}
</TextCustom>
<BadgeCustom
@@ -34,18 +39,12 @@ export default function Voting_BoxPublishSection({
</BadgeCustom>
<Grid>
<Grid.Col span={3} style={{ alignItems: "center" }}>
<CircleContainer value={"10"} />
</Grid.Col>
<Grid.Col span={3} style={{ alignItems: "center" }}>
<CircleContainer value={"9"} />
</Grid.Col>
<Grid.Col span={3} style={{ alignItems: "center" }}>
<CircleContainer value={"10"} />
</Grid.Col>
<Grid.Col span={3} style={{ alignItems: "center" }}>
<CircleContainer value={"9"} />
</Grid.Col>
{Array.from({ length: 4 }).map((_, i) => (
<Grid.Col span={3} style={{ alignItems: "center" }} key={i}>
<CircleContainer value={9 % (i + 4)} />
<TextCustom size="small">Pilihan {i + 1}</TextCustom>
</Grid.Col>
))}
</Grid>
{bottomComponent}
</StackCustom>

View File

@@ -0,0 +1,124 @@
import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components";
import { router } from "expo-router";
import { View } from "react-native";
export default function Voting_ButtonStatusSection({
status,
}: {
status: string;
}) {
const handleBatalkanReview = () => {
AlertDefaultSystem({
title: "Batalkan Review",
message: "Apakah Anda yakin ingin batalkan review ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
console.log("Hapus");
router.back();
},
});
};
const handleAjukanReview = () => {
AlertDefaultSystem({
title: "Ajukan Review",
message: "Apakah Anda yakin ingin ajukan review ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
console.log("Hapus");
router.back();
},
});
};
const handleEditKembali = () => {
AlertDefaultSystem({
title: "Edit Kembali",
message: "Apakah Anda yakin ingin edit kembali ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
console.log("Hapus");
router.back();
},
});
};
const handleOpenDeleteAlert = () => {
AlertDefaultSystem({
title: "Hapus",
message: "Apakah Anda yakin ingin menghapus data ini?",
textLeft: "Batal",
textRight: "Hapus",
onPressRight: () => {
console.log("Hapus");
router.back();
},
});
};
const DeleteButton = () => {
return (
<>
<ButtonCustom
backgroundColor="red"
textColor="white"
onPress={handleOpenDeleteAlert}
>
Hapus
</ButtonCustom>
</>
);
};
switch (status) {
case "publish":
return <></>;
case "review":
return (
<ButtonCustom onPress={handleBatalkanReview}>
Batalkan Review
</ButtonCustom>
);
case "draft":
return (
<>
<Grid>
<Grid.Col span={5}>
<ButtonCustom onPress={handleAjukanReview}>
Ajukan Review
</ButtonCustom>
</Grid.Col>
<Grid.Col span={2}>
<View />
</Grid.Col>
<Grid.Col span={5}>{DeleteButton()}</Grid.Col>
</Grid>
</>
);
case "reject":
return (
<>
<Grid>
<Grid.Col span={5}>
<ButtonCustom onPress={handleEditKembali}>
Edit Kembali
</ButtonCustom>
</Grid.Col>
<Grid.Col span={2}>
<View />
</Grid.Col>
<Grid.Col span={5}>{DeleteButton()}</Grid.Col>
</Grid>
</>
);
default:
return <ButtonCustom disabled>Status Undifined</ButtonCustom>;
}
}

View File

@@ -0,0 +1,32 @@
import { BadgeCustom, TextCustom } from "@/components";
import { GStyles } from "@/styles/global-styles";
import dayjs from "dayjs";
import { View } from "react-native";
export function Voting_ComponentDetailDataSection() {
return (
<>
<TextCustom align="center" bold size="large">
Title of Voting Here
</TextCustom>
<TextCustom>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Perspiciatis
corporis blanditiis est provident corrupti facilis iste cum voluptate.
Natus eum aut quos consequatur doloribus fugiat sit ullam minima non
enim?
</TextCustom>
<View>
<TextCustom bold size="small" align="center">
Batas Voting
</TextCustom>
<BadgeCustom
style={[GStyles.alignSelfCenter, { width: "70%" }]}
variant="light"
>
{dayjs().format("DD/MM/YYYY")} -{" "}
{dayjs().add(1, "day").format("DD/MM/YYYY")}
</BadgeCustom>
</View>
</>
);
}

View File

@@ -320,4 +320,7 @@ export const GStyles = StyleSheet.create({
alignSelfCenter: {
alignSelf: "center",
},
alignSelfFlexEnd: {
alignSelf: "flex-end",
},
});

34
utils/fileValidation.ts Normal file
View File

@@ -0,0 +1,34 @@
// utils/fileValidation.ts
const ALLOWED_TYPES: Record<string, string[]> = {
image: ["jpeg", "jpg", "png"],
document: ["pdf", "png"],
pdf: ["pdf"],
png: ["png"],
};
export const isValidFileType = (
fileName: string,
allowedExtensions: string[]
): boolean => {
const extension = fileName.split(".").pop()?.toLowerCase();
return !!extension && allowedExtensions.includes(extension);
};
// Helper: Dapatkan MIME type berdasarkan ekstensi (opsional)
export const getMimeType = (fileName: string): string => {
const ext = fileName.split(".").pop()?.toLowerCase();
switch (ext) {
case "jpg":
return "image/jpg";
case "jpeg":
return "image/jpeg";
case "png":
return "image/png";
case "pdf":
return "application/pdf";
default:
return "application/octet-stream";
}
};
export default ALLOWED_TYPES;