diff --git a/apps/expo/package.json b/apps/expo/package.json
index 8e46c3b..8c40081 100644
--- a/apps/expo/package.json
+++ b/apps/expo/package.json
@@ -19,6 +19,7 @@
"@expo/metro-config": "^0.10.7",
"@expo/vector-icons": "^13.0.0",
"@gorhom/bottom-sheet": "^4",
+ "@hookform/resolvers": "^3.3.0",
"@shopify/flash-list": "1.4.3",
"@tanstack/react-query": "^5.0.5",
"@trpc/client": "next",
@@ -35,14 +36,15 @@
"nativewind": "^2.0.11",
"react": "18.2.0",
"react-dom": "18.2.0",
+ "react-hook-form": "^7.47.0",
"react-native": "0.72.6",
"react-native-gesture-handler": "~2.12.1",
- "react-native-parallax-scroll-view": "^0.21.3",
"react-native-reanimated": "^3.3.0",
"react-native-safe-area-context": "4.6.3",
"react-native-screens": "~3.22.1",
"react-native-ui-lib": "^7.9.1",
"superjson": "1.13.1",
+ "zod": "^3.21.4",
"zustand": "^4.4.6"
},
"devDependencies": {
@@ -70,4 +72,4 @@
]
},
"prettier": "@vivat/prettier-config"
-}
+}
\ No newline at end of file
diff --git a/apps/expo/src/app/(app)/[productId].tsx b/apps/expo/src/app/(app)/[productId].tsx
index c80f502..324ce53 100644
--- a/apps/expo/src/app/(app)/[productId].tsx
+++ b/apps/expo/src/app/(app)/[productId].tsx
@@ -1,6 +1,6 @@
import React from "react";
import { ActivityIndicator } from "react-native";
-import ParallaxScrollView from "react-native-parallax-scroll-view";
+import { RefreshControl, ScrollView } from "react-native-gesture-handler";
import {
AnimatedImage,
Avatar,
@@ -10,7 +10,7 @@ import {
Text,
View,
} from "react-native-ui-lib";
-import { usePathname } from "expo-router";
+import { Link, usePathname } from "expo-router";
import { api } from "@/utils/api";
import { colors } from "@/utils/constant";
import rupiahFormatter from "@/utils/rupiahFormatter";
@@ -18,123 +18,105 @@ import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
export default function ProductDetailScreen() {
const pathname = usePathname();
- const { data } = api.product.showProduct.useQuery({
+ const { data, isLoading, refetch } = api.product.showProduct.useQuery({
id: pathname.slice(1),
});
return (
-
- (
- //
- //
- //
- // {data?.name}
- //
- //
- // {data?.category}
- //
- //
- //
- //
- // {rupiahFormatter(data?.price)}
- //
- //
- // Stok: {data?.stock}
- //
- //
- //
- // )}
- renderBackground={() => (
- }
- />
- )}
- >
-
-
-
-
- {data?.name}
-
-
- {data?.category}
-
-
-
-
- {rupiahFormatter(data?.price)}
-
-
- Stok: {data?.stock}
-
-
+ refetch()} />
+ }
+ >
+ }
+ />
+
+
+
+
+ {data?.name}
+
+
+ {data?.category.name}
+
-
-
-
-
-
- {data?.user.name}
- {data?.user.major}
-
-
-
-
+
+
+
+
+
+ {data?.user.name}
+ {data?.user.major}
-
- {data?.description}
-
+
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+
+
+ {data?.description}
+
+
+
+ {/* */}
+
+
+
-
-
+
+
);
}
diff --git a/apps/expo/src/app/(app)/_layout.tsx b/apps/expo/src/app/(app)/_layout.tsx
index 2362916..ddd6f71 100644
--- a/apps/expo/src/app/(app)/_layout.tsx
+++ b/apps/expo/src/app/(app)/_layout.tsx
@@ -1,23 +1,72 @@
import React from "react";
-import { Stack } from "expo-router";
+import { TouchableOpacity } from "react-native-gesture-handler";
+import { Link, Stack } from "expo-router";
import Header from "@/components/Header";
import { colors } from "@/utils/constant";
+import { Ionicons } from "@expo/vector-icons";
export default function AuthLayout() {
return (
,
contentStyle: {
flex: 1,
- backgroundColor: colors.primary,
},
}}
>
+ ,
+ contentStyle: {
+ backgroundColor: colors.primary,
+ },
+ }}
+ />
+ ,
+ contentStyle: {
+ backgroundColor: colors.primary,
+ },
+ }}
+ />
,
+ contentStyle: {
+ backgroundColor: colors.primary,
+ },
+ }}
+ />
+
+ (
+
+
+
+
+
+ ),
+ }}
+ />
+
diff --git a/apps/expo/src/app/(app)/address/create.tsx b/apps/expo/src/app/(app)/address/create.tsx
new file mode 100644
index 0000000..1f4fd89
--- /dev/null
+++ b/apps/expo/src/app/(app)/address/create.tsx
@@ -0,0 +1,63 @@
+import { Button, LoaderScreen, View } from "react-native-ui-lib";
+import { useRouter } from "expo-router";
+import Input from "@/components/forms/Input";
+import { api } from "@/utils/api";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { FormProvider, useForm } from "react-hook-form";
+import { z } from "zod";
+
+const schema = z.object({
+ title: z.string(),
+ address: z.string(),
+ zipCode: z.string().length(5),
+ recipient: z.string(),
+ phoneNumber: z.string(),
+});
+
+export default function CreateAddressScreen() {
+ const router = useRouter();
+ const { mutate, isPending } = api.user.createAddress.useMutation();
+
+ const methods = useForm>({
+ resolver: zodResolver(schema),
+ });
+ const { handleSubmit } = methods;
+ const onSubmit = handleSubmit((data) => {
+ mutate(data, {
+ onSuccess: () => {
+ void api.useUtils().user.getAddresses.invalidate();
+ router.back();
+ },
+ });
+ });
+
+ if (isPending) return ;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/expo/src/app/(app)/address/index.tsx b/apps/expo/src/app/(app)/address/index.tsx
new file mode 100644
index 0000000..82bef29
--- /dev/null
+++ b/apps/expo/src/app/(app)/address/index.tsx
@@ -0,0 +1,66 @@
+import React, { useRef } from "react";
+import { Swipeable } from "react-native-gesture-handler";
+import { Button, Spacings, Text, View } from "react-native-ui-lib";
+import { api } from "@/utils/api";
+
+export default function AddressScreen() {
+ const ref = useRef(null);
+
+ const { data } = api.user.getAddresses.useQuery();
+ const { mutate } = api.user.setDefaultAddress.useMutation();
+
+ const setDefaultAddress = (id: string) => {
+ console.log(id);
+ mutate(
+ { id },
+ {
+ onSuccess: () => {
+ void api.useUtils().user.invalidate();
+ ref.current?.close();
+ },
+ },
+ );
+ };
+
+ return (
+
+ {data?.map((address) => (
+ (
+ setDefaultAddress(address.id)}
+ />
+ )}
+ childrenContainerStyle={{ padding: Spacings.s4 }}
+ >
+
+
+
+ {address.title}
+
+ {address.default && (
+
+ (Alamat Utama)
+
+ )}
+
+ {address.recipient}
+ {address.phoneNumber}
+
+ {address.address}
+
+
+
+ ))}
+
+ );
+}
diff --git a/apps/expo/src/app/(app)/checkout.tsx b/apps/expo/src/app/(app)/checkout.tsx
new file mode 100644
index 0000000..08d4baa
--- /dev/null
+++ b/apps/expo/src/app/(app)/checkout.tsx
@@ -0,0 +1,28 @@
+import React from "react";
+import { Text, TouchableOpacity, View } from "react-native-ui-lib";
+import { Link } from "expo-router";
+import { api } from "@/utils/api";
+import { colors } from "@/utils/constant";
+import { Ionicons } from "@expo/vector-icons";
+
+export default function CheckoutScreen() {
+ const { data } = api.user.getDefaultAddress.useQuery();
+
+ return (
+
+
+
+
+ Alamat
+
+
+
+ {data?.title ?? "Select address"}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/expo/src/components/forms/Input.tsx b/apps/expo/src/components/forms/Input.tsx
new file mode 100644
index 0000000..b1b1f8e
--- /dev/null
+++ b/apps/expo/src/components/forms/Input.tsx
@@ -0,0 +1,51 @@
+import type { TextFieldProps } from "react-native-ui-lib";
+import { BorderRadiuses, Spacings, TextField } from "react-native-ui-lib";
+import { colors } from "@/utils/constant";
+import { Controller, get, useFormContext } from "react-hook-form";
+
+type InputProps = {
+ /** Input label */
+ label: string;
+ /**
+ * id to be initialized with React Hook Form,
+ * must be the same with the pre-defined types.
+ */
+ id: string;
+} & TextFieldProps;
+
+export default function Input({ id, label, ...props }: InputProps) {
+ const {
+ formState: { errors },
+ control,
+ } = useFormContext();
+ const error = get(errors, id);
+
+ return (
+ (
+
+ )}
+ />
+ );
+}
diff --git a/apps/expo/src/utils/api.tsx b/apps/expo/src/utils/api.tsx
index 44a31b9..978a76d 100644
--- a/apps/expo/src/utils/api.tsx
+++ b/apps/expo/src/utils/api.tsx
@@ -6,14 +6,8 @@ import { httpBatchLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import superjson from "superjson";
-
-
import type { AppRouter } from "@vivat/api";
-
-
-
-
/**
* A set of typesafe hooks for consuming your API.
*/
@@ -37,14 +31,11 @@ const getBaseUrl = () => {
const debuggerHost = Constants.expoConfig?.hostUri;
const localhost = debuggerHost?.split(":")[0];
- if (!localhost) {
- if (process.env.EXPO_PUBLIC_API_URL) {
- console.log(
- "Failed to get localhost. Falling back to EXPO_PUBLIC_API_URL.",
- );
- return process.env.EXPO_PUBLIC_API_URL;
- }
+ if (process.env.EXPO_PUBLIC_API_URL) {
+ return process.env.EXPO_PUBLIC_API_URL;
+ }
+ if (!localhost) {
throw new Error(
"Failed to get localhost. Please point to your production server.",
);
@@ -87,4 +78,4 @@ export function TRPCProvider(props: { children: React.ReactNode }) {
);
-}
\ No newline at end of file
+}
diff --git a/apps/expo/tailwind.config.ts b/apps/expo/tailwind.config.ts
index d452c1f..76bc0a5 100644
--- a/apps/expo/tailwind.config.ts
+++ b/apps/expo/tailwind.config.ts
@@ -1,16 +1,17 @@
import type { Config } from "tailwindcss";
import baseConfig from "@vivat/tailwind-config";
+import { colors } from "@/utils/constant";
export default {
content: ["./src/**/*.{ts,tsx}"],
presets: [baseConfig],
theme: {
extend: {
- // colors: {
- // primary: colors.primary,
- // secondary: colors.secondary,
- // },
+ colors: {
+ primary: colors.primary,
+ secondary: colors.secondary,
+ },
},
},
} satisfies Config;
diff --git a/packages/db/schema/addresses.ts b/packages/db/schema/addresses.ts
index be9455c..d5b08b8 100644
--- a/packages/db/schema/addresses.ts
+++ b/packages/db/schema/addresses.ts
@@ -1,30 +1,34 @@
import { relations, sql } from "drizzle-orm";
-import { boolean, varchar } from "drizzle-orm/mysql-core";
+import { boolean, char, varchar } from "drizzle-orm/mysql-core";
+import { createInsertSchema } from "drizzle-zod";
import { mySqlTable } from "./_table";
import { users } from "./users";
-import { createInsertSchema } from "drizzle-zod";
export const addresses = mySqlTable("addresses", {
- id: varchar("id", { length: 36 })
- .notNull()
- .primaryKey()
- .default(sql`(UUID())`),
- title: varchar("title", { length: 255 }).notNull(),
- address: varchar("address", { length: 255 }).notNull(),
- zipCode: varchar("zip_code", { length: 255 }).notNull(),
- recipient: varchar("recipient", { length: 255 }).notNull(),
- phoneNumber: varchar("phone_number", { length: 255 }).notNull(),
- userId: varchar("user_id", { length: 255 }).notNull(),
- default: boolean("default").notNull().default(false),
+ id: varchar("id", { length: 36 })
+ .notNull()
+ .primaryKey()
+ .default(sql`(UUID())`),
+ title: varchar("title", { length: 255 }).notNull(),
+ address: varchar("address", { length: 255 }).notNull(),
+ zipCode: char("zip_code", { length: 5 }).notNull(),
+ recipient: varchar("recipient", { length: 255 }).notNull(),
+ phoneNumber: varchar("phone_number", { length: 255 }).notNull(),
+ userId: varchar("user_id", { length: 255 }).notNull(),
+ default: boolean("default").notNull().default(false),
});
export const addressesRelations = relations(addresses, ({ one }) => ({
- user: one(users, {
- fields: [addresses.userId],
- references: [users.id],
- }),
+ user: one(users, {
+ fields: [addresses.userId],
+ references: [users.id],
+ }),
}));
-export const insertAddressSchema = createInsertSchema(addresses);
-export const addressIdSchema = insertAddressSchema.pick({ id: true }).required();
\ No newline at end of file
+export const insertAddressSchema = createInsertSchema(addresses).omit({
+ userId: true,
+});
+export const addressIdSchema = insertAddressSchema
+ .pick({ id: true })
+ .required();
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a6f7014..de35b2b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -41,6 +41,9 @@ importers:
'@gorhom/bottom-sheet':
specifier: ^4
version: 4.5.1(@types/react@18.2.33)(react-native-gesture-handler@2.12.1)(react-native-reanimated@3.3.0)(react-native@0.72.6)(react@18.2.0)
+ '@hookform/resolvers':
+ specifier: ^3.3.0
+ version: 3.3.2(react-hook-form@7.48.2)
'@shopify/flash-list':
specifier: 1.4.3
version: 1.4.3(@babel/runtime@7.23.2)(react-native@0.72.6)(react@18.2.0)
@@ -89,15 +92,15 @@ importers:
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
+ react-hook-form:
+ specifier: ^7.47.0
+ version: 7.48.2(react@18.2.0)
react-native:
specifier: 0.72.6
version: 0.72.6(@babel/core@7.23.2)(@babel/preset-env@7.23.2)(react@18.2.0)
react-native-gesture-handler:
specifier: ~2.12.1
version: 2.12.1(react-native@0.72.6)(react@18.2.0)
- react-native-parallax-scroll-view:
- specifier: ^0.21.3
- version: 0.21.3
react-native-reanimated:
specifier: ^3.3.0
version: 3.3.0(@babel/core@7.23.2)(@babel/plugin-proposal-nullish-coalescing-operator@7.18.6)(@babel/plugin-proposal-optional-chaining@7.21.0)(@babel/plugin-transform-arrow-functions@7.22.5)(@babel/plugin-transform-shorthand-properties@7.22.5)(@babel/plugin-transform-template-literals@7.22.5)(react-native@0.72.6)(react@18.2.0)
@@ -113,6 +116,9 @@ importers:
superjson:
specifier: 1.13.1
version: 1.13.1
+ zod:
+ specifier: ^3.21.4
+ version: 3.22.4
zustand:
specifier: ^4.4.6
version: 4.4.6(@types/react@18.2.33)(react@18.2.0)
@@ -11020,12 +11026,6 @@ packages:
react-native: 0.72.6(@babel/core@7.23.2)(@babel/preset-env@7.23.2)(react@18.2.0)
dev: false
- /react-native-parallax-scroll-view@0.21.3:
- resolution: {integrity: sha512-oVbZPijNGYGcpU2z7k/ZOOnl0vYxch7qtVvaJpH4+wLVnEP49NC2diCzjFGz8PE0yEcgyeTdkpdUK8RTD3QEew==}
- dependencies:
- prop-types: 15.8.1
- dev: false
-
/react-native-reanimated@3.3.0(@babel/core@7.23.2)(@babel/plugin-proposal-nullish-coalescing-operator@7.18.6)(@babel/plugin-proposal-optional-chaining@7.21.0)(@babel/plugin-transform-arrow-functions@7.22.5)(@babel/plugin-transform-shorthand-properties@7.22.5)(@babel/plugin-transform-template-literals@7.22.5)(react-native@0.72.6)(react@18.2.0):
resolution: {integrity: sha512-LzfpPZ1qXBGy5BcUHqw3pBC0qSd22qXS3t8hWSbozXNrBkzMhhOrcILE/nEg/PHpNNp1xvGOW8NwpAMF006roQ==}
peerDependencies: