Skip to content

Commit

Permalink
feat(expo/listing): image upload loading
Browse files Browse the repository at this point in the history
  • Loading branch information
mrevanzak committed Nov 25, 2023
1 parent b2b2881 commit 1f9e201
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 37 deletions.
5 changes: 3 additions & 2 deletions apps/expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "index.tsx",
"scripts": {
"clean": "git clean -xdf .expo .turbo node_modules",
"dev": "expo start --android",
"dev": "expo start",
"dev:android": "expo start --android",
"dev:ios": "expo start --ios",
"lint": "eslint .",
Expand All @@ -26,7 +26,6 @@
"@trpc/client": "next",
"@trpc/react-query": "next",
"@trpc/server": "next",
"base64-arraybuffer": "^1.0.2",
"expo": "^49.0.13",
"expo-auth-session": "~5.0.2",
"expo-clipboard": "~4.3.1",
Expand All @@ -50,6 +49,8 @@
"react-native-screens": "~3.22.1",
"react-native-ui-lib": "^7.9.1",
"superjson": "1.13.1",
"tus-js-client": "^3.1.1",
"use-tus": "^0.7.3",
"zod": "^3.21.4",
"zustand": "^4.4.6"
},
Expand Down
76 changes: 41 additions & 35 deletions apps/expo/src/app/(app)/(tabs)/listing.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { ActivityIndicator, Alert } from "react-native";
import {
AnimatedImage,
AnimatedScanner,
BorderRadiuses,
Button,
KeyboardAwareScrollView,
Text,
View,
} from "react-native-ui-lib";
import Constants from "expo-constants";
import * as FileSystem from "expo-file-system";
import * as ImagePicker from "expo-image-picker";
import { useRouter } from "expo-router";
import Input from "@/components/forms/Input";
import Picker from "@/components/forms/Picker";
import { api } from "@/utils/api";
import colors from "@/utils/colors";
import { storageClient } from "@/utils/supabase";
import { storageClient, uploadOptions } from "@/utils/supabase";
import { useUser } from "@clerk/clerk-expo";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { zodResolver } from "@hookform/resolvers/zod";
import { decode } from "base64-arraybuffer";
import { FormProvider, useForm } from "react-hook-form";
import { useTus } from "use-tus";
import { z } from "zod";

const schema = z.object({
Expand All @@ -34,71 +34,76 @@ const schema = z.object({

export default function UploadProductScreen() {
const router = useRouter();
const [image, setImage] = useState<string | undefined>();
const [uploading, setUploading] = useState(false);
const [image, setImage] = useState<ImagePicker.ImagePickerResult>();
const [uploadProggres, setUploadProgress] = useState(0);
const [isUploading, setIsUploading] = useState(false);
const { user } = useUser();

const { data } = api.category.getCategories.useQuery({ partial: true });
const { mutate } = api.product.addProduct.useMutation({});
const { mutate } = api.product.addProduct.useMutation();
const { setUpload } = useTus({ autoStart: true });

const methods = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
});
const { handleSubmit, reset } = methods;
const onSubmit = handleSubmit(async (data) => {
setUploading(true);
setIsUploading(true);
const filePath = `${user?.id}/${data.name}.png`;

await onUpload(filePath);
const imgUrl = storageClient.from("products").getPublicUrl(filePath, {
transform: {
quality: 50,
},
});
const { error } = await onUpload(filePath);
if (error) {
Alert.alert("Gagal mengupload gambar");
return;
}

const imgUrl = storageClient.from("products").getPublicUrl(filePath);
mutate(
{
...data,
image: imgUrl.data.publicUrl,
},
{
onSuccess: () => {
setUploading(false);
reset();
setImage(undefined);
setIsUploading(false);
router.push("/home");
},
},
);
});

const onUpload = async (filePath: string) => {
if (!image) return;
const base64 = await FileSystem.readAsStringAsync(image, {
encoding: "base64",
});

const { error } = await storageClient
.from("products")
.upload(filePath, decode(base64), {
contentType: "image/png",
const onUpload = useCallback(
(filePath: string) => {
return new Promise<{ error?: Error }>((resolve) => {
setUpload(image, {
...uploadOptions("products", filePath),
onProgress(bytesSent, bytesTotal) {
setUploadProgress((bytesSent / bytesTotal) * 100);
},
onSuccess() {
resolve({ error: undefined });
},
onError(error) {
setIsUploading(false);
resolve({ error });
},
});
});
if (error) {
console.log(error);
return;
}
};
},
[image, setUpload],
);

const onSelectImage = async () => {
const options: ImagePicker.ImagePickerOptions = {
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
quality: 1,
};

const result = await ImagePicker.launchImageLibraryAsync(options);
if (!result.canceled) {
setImage(result.assets[0]?.uri);
setImage(result);
}
};

Expand Down Expand Up @@ -133,10 +138,10 @@ export default function UploadProductScreen() {
Upload Gambar
</Text>
<View flex center paddingV-s6 className="space-y-2">
{image ? (
{image?.assets ? (
<>
<AnimatedImage
source={{ uri: image }}
source={{ uri: image.assets[0]?.uri }}
style={{ width: 200, height: 200 }}
loader={
<ActivityIndicator
Expand All @@ -145,6 +150,7 @@ export default function UploadProductScreen() {
/>
}
/>
{isUploading && <AnimatedScanner progress={uploadProggres} />}
<Button
onPress={onSelectImage}
label="Ganti Gambar"
Expand Down Expand Up @@ -212,7 +218,7 @@ export default function UploadProductScreen() {
onPress={onSubmit}
bg-primary
br40
disabled={uploading}
disabled={isUploading}
/>
</KeyboardAwareScrollView>
</View>
Expand Down
22 changes: 22 additions & 0 deletions apps/expo/src/utils/supabase.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { StorageClient } from "@supabase/storage-js";
import type { UploadOptions } from "use-tus";

const STORAGE_URL = process.env.EXPO_PUBLIC_SUPABASE_STORAGE_URL;
const SERVICE_KEY = process.env.EXPO_PUBLIC_SUPABASE_SERVICE_KEY;
Expand All @@ -11,3 +12,24 @@ export const storageClient = new StorageClient(STORAGE_URL, {
apikey: SERVICE_KEY,
Authorization: `Bearer ${SERVICE_KEY}`,
});

export const uploadOptions = (
bucketName: string,
fileName: string,
): UploadOptions => ({
endpoint: `${STORAGE_URL}/upload/resumable`,
retryDelays: [0, 3000, 5000, 10000, 20000],
headers: {
authorization: `Bearer ${SERVICE_KEY}`,
"x-upsert": "true", // optionally set upsert to true to overwrite existing files
},
uploadDataDuringCreation: true,
removeFingerprintOnSuccess: true, // Important if you want to allow re-uploading the same file https://github.com/tus/tus-js-client/blob/main/docs/api.md#removefingerprintonsuccess
metadata: {
bucketName: bucketName,
objectName: fileName,
contentType: "image/png",
cacheControl: "3600",
},
chunkSize: 6 * 1024 * 1024, // NOTE: it must be set to 6MB (for now) do not change it
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dev:web": "turbo --filter @vivat/nextjs dev",
"dev:android": "pnpm --filter expo dev:android",
"dev:ios": "pnpm --filter expo dev:ios",
"dev:expo": "pnpm --filter expo dev",
"format": "turbo format --continue -- --cache --cache-location='node_modules/.cache/.prettiercache'",
"format:fix": "turbo format --continue -- --write --cache --cache-location='node_modules/.cache/.prettiercache'",
"lint": "turbo lint --continue -- --cache --cache-location 'node_modules/.cache/.eslintcache' && manypkg check",
Expand Down
94 changes: 94 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 1f9e201

Please sign in to comment.