diff --git a/package-lock.json b/package-lock.json index 03ee1e4..f9ed4c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,16 @@ "version": "0.0.1", "license": "MIT", "dependencies": { + "@capacitor-community/filesystem-react": "^0.1.0", "@capacitor/android": "3.3.2", "@capacitor/app": "1.0.6", + "@capacitor/camera": "^1.2.1", "@capacitor/core": "^3.3.2", "@capacitor/geolocation": "^1.1.3", "@capacitor/haptics": "1.1.3", "@capacitor/keyboard": "1.1.3", "@capacitor/status-bar": "1.0.6", + "@ionic/pwa-elements": "^3.0.2", "@ionic/react": "^5.5.0", "@ionic/react-router": "^5.5.0", "@react-google-maps/api": "^2.5.0", @@ -1871,6 +1874,16 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, + "node_modules/@capacitor-community/filesystem-react": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@capacitor-community/filesystem-react/-/filesystem-react-0.1.0.tgz", + "integrity": "sha512-DS35YDBM30+/ly2aYINCGDALbk0fc91y/nj1J4PRY7AcOs+/eONAxw4woXlmgq2JsPMF1TyNmWws5fQp20+ASg==", + "peerDependencies": { + "@capacitor/core": ">=3.0.0", + "@capacitor/filesystem": "*", + "react": "*" + } + }, "node_modules/@capacitor/android": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-3.3.2.tgz", @@ -1887,6 +1900,14 @@ "@capacitor/core": "^3.0.0" } }, + "node_modules/@capacitor/camera": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@capacitor/camera/-/camera-1.2.1.tgz", + "integrity": "sha512-CVT4ITwCK4AeTpNNlYFb0vuqWVD6490PWHqdaPV977clySbspDbrwPeWFKkx8emBnoNHW5urKN7tz737PD7jcg==", + "peerDependencies": { + "@capacitor/core": "^3.0.0" + } + }, "node_modules/@capacitor/cli": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-3.3.2.tgz", @@ -1926,6 +1947,15 @@ "tslib": "^2.1.0" } }, + "node_modules/@capacitor/filesystem": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-1.0.6.tgz", + "integrity": "sha512-8xqUbDZFGBMhgqoBSn9wEd9OBPdHIRegQ9zCCZcpHNf3FFAIby1ck+aDFnoq+Da49xhD6ks1SKCBSxz/26qWTw==", + "peer": true, + "peerDependencies": { + "@capacitor/core": "^3.0.0" + } + }, "node_modules/@capacitor/geolocation": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-1.2.0.tgz", @@ -2640,6 +2670,11 @@ "tslib": "^2.1.0" } }, + "node_modules/@ionic/pwa-elements": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@ionic/pwa-elements/-/pwa-elements-3.0.2.tgz", + "integrity": "sha512-Wwb8/NgzeX1GA8MDPvcis7nx1vqtWdEaDz3t0tTbtUduR/oCNkWZkY2PFL49ET0+/lRhcqE2jZwXvTVn0R1UGw==" + }, "node_modules/@ionic/react": { "version": "5.9.1", "resolved": "https://registry.npmjs.org/@ionic/react/-/react-5.9.1.tgz", @@ -25018,6 +25053,12 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, + "@capacitor-community/filesystem-react": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@capacitor-community/filesystem-react/-/filesystem-react-0.1.0.tgz", + "integrity": "sha512-DS35YDBM30+/ly2aYINCGDALbk0fc91y/nj1J4PRY7AcOs+/eONAxw4woXlmgq2JsPMF1TyNmWws5fQp20+ASg==", + "requires": {} + }, "@capacitor/android": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-3.3.2.tgz", @@ -25030,6 +25071,12 @@ "integrity": "sha512-NjHIs6f4WJQuhabnCkcE6YLyIIn+t4Al5etB/SJGZJwUYRe1yJYtZ4T/KobDIzwwZn9I9de7QbEA5947lGttBQ==", "requires": {} }, + "@capacitor/camera": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@capacitor/camera/-/camera-1.2.1.tgz", + "integrity": "sha512-CVT4ITwCK4AeTpNNlYFb0vuqWVD6490PWHqdaPV977clySbspDbrwPeWFKkx8emBnoNHW5urKN7tz737PD7jcg==", + "requires": {} + }, "@capacitor/cli": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-3.3.2.tgz", @@ -25062,6 +25109,13 @@ "tslib": "^2.1.0" } }, + "@capacitor/filesystem": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-1.0.6.tgz", + "integrity": "sha512-8xqUbDZFGBMhgqoBSn9wEd9OBPdHIRegQ9zCCZcpHNf3FFAIby1ck+aDFnoq+Da49xhD6ks1SKCBSxz/26qWTw==", + "peer": true, + "requires": {} + }, "@capacitor/geolocation": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-1.2.0.tgz", @@ -25648,6 +25702,11 @@ "tslib": "^2.1.0" } }, + "@ionic/pwa-elements": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@ionic/pwa-elements/-/pwa-elements-3.0.2.tgz", + "integrity": "sha512-Wwb8/NgzeX1GA8MDPvcis7nx1vqtWdEaDz3t0tTbtUduR/oCNkWZkY2PFL49ET0+/lRhcqE2jZwXvTVn0R1UGw==" + }, "@ionic/react": { "version": "5.9.1", "resolved": "https://registry.npmjs.org/@ionic/react/-/react-5.9.1.tgz", diff --git a/package.json b/package.json index 5937ad9..0adb733 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,16 @@ "eject": "react-scripts eject" }, "dependencies": { + "@capacitor-community/filesystem-react": "^0.1.0", "@capacitor/android": "3.3.2", "@capacitor/app": "1.0.6", + "@capacitor/camera": "^1.2.1", "@capacitor/core": "^3.3.2", "@capacitor/geolocation": "^1.1.3", "@capacitor/haptics": "1.1.3", "@capacitor/keyboard": "1.1.3", "@capacitor/status-bar": "1.0.6", + "@ionic/pwa-elements": "^3.0.2", "@ionic/react": "^5.5.0", "@ionic/react-router": "^5.5.0", "@react-google-maps/api": "^2.5.0", diff --git a/public/assets/images/avatar-placeholder.png b/public/assets/images/avatar-placeholder.png new file mode 100644 index 0000000..0989209 Binary files /dev/null and b/public/assets/images/avatar-placeholder.png differ diff --git a/src/contexts/auth/AuthProvider.tsx b/src/contexts/auth/AuthProvider.tsx index ba3d5a1..c0107e5 100644 --- a/src/contexts/auth/AuthProvider.tsx +++ b/src/contexts/auth/AuthProvider.tsx @@ -28,7 +28,9 @@ export const AuthProvider: React.FC = ({ children }) => { fullName: string, phoneNumber: string, address: string, - gender: 'male' | 'female' + gender: 'male' | 'female', + bio: string, + photoUrl: string, ) => { try { await registerUser( @@ -37,7 +39,9 @@ export const AuthProvider: React.FC = ({ children }) => { fullName, phoneNumber, address, - gender + gender, + bio, + photoUrl, ); } catch (err) { console.error(err); diff --git a/src/contexts/auth/auth.context.ts b/src/contexts/auth/auth.context.ts index c58b91c..c609cc4 100644 --- a/src/contexts/auth/auth.context.ts +++ b/src/contexts/auth/auth.context.ts @@ -1,6 +1,5 @@ import { createContext } from 'react'; import { User } from 'firebase/auth'; -import { UserData } from 'types/userData'; interface Context { currentUser: User | null; @@ -10,7 +9,9 @@ interface Context { fullName: string, phoneNumber: string, address: string, - gender: 'male' | 'female' + gender: 'male' | 'female', + bio: string, + photoUrl: string, ) => void; login: (email: string, password: string) => void; logout: () => void; @@ -18,7 +19,7 @@ interface Context { export const AuthContext = createContext({ currentUser: null, - register: () => {}, - login: () => {}, - logout: () => {}, + register: () => { }, + login: () => { }, + logout: () => { }, }); diff --git a/src/contexts/userData/UserDataProvider.tsx b/src/contexts/userData/UserDataProvider.tsx new file mode 100644 index 0000000..74282f8 --- /dev/null +++ b/src/contexts/userData/UserDataProvider.tsx @@ -0,0 +1,41 @@ +import { useState } from 'react'; +import { User } from 'firebase/auth'; +import { UserDataContext } from './userData.context'; +import { UserData } from 'types/userData'; +import { getUserData } from 'services/firebase'; + +const initialData: UserData = { + id: '1', + fullName: 'John Doe', + gender: 'male', + email: 'example@domain.com', + phoneNumber: '12345', + address: 'USA', + bio: '', + photoUrl: '', +}; + +export const UserDataProvider: React.FC = ({ children }) => { + const [userData, setUserData] = useState(initialData); + + const fetchUserData = async (currentUser: User | null) => { + try { + const data = await getUserData(currentUser); + + if (!data) return; + + setUserData(data); + } catch (error) { + console.log(error); + throw new Error('Oops! Something went wrong.'); + } + }; + + return ( + + {children} + + ); +}; diff --git a/src/contexts/userData/index.tsx b/src/contexts/userData/index.tsx new file mode 100644 index 0000000..670e993 --- /dev/null +++ b/src/contexts/userData/index.tsx @@ -0,0 +1,2 @@ +export * from './UserDataProvider'; +export * from './userData.context'; diff --git a/src/contexts/userData/userData.context.ts b/src/contexts/userData/userData.context.ts new file mode 100644 index 0000000..6d503a4 --- /dev/null +++ b/src/contexts/userData/userData.context.ts @@ -0,0 +1,24 @@ +import { createContext } from 'react'; +import { User } from 'firebase/auth'; +import { UserData } from 'types/userData'; + +const initialData: UserData = { + id: '1', + fullName: 'John Doe', + gender: 'male', + email: 'example@domain.com', + phoneNumber: '12345', + address: 'USA', + bio: '', + photoUrl: '', +}; + +interface Context { + userData: UserData; + fetchUserData: (currentUser: User | null) => void; +} + +export const UserDataContext = createContext({ + userData: initialData, + fetchUserData: () => { }, +}); \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index facbf57..ac0d0e3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,20 +1,26 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { defineCustomElements } from '@ionic/pwa-elements/loader'; + import App from './App'; import * as serviceWorkerRegistration from './serviceWorkerRegistration'; import reportWebVitals from './reportWebVitals'; + import { AuthProvider } from 'contexts/auth'; +import { UserDataProvider } from 'contexts/userData'; import { PersonalContactProvider } from 'contexts/personalContact'; import { EmergencyServiceProvider } from 'contexts/emergencyService'; ReactDOM.render( - - - - - + + + + + + + , document.getElementById('root') @@ -29,3 +35,6 @@ serviceWorkerRegistration.register(); // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); + +// Call the element loader after the app has been rendered the first time +defineCustomElements(window); \ No newline at end of file diff --git a/src/pages/auth/Register.tsx b/src/pages/auth/Register.tsx index 4ecfbd8..efb334b 100644 --- a/src/pages/auth/Register.tsx +++ b/src/pages/auth/Register.tsx @@ -26,7 +26,6 @@ import { lockClosedOutline, callOutline, transgenderOutline, - homeOutline, } from 'ionicons/icons'; import { AuthContext } from 'contexts/auth'; @@ -43,7 +42,6 @@ const Register: React.FC = () => { const fullNameRef = useRef(null); const emailRef = useRef(null); const phoneNumberRef = useRef(null); - const addressRef = useRef(null); const passwordRef = useRef(null); const confirmPasswordRef = useRef(null); @@ -61,7 +59,9 @@ const Register: React.FC = () => { const fullName = fullNameRef.current?.value; const email = emailRef.current?.value; const phoneNumber = phoneNumberRef.current?.value; - const address = addressRef.current?.value; + const address = ""; + const bio = ""; + const photoUrl = ""; const password = passwordRef.current?.value; const confirmPassword = confirmPasswordRef.current?.value; @@ -89,14 +89,6 @@ const Register: React.FC = () => { }); } - if (!address || address.toString().trim().length === 0) { - return presentToast({ - message: 'Alamat wajib diisi.', - duration: 2000, - color: 'warning', - }); - } - if (!selectedGender) { return presentToast({ message: 'Jenis kelamin wajib diisi.', @@ -140,8 +132,10 @@ const Register: React.FC = () => { password.toString(), fullName.toString().trim(), phoneNumber.toString().trim(), - address.toString().trim(), - selectedGender + address, + selectedGender, + bio, + photoUrl, ); presentToast({ @@ -232,20 +226,6 @@ const Register: React.FC = () => { required /> - - - - { - const [userData, setUserData] = useState(initialData); - const [gender, setGender] = useState<'male' | 'female'>('male'); + const [gender, setGender] = useState<'male' | 'female'>(); + const [photo, setPhoto] = useState(''); const [presentLoading, dismissLoading] = useIonLoading(); const [presentToast] = useIonToast(); @@ -45,34 +38,55 @@ const EditProfile: React.FC = () => { const emailRef = useRef(null); const phoneNumberRef = useRef(null); const addressRef = useRef(null); + const bioRef = useRef(null); const { currentUser } = useContext(AuthContext); + const { userData, fetchUserData } = useContext(UserDataContext); const history = useHistory(); useEffect(() => { - const fetchUserData = async () => { + const getUserData = async () => { presentLoading({ spinner: 'bubbles', cssClass: 'loading' }); - try { - const data = await getUserData(currentUser); - if (!data) return; - - setUserData(data); + try { + await fetchUserData(currentUser); } catch (error) { - console.log(error); + console.error(error); } + dismissLoading(); }; - fetchUserData(); + getUserData(); + console.log('hihih'); }, [currentUser, presentLoading, dismissLoading]); + const handleAddPhoto = async () => { + try { + const photo = await Camera.getPhoto({ + quality: 80, + allowEditing: false, + resultType: CameraResultType.Uri, + }); + + if (!photo || !photo.webPath) { + return; + } + + setPhoto(photo.webPath); + } catch (error) { + console.error(error); + setPhoto(''); + } + }; + const handleEditUserData = async () => { const fullName = fullNameRef.current?.value as string; const email = emailRef.current?.value as string; const phoneNumber = phoneNumberRef.current?.value as string; const address = addressRef.current?.value as string; + const bio = bioRef.current?.value as string; const updatedUser = { fullName: fullName ?? userData.fullName, @@ -80,10 +94,14 @@ const EditProfile: React.FC = () => { email: email ?? userData.email, phoneNumber: phoneNumber ?? userData.phoneNumber, address: address ?? userData.address, + bio: bio ?? userData.bio, + photoUrl: userData.photoUrl, }; try { - await updateUserData(currentUser, updatedUser); + const base64 = await base64FromPath(photo!); + + await updateUserData(currentUser, updatedUser, base64, photo); presentToast({ message: 'Profil berhasil di ubah.', @@ -108,10 +126,14 @@ const EditProfile: React.FC = () => { - avatar + {photo ? ( + avatar + ) : ( + avatar + )} - - + + Pilih Foto @@ -126,7 +148,9 @@ const EditProfile: React.FC = () => { userData.fullName = e.detail.value} value={userData.fullName} + placeholder="Nama Lengkap" inputMode="text" clearInput /> @@ -136,7 +160,13 @@ const EditProfile: React.FC = () => { Bio - + userData.bio = e.detail.value} + value={userData.bio} + placeholder="Bio" + inputMode="text" + clearInput /> @@ -144,7 +174,7 @@ const EditProfile: React.FC = () => { Gender setGender(e.detail.value)} > Laki-Laki @@ -159,8 +189,10 @@ const EditProfile: React.FC = () => { @@ -169,7 +201,10 @@ const EditProfile: React.FC = () => { No. Telp userData.phoneNumber = e.detail.value} value={userData.phoneNumber} + placeholder="Nomor Telepon" inputMode="tel" maxlength={12} clearInput @@ -182,7 +217,9 @@ const EditProfile: React.FC = () => { userData.address = e.detail.value} value={userData.address} + placeholder="Alamat" inputMode="text" clearInput /> diff --git a/src/pages/main/profile/Profile.tsx b/src/pages/main/profile/Profile.tsx index 2cecca2..d55d5bc 100644 --- a/src/pages/main/profile/Profile.tsx +++ b/src/pages/main/profile/Profile.tsx @@ -23,44 +23,33 @@ import { } from 'ionicons/icons'; import { AuthContext } from 'contexts/auth'; -import { getUserData } from 'services/firebase'; -import { UserData } from 'types/userData'; +import { UserDataContext } from 'contexts/userData'; import Layout from 'components/layout'; import styles from 'styles/main/profile/Profile.module.scss'; -const initialData: UserData = { - id: '1', - fullName: 'John Doe', - gender: 'male', - email: 'example@domain.com', - phoneNumber: '12345', - address: 'USA', -}; - const Profile: React.FC = () => { - const [userData, setUserData] = useState(initialData); const { currentUser, logout } = useContext(AuthContext); + const { userData, fetchUserData } = useContext(UserDataContext); const [presentToast] = useIonToast(); const [presentLoading, dismissLoading] = useIonLoading(); const history = useHistory(); useEffect(() => { - const fetchUserData = async () => { + const getUserData = async () => { presentLoading({ spinner: 'bubbles', cssClass: 'loading' }); - try { - const data = await getUserData(currentUser); - if (!data) return; - - setUserData(data); + try { + await fetchUserData(currentUser); } catch (error) { - console.log(error); + console.error(error); } + dismissLoading(); }; - fetchUserData(); + getUserData(); + console.log('hihih'); }, [currentUser, presentLoading, dismissLoading]); const handleLogout = async () => { @@ -87,13 +76,13 @@ const Profile: React.FC = () => {
- avatar + avatar

{userData.fullName}

-

Hello World

+

{(userData.bio) ? userData.bio : 'Bio'}

@@ -134,7 +123,7 @@ const Profile: React.FC = () => { - {userData.address} + {(userData.address) ? userData.address : 'Alamat'} diff --git a/src/services/firebase.ts b/src/services/firebase.ts index 0ec210c..aa54f0f 100644 --- a/src/services/firebase.ts +++ b/src/services/firebase.ts @@ -40,7 +40,9 @@ const registerUser = async ( fullName: string, phoneNumber: string, address: string, - gender: 'male' | 'female' + gender: 'male' | 'female', + bio: string, + photoUrl: string, ) => { try { const { user } = await createUserWithEmailAndPassword( @@ -56,6 +58,8 @@ const registerUser = async ( phoneNumber, address, gender, + bio, + photoUrl, }); await setDoc( @@ -127,11 +131,27 @@ const updateUserData = async ( email: string; phoneNumber: string; address: string; - } + bio: string; + photoUrl: string; + }, + base64: string, + photo: string, ) => { - if (!currentUser) return; + if (!currentUser) throw new Error('Current user is null.'); + + const photoName = currentUser.uid + '.jpeg'; + const storageRef = ref(storage, `assets/profile-pictures/${photoName}`); try { + if (photo) { + const photoBlob = await (await fetch(base64)).blob(); + + await uploadBytes(storageRef, photoBlob); + + const photoUrl = await getDownloadURL(storageRef); + updatedUser.photoUrl = photoUrl; + } + const usersDocRef = doc(firestore, 'users', currentUser.uid); await updateDoc(usersDocRef, updatedUser); } catch (err) { diff --git a/src/types/userData.ts b/src/types/userData.ts index bcb0eb0..f35b63d 100644 --- a/src/types/userData.ts +++ b/src/types/userData.ts @@ -5,4 +5,6 @@ export interface UserData { phoneNumber: string; address: string; gender: 'male' | 'female'; + bio: string; + photoUrl: string; }; \ No newline at end of file