Skip to content

Commit

Permalink
feat(expo): checkout screen and functionallity
Browse files Browse the repository at this point in the history
  • Loading branch information
mrevanzak committed Nov 13, 2023
1 parent 894cd83 commit 5b7acc8
Show file tree
Hide file tree
Showing 11 changed files with 280 additions and 36 deletions.
8 changes: 7 additions & 1 deletion apps/expo/src/app/(app)/[productId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,13 @@ export default function ProductDetailScreen() {
className="space-x-4 rounded-b-none"
>
{/* <Button bg-white primary label="Keranjang" br40 flex /> */}
<Link href="/(app)/checkout" asChild>
<Link
href={{
pathname: "/checkout",
params: { productId: data?.id ?? "" },
}}
asChild
>
<Button bg-secondary primary label="Beli" br40 flex />
</Link>
</View>
Expand Down
117 changes: 111 additions & 6 deletions apps/expo/src/app/(app)/checkout.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,133 @@
import React from "react";
import { Text, TouchableOpacity, View } from "react-native-ui-lib";
import { Link } from "expo-router";
import {
Button,
Spacings,
Text,
TouchableOpacity,
View,
} from "react-native-ui-lib";
import { Link, useLocalSearchParams } from "expo-router";
import Input from "@/components/forms/Input";
import RadioButton from "@/components/forms/RadioButton";
import { api } from "@/utils/api";
import colors from "@/utils/colors";
import rupiahFormatter from "@/utils/rupiahFormatter";
import { Ionicons } from "@expo/vector-icons";
import { zodResolver } from "@hookform/resolvers/zod";
import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod";

const schema = z.object({
note: z.string().optional(),
courier: z.string(),
});

const ONGKIR = 40000;

export default function CheckoutScreen() {
const { data } = api.user.getDefaultAddress.useQuery();
const { productId } = useLocalSearchParams();

const { data: product } = api.product.showProduct.useQuery({
id: productId as string,
});
const { data: address } = api.user.getDefaultAddress.useQuery();
const { mutate, isPending } = api.order.checkout.useMutation();

const methods = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
});
const { handleSubmit } = methods;
const onSubmit = handleSubmit((data) => {
if (!address || !product) return;
mutate(
{
note: data.note,
addressId: address.id,
productId: productId as string,
totalPrice: product.price + 10000,
courier: data.courier,
},
{
onSuccess: () => {
console.log("success");
// void utils.user.getAddresses.invalidate();
},
},
);
});

return (
<View bg-white br50 flex padding-s4 className="rounded-b-none">
<Link href="/address/" asChild>
<TouchableOpacity row spread paddingV-s2 className="border-b">
<TouchableOpacity
row
spread
paddingV-s2
paddingH-s4
br40
className="border-primary mb-4 border"
>
<Text text70 primary>
Alamat
</Text>
<View row>
<Text className={data?.title ? "" : "text-gray-400"} text70>
{data?.title ?? "Select address"}
<Text className={address?.title ? "" : "text-red-400"} text70>
{address?.title ?? "Select address"}
</Text>
<Ionicons name="chevron-forward" size={24} color={colors.primary} />
</View>
</TouchableOpacity>
</Link>
<FormProvider {...methods}>
<Input
id="note"
label="Catatan"
multiline
placeholder="Masukan catatan"
/>
<View
paddingV-s2
paddingH-s4
br40
className="border-primary mb-4 border"
>
<Text text80 primary marginB-s1>
Kurir
</Text>
<RadioButton
id="courier"
options={["JNE", "TIKI", "SICEPAT"]}
containerStyle={{
paddingVertical: Spacings.s1,
}}
size={Spacings.s5}
/>
</View>
</FormProvider>
<View paddingV-s2 paddingH-s4 br40 className="border-primary mb-4 border">
<Text text80 primary marginB-s1>
Rincian
</Text>
<View row spread>
<Text text80>Subtotal</Text>
<Text text80>{rupiahFormatter(product?.price)}</Text>
</View>
<View row spread>
<Text text80>Ongkos Kirim</Text>
<Text text80>{rupiahFormatter(ONGKIR)}</Text>
</View>
<View row spread>
<Text text80>Total</Text>
<Text text80>{rupiahFormatter(product?.price ?? 0 + ONGKIR)}</Text>
</View>
</View>
<Button
label="Beli"
onPress={onSubmit}
bg-primary
br40
disabled={isPending}
/>
</View>
);
}
57 changes: 57 additions & 0 deletions apps/expo/src/components/forms/RadioButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { RadioButtonProps as UIRadioButtonProps } from "react-native-ui-lib";
import { Text, RadioButton as UIRadioButton } from "react-native-ui-lib";
import colors from "@/utils/colors";
import { Controller, get, useFormContext } from "react-hook-form";

type RadioButtonProps = {
/**
* id to be initialized with React Hook Form,
* must be the same with the pre-defined types.
*/
id: string;

/**
* value to be passed to the onChange function
* when the button is pressed.
*/
options: string[];
} & UIRadioButtonProps;

export default function RadioButton({
id,
options,
...props
}: RadioButtonProps) {
const {
control,
setValue,
formState: { errors },
} = useFormContext();
const error = get(errors, id) as Error;

return (
<Controller
control={control}
name={id}
render={({ field }) => (
<>
{error && (
<Text red40 text80 marginB-s2>
{error.message}
</Text>
)}
{options.map((option, index) => (
<UIRadioButton
label={option}
key={index}
color={colors.primary}
selected={field.value === option}
onPress={() => setValue(id, option)}
{...props}
/>
))}
</>
)}
/>
);
}
5 changes: 3 additions & 2 deletions packages/api/src/root.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { authRouter } from "./router/auth";
import { categoryRouter } from "./router/category";
import { orderRouter } from "./router/orders";
import { productRouter } from "./router/product";
import { userRouter } from "./router/user";
import { createTRPCRouter } from "./trpc";


export const appRouter = createTRPCRouter({
auth: authRouter,
product: productRouter,
category: categoryRouter,
user: userRouter,
order: orderRouter,
});

// export type definition of API
export type AppRouter = typeof appRouter;
export type AppRouter = typeof appRouter;
11 changes: 11 additions & 0 deletions packages/api/src/router/orders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { insertOrderParams, orders } from "@vivat/db/schema/orders";

import { createTRPCRouter, protectedProcedure } from "../trpc";

export const orderRouter = createTRPCRouter({
checkout: protectedProcedure
.input(insertOrderParams)
.mutation(async ({ input, ctx }) => {
return await ctx.db.insert(orders).values(input);
}),
});
13 changes: 10 additions & 3 deletions packages/db/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { Client } from "@planetscale/database";
import { drizzle } from "drizzle-orm/planetscale-serverless";

import * as addresses from "./schema/addresses";
import * as categories from "./schema/categories";
import * as orders from "./schema/orders";
import * as products from "./schema/products";
import * as users from "./schema/users";
import * as categories from "./schema/categories";
import * as addresses from "./schema/addresses";

export const schema = { ...users, ...products, ...categories, ...addresses };
export const schema = {
...users,
...products,
...categories,
...addresses,
...orders,
};

export { mySqlTable as tableCreator } from "./schema/_table";

Expand Down
6 changes: 5 additions & 1 deletion packages/db/schema/addresses.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { relations, sql } from "drizzle-orm";
import { boolean, char, varchar } from "drizzle-orm/mysql-core";
import { boolean, char, timestamp, varchar } from "drizzle-orm/mysql-core";
import { createInsertSchema } from "drizzle-zod";

import { mySqlTable } from "./_table";
Expand All @@ -10,6 +10,10 @@ export const addresses = mySqlTable("addresses", {
.notNull()
.primaryKey()
.default(sql`(UUID())`),
createdAt: timestamp("created_at")
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp("updated_at").onUpdateNow(),
title: varchar("title", { length: 255 }).notNull(),
address: varchar("address", { length: 255 }).notNull(),
zipCode: char("zip_code", { length: 5 }).notNull(),
Expand Down
6 changes: 5 additions & 1 deletion packages/db/schema/categories.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { relations, sql } from "drizzle-orm";
import { varchar } from "drizzle-orm/mysql-core";
import { timestamp, varchar } from "drizzle-orm/mysql-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type { z } from "zod";

Expand All @@ -13,6 +13,10 @@ export const categories = mySqlTable("categories", {
.default(sql`(UUID())`),
name: varchar("name", { length: 256 }).notNull(),
imageUrl: varchar("image_url", { length: 256 }).notNull(),
createdAt: timestamp("created_at")
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp("updated_at").onUpdateNow(),
});

export const categorysRelations = relations(categories, ({ many }) => ({
Expand Down
50 changes: 50 additions & 0 deletions packages/db/schema/orders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { relations, sql } from "drizzle-orm";
import { int, mysqlEnum, timestamp, varchar } from "drizzle-orm/mysql-core";
import { createInsertSchema } from "drizzle-zod";

import { mySqlTable } from "./_table";
import { addresses } from "./addresses";
import { products } from "./products";

export const orders = mySqlTable("orders", {
id: varchar("id", { length: 36 })
.notNull()
.primaryKey()
.default(sql`(UUID())`),
status: mysqlEnum("status", [
"pending",
"payment",
"confirmed",
"shipped",
"delivered",
"cancelled",
"done",
])
.notNull()
.default("pending"),
totalPrice: int("total_price").notNull(),
courier: varchar("courier", { length: 255 }).notNull(),
note: varchar("note", { length: 255 }),
createdAt: timestamp("created_at")
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp("updated_at").onUpdateNow(),
productId: varchar("product_id", { length: 255 }).notNull(),
addressId: varchar("address_id", { length: 255 }).notNull(),
});

export const ordersRelations = relations(orders, ({ one }) => ({
product: one(products, {
fields: [orders.productId],
references: [products.id],
}),
address: one(addresses, {
fields: [orders.addressId],
references: [addresses.id],
}),
}));

export const insertOrderSchema = createInsertSchema(orders);
export const insertOrderParams = insertOrderSchema.omit({
id: true,
});
4 changes: 2 additions & 2 deletions packages/db/schema/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { z } from "zod";

import { mySqlTable } from "./_table";
import { categories } from "./categories";
import { users, usersToProducts } from "./users";
import { productSold, users } from "./users";

export const products = mySqlTable(
"products",
Expand Down Expand Up @@ -38,7 +38,7 @@ export const productsRelations = relations(products, ({ many, one }) => ({
fields: [products.sellerId],
references: [users.id],
}),
usersToProducts: many(usersToProducts),
productSold: many(productSold),
category: one(categories, {
fields: [products.categoryId],
references: [categories.id],
Expand Down
Loading

0 comments on commit 5b7acc8

Please sign in to comment.