deskripsi:
- new component camera
This commit is contained in:
2025-07-04 15:36:09 +08:00
parent 17e6208aae
commit b54693caa7
15 changed files with 528 additions and 42 deletions

View File

@@ -38,6 +38,14 @@
"resizeMode": "contain",
"backgroundColor": "#ffffff"
}
],
[
"expo-camera",
{
"cameraPermission": "Allow $(PRODUCT_NAME) to access your camera",
"microphonePermission": "Allow $(PRODUCT_NAME) to access your microphone",
"recordAudioAndroid": true
}
]
],
"experiments": {

View File

@@ -3,23 +3,22 @@ import {
ButtonCustom,
SelectCustom,
StackCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { StyleSheet, Text } from "react-native";
import { StyleSheet } from "react-native";
export default function ProfileEdit() {
const { id } = useLocalSearchParams();
const [nama, setNama] = useState("Bagas Banuna");
const [email, setEmail] = useState("bagasbanuna@gmail.com");
const [alamat, setAlamat] = useState("Bandar Lampung");
const [selectedValue, setSelectedValue] = useState<string | number>("");
const [data, setData] = useState({
nama: "Bagas Banuna",
email: "bagasbanuna@gmail.com",
alamat: "Jember",
selectedValue: "",
});
const options = [
{ label: "React", value: "react" },
@@ -33,15 +32,24 @@ export default function ProfileEdit() {
{ label: "SvelteKit", value: "sveltekit" },
];
const handleSave = () => {
console.log({
nama: data.nama,
email: data.email,
alamat: data.alamat,
selectedValue: data.selectedValue,
});
router.back();
};
return (
<ViewWrapper
bottomBarComponent={
<ButtonCustom
disabled={!nama || !email || !alamat || !selectedValue}
onPress={() => {
console.log("data >>", nama, email, alamat, selectedValue);
router.back();
}}
disabled={
!data.nama || !data.email || !data.alamat || !data.selectedValue
}
onPress={handleSave}
>
Simpan
</ButtonCustom>
@@ -52,37 +60,36 @@ export default function ProfileEdit() {
label="Framework"
placeholder="Pilih framework favoritmu"
data={options}
value={selectedValue}
onChange={setSelectedValue}
value={data.selectedValue}
onChange={(value) => {
setData({ ...(data as any), selectedValue: value });
}}
/>
{/* {selectedValue && (
<Text style={styles.result}>Terpilih: {selectedValue}</Text>
)} */}
<TextInputCustom
label="Nama"
placeholder="Nama"
value={nama}
value={data.nama}
onChangeText={(text) => {
setNama(text);
setData({ ...data, nama: text });
}}
required
/>
<TextInputCustom
label="Email"
placeholder="Email"
value={email}
value={data.email}
onChangeText={(text) => {
setEmail(text);
setData({ ...data, email: text });
}}
required
/>
<TextInputCustom
label="Alamat"
placeholder="Alamat"
value={alamat}
value={data.alamat}
onChangeText={(text) => {
setAlamat(text);
setData({ ...data, alamat: text });
}}
required
/>

View File

@@ -0,0 +1,168 @@
import {
ButtonCustom,
Spacing,
StackCustom,
ViewWrapper
} from "@/components";
import AntDesign from "@expo/vector-icons/AntDesign";
import FontAwesome6 from "@expo/vector-icons/FontAwesome6";
import { CameraType, CameraView, useCameraPermissions } from "expo-camera";
import { Image } from "expo-image";
import * as ImagePicker from "expo-image-picker";
import { router } from "expo-router";
import { useRef, useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
export default function TakePictureProfile() {
const [permission, requestPermission] = useCameraPermissions();
const ref = useRef<CameraView>(null);
const [uri, setUri] = useState<string | null>(null);
const [facing, setFacing] = useState<CameraType>("back");
if (!permission?.granted) {
return (
<View style={styles.container}>
<Text style={{ textAlign: "center" }}>
We need your permission to use the camera
</Text>
<Pressable onPress={requestPermission}>
<Text style={{ color: "blue", marginTop: 10 }}>Grant permission</Text>
</Pressable>
</View>
);
}
const takePicture = async () => {
const photo = await ref.current?.takePictureAsync();
setUri(photo?.uri || null);
};
const pickImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 1,
});
if (!result.canceled) {
setUri(result.assets[0].uri);
}
};
const toggleFacing = () => {
setFacing((prev) => (prev === "back" ? "front" : "back"));
};
const renderPicture = () => {
return (
<View>
<Image
source={uri ? uri : null}
contentFit="contain"
style={{ width: 340, aspectRatio: 1 }}
/>
<Spacing />
<StackCustom>
<ButtonCustom onPress={() => setUri(null)} title="Foto ulang" />
<ButtonCustom
onPress={() => {
console.log("Update foto");
router.back();
}}
title="Update Foto"
/>
</StackCustom>
</View>
);
};
const renderCameraUI = () => {
return (
<View style={styles.cameraOverlay}>
<View style={styles.shutterContainer}>
<Pressable onPress={toggleFacing}>
<FontAwesome6 name="rotate-left" size={32} color="white" />
</Pressable>
<Pressable onPress={takePicture}>
{({ pressed }) => (
<View
style={[
styles.shutterBtn,
{
opacity: pressed ? 0.5 : 1,
},
]}
>
<View style={styles.shutterBtnInner} />
</View>
)}
</Pressable>
<Pressable onPress={pickImage}>
<AntDesign name="folderopen" size={32} color="white" />
</Pressable>
</View>
</View>
);
};
return (
<>
{uri ? (
<ViewWrapper>
<View style={styles.container}>{renderPicture()}</View>
</ViewWrapper>
) : (
<>
<CameraView
style={styles.camera}
ref={ref}
facing={facing}
responsiveOrientationWhenOrientationLocked
/>
{renderCameraUI()}
</>
)}
</>
);
}
const styles = StyleSheet.create({
container: {
justifyContent: "center",
alignItems: "center",
},
camera: {
flex: 1,
width: "100%",
},
cameraOverlay: {
...StyleSheet.absoluteFillObject,
justifyContent: "flex-end",
padding: 44,
},
shutterContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
shutterBtn: {
backgroundColor: "transparent",
borderWidth: 5,
borderColor: "white",
width: 85,
height: 85,
borderRadius: 45,
alignItems: "center",
justifyContent: "center",
},
shutterBtnInner: {
width: 70,
height: 70,
borderRadius: 50,
backgroundColor: "white",
},
});

View File

@@ -0,0 +1,190 @@
// COMPONENT : Jika ingin uoload gambar dan video gunakan component ini
import {
ButtonCustom,
Spacing,
StackCustom,
ViewWrapper
} from "@/components";
import AntDesign from "@expo/vector-icons/AntDesign";
import Feather from "@expo/vector-icons/Feather";
import FontAwesome6 from "@expo/vector-icons/FontAwesome6";
import {
CameraMode,
CameraType,
CameraView,
useCameraPermissions,
} from "expo-camera";
import { Image } from "expo-image";
import { router } from "expo-router";
import { useRef, useState } from "react";
import { Button, Pressable, StyleSheet, Text, View } from "react-native";
export default function TakePictureProfile2() {
const [permission, requestPermission] = useCameraPermissions();
const ref = useRef<CameraView>(null);
const [uri, setUri] = useState<string | null>(null);
const [mode, setMode] = useState<CameraMode>("picture");
const [facing, setFacing] = useState<CameraType>("back");
const [recording, setRecording] = useState(false);
if (!permission?.granted) {
return (
<View style={styles.container}>
<Text style={{ textAlign: "center" }}>
We need your permission to use the camera
</Text>
<Button onPress={requestPermission} title="Grant permission" />
</View>
);
}
const takePicture = async () => {
const photo = await ref.current?.takePictureAsync();
setUri(photo?.uri || null);
};
const recordVideo = async () => {
if (recording) {
setRecording(false);
ref.current?.stopRecording();
return;
}
setRecording(true);
const video = await ref.current?.recordAsync();
console.log({ video });
};
const toggleMode = () => {
setMode((prev) => (prev === "picture" ? "video" : "picture"));
};
const toggleFacing = () => {
setFacing((prev) => (prev === "back" ? "front" : "back"));
};
const renderPicture = () => {
console.log("renderPicture", uri);
return (
<View>
<Image
source={uri ? uri : null}
contentFit="contain"
style={{ width: 340, aspectRatio: 1 }}
/>
<Spacing />
<StackCustom>
<ButtonCustom onPress={() => setUri(null)} title="Foto ulang" />
<ButtonCustom
onPress={() => {
console.log("Update foto");
router.back();
}}
title="Update Foto"
/>
</StackCustom>
</View>
);
};
const renderCameraUI = () => {
return (
<View style={styles.cameraOverlay}>
<View style={styles.shutterContainer}>
<Pressable onPress={toggleMode}>
{mode === "picture" ? (
<AntDesign name="picture" size={32} color="white" />
) : (
<Feather name="video" size={32} color="white" />
)}
</Pressable>
<Pressable onPress={mode === "picture" ? takePicture : recordVideo}>
{({ pressed }) => (
<View
style={[
styles.shutterBtn,
{
opacity: pressed ? 0.5 : 1,
},
]}
>
<View
style={[
styles.shutterBtnInner,
{
backgroundColor: mode === "picture" ? "white" : "red",
},
]}
/>
</View>
)}
</Pressable>
<Pressable onPress={toggleFacing}>
<FontAwesome6 name="rotate-left" size={32} color="white" />
</Pressable>
</View>
</View>
);
};
return (
<>
{uri ? (
<ViewWrapper>
<View style={styles.container}>{renderPicture()}</View>
</ViewWrapper>
) : (
<>
<CameraView
style={styles.camera}
ref={ref}
mode={mode}
facing={facing}
mute={false}
responsiveOrientationWhenOrientationLocked
/>
{renderCameraUI()}
</>
)}
</>
);
}
const styles = StyleSheet.create({
container: {
justifyContent: "center",
alignItems: "center",
},
camera: {
flex: 1,
width: "100%",
},
cameraOverlay: {
...StyleSheet.absoluteFillObject,
justifyContent: "flex-end",
padding: 44,
},
shutterContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
shutterBtn: {
backgroundColor: "transparent",
borderWidth: 5,
borderColor: "white",
width: 85,
height: 85,
borderRadius: 45,
alignItems: "center",
justifyContent: "center",
},
shutterBtnInner: {
width: 70,
height: 70,
borderRadius: 50,
},
});

View File

@@ -1,11 +1,41 @@
import { Text, View } from "react-native";
import { useLocalSearchParams } from "expo-router";
/* eslint-disable @typescript-eslint/no-unused-vars */
import { BaseBox, ButtonCustom } from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import ButtonUpload from "@/components/Button/ButtonUpload";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { router, useLocalSearchParams } from "expo-router";
import { Image } from "react-native";
export default function UpdatePhotoBackground() {
export default function UpdateBackgroundProfile() {
const { id } = useLocalSearchParams();
return (
<View>
<Text>Update Photo Background {id}</Text>
</View>
)
<ViewWrapper
bottomBarComponent={
<ButtonCustom
onPress={() => {
console.log("Simpan foto");
router.back();
}}
>
Simpan
</ButtonCustom>
}
>
<BaseBox
style={{ alignItems: "center", justifyContent: "center", height: 250 }}
>
<Image
source={DUMMY_IMAGE.background}
resizeMode="cover"
style={{ width: "100%", height: "100%", borderRadius: 10 }}
/>
</BaseBox>
<ButtonUpload
onPress={() =>
router.navigate("/(application)/profile/[id]/take-picture")
}
/>
</ViewWrapper>
);
}

View File

@@ -1,11 +1,41 @@
import { Text, View } from "react-native";
import { useLocalSearchParams } from "expo-router";
import { BaseBox, ButtonCustom } from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import ButtonUpload from "@/components/Button/ButtonUpload";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { router, useLocalSearchParams } from "expo-router";
import { Image } from "react-native";
export default function UpdatePhotoProfile() {
const { id } = useLocalSearchParams();
return (
<View>
<Text>Update Photo Profile {id}</Text>
</View>
<ViewWrapper
bottomBarComponent={
<ButtonCustom
onPress={() => {
console.log("Simpan foto");
router.back();
}}
>
Simpan
</ButtonCustom>
}
>
<BaseBox
style={{ alignItems: "center", justifyContent: "center", height: 250 }}
>
<Image
source={DUMMY_IMAGE.avatar}
resizeMode="cover"
style={{ width: 200, height: 200 }}
/>
</BaseBox>
<ButtonUpload
onPress={() => {
console.log("ID >>", id);
router.navigate("/(application)/profile/[id]/take-picture");
}}
/>
</ViewWrapper>
);
}

View File

@@ -24,6 +24,14 @@ export default function ProfileLayout() {
name="[id]/update-background"
options={{ title: "Update Latar Belakang" }}
/>
<Stack.Screen
name="[id]/take-picture"
options={{ title: "Ambil Foto" }}
/>
<Stack.Screen
name="[id]/take-picture2"
options={{ title: "Ambil Foto 2" }}
/>
</Stack>
</>
);

View File

@@ -13,10 +13,12 @@
"@types/react-native-vector-icons": "^6.4.18",
"expo": "53.0.13",
"expo-blur": "~14.1.5",
"expo-camera": "~16.1.10",
"expo-constants": "~17.1.6",
"expo-font": "~13.3.1",
"expo-haptics": "~14.1.4",
"expo-image": "~2.3.0",
"expo-image-picker": "~16.1.4",
"expo-linking": "~7.1.5",
"expo-router": "~5.1.1",
"expo-splash-screen": "~0.30.9",
@@ -811,6 +813,8 @@
"expo-blur": ["expo-blur@14.1.5", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-CCLJHxN4eoAl06ESKT3CbMasJ98WsjF9ZQEJnuxtDb9ffrYbZ+g9ru84fukjNUOTtc8A8yXE5z8NgY1l0OMrmQ=="],
"expo-camera": ["expo-camera@16.1.10", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-qoRJeSwPmMbuu0VfnQTC+q79Kt2SqTWColEImgithL9u0qUQcC55U89IfhZk55Hpt6f1DgKuDzUOG5oY+snSWg=="],
"expo-constants": ["expo-constants@17.1.6", "", { "dependencies": { "@expo/config": "~11.0.9", "@expo/env": "~1.0.5" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-q5mLvJiLtPcaZ7t2diSOlQ2AyxIO8YMVEJsEfI/ExkGj15JrflNQ7CALEW6IF/uNae/76qI/XcjEuuAyjdaCNw=="],
"expo-file-system": ["expo-file-system@18.1.10", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-SyaWg+HitScLuyEeSG9gMSDT0hIxbM9jiZjSBP9l9zMnwZjmQwsusE6+7qGiddxJzdOhTP4YGUfvEzeeS0YL3Q=="],
@@ -821,6 +825,10 @@
"expo-image": ["expo-image@2.3.0", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" } }, "sha512-muL8OSbgCskQJsyqenKPNULWXwRm5BY2ruS6WMo/EzFyI3iXI/0mXgb2J/NXUa8xCEYxSyoGkGZFyCBvGY1ofA=="],
"expo-image-loader": ["expo-image-loader@5.1.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-sEBx3zDQIODWbB5JwzE7ZL5FJD+DK3LVLWBVJy6VzsqIA6nDEnSFnsnWyCfCTSvbGigMATs1lgkC2nz3Jpve1Q=="],
"expo-image-picker": ["expo-image-picker@16.1.4", "", { "dependencies": { "expo-image-loader": "~5.1.0" }, "peerDependencies": { "expo": "*" } }, "sha512-bTmmxtw1AohUT+HxEBn2vYwdeOrj1CLpMXKjvi9FKSoSbpcarT4xxI0z7YyGwDGHbrJqyyic3I9TTdP2J2b4YA=="],
"expo-keep-awake": ["expo-keep-awake@14.1.4", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-wU9qOnosy4+U4z/o4h8W9PjPvcFMfZXrlUoKTMBW7F4pLqhkkP/5G4EviPZixv4XWFMjn1ExQ5rV6BX8GwJsWA=="],
"expo-linking": ["expo-linking@7.1.5", "", { "dependencies": { "expo-constants": "~17.1.6", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-8g20zOpROW78bF+bLI4a3ZWj4ntLgM0rCewKycPL0jk9WGvBrBtFtwwADJgOiV1EurNp3lcquerXGlWS+SOQyA=="],

View File

@@ -1,7 +1,7 @@
// components/Button/Button.tsx
import React from "react";
import { Text, TouchableOpacity } from "react-native";
import { StyleProp, Text, TouchableOpacity, ViewStyle } from "react-native";
import buttonStyles from "./buttonCustomStyles";
import { radiusMap } from "@/constants/radius-value";
import { MainColor } from "@/constants/color-palet";
@@ -21,6 +21,7 @@ interface ButtonProps {
radius?: RadiusType; // ← bisa string enum atau number
disabled?: boolean;
iconLeft?: React.ReactNode;
style?: StyleProp<ViewStyle>;
}
const ButtonCustom: React.FC<ButtonProps> = ({
@@ -32,6 +33,7 @@ const ButtonCustom: React.FC<ButtonProps> = ({
radius = "full", // default md
disabled = false,
iconLeft,
style,
}) => {
const borderRadius =
typeof radius === "number" ? radius : radiusMap[radius ?? "md"]; // fallback ke 'md'
@@ -44,7 +46,7 @@ const ButtonCustom: React.FC<ButtonProps> = ({
return (
<TouchableOpacity
style={[styles.button, disabled && styles.disabled]}
style={[styles.button, disabled && styles.disabled, style]}
onPress={onPress}
disabled={disabled}
activeOpacity={0.8}

View File

@@ -0,0 +1,21 @@
import { MainColor } from "@/constants/color-palet";
import { GStyles } from "@/styles/global-styles";
import { Feather } from "@expo/vector-icons";
import React from "react";
import ButtonCustom from "./ButtonCustom";
interface ButtonUploadProps {
title?: string;
onPress: () => void;
}
export default function ButtonUpload({ onPress, title = "Upload" }: ButtonUploadProps) {
return (
<ButtonCustom
onPress={onPress}
iconLeft={<Feather name="upload" size={20} color={MainColor.black} />}
style={GStyles.buttonCentered50Percent}
>
{title}
</ButtonCustom>
);
}

View File

View File

@@ -0,0 +1,6 @@
const DUMMY_IMAGE = {
avatar: require("@/assets/images/dummy/dummy-avatar.png"),
background: require("@/assets/images/dummy/dummy-image-background.jpg"),
};
export default DUMMY_IMAGE;

View File

@@ -20,10 +20,12 @@
"@types/react-native-vector-icons": "^6.4.18",
"expo": "53.0.13",
"expo-blur": "~14.1.5",
"expo-camera": "~16.1.10",
"expo-constants": "~17.1.6",
"expo-font": "~13.3.1",
"expo-haptics": "~14.1.4",
"expo-image": "~2.3.0",
"expo-image-picker": "~16.1.4",
"expo-linking": "~7.1.5",
"expo-router": "~5.1.1",
"expo-splash-screen": "~0.30.9",

View File

@@ -155,4 +155,10 @@ export const GStyles = StyleSheet.create({
paddingVertical: 10,
},
// =============== BOTTOM BAR =============== //
// =============== BUTTON =============== //
buttonCentered50Percent: {
width: "50%",
alignSelf: "center",
},
});

View File

@@ -13,5 +13,5 @@
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts"
, "components/Button/ButtonCustom" ]
]
}