feat(components): Add buy-modal component for purchasing crypto
This commit introduces a new component, `buy-modal.tsx`, within the cryptos scope of components. This component allows the option to buy cryptocurrency either from a user or directly from the server. It provides form validation, API integrations, handles states, and displays proper response messages.
This commit is contained in:
parent
37b7116a69
commit
49d485c11a
193
src/components/cryptos/buy-modal.tsx
Normal file
193
src/components/cryptos/buy-modal.tsx
Normal file
@ -0,0 +1,193 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Ban, DollarSign, RefreshCw} from "lucide-react";
|
||||
import * as z from "zod";
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {Dispatch, SetStateAction, useEffect, useState} from "react";
|
||||
import type {ICryptoInWalletInfo} from "@/interfaces/crypto.interface";
|
||||
import {toast} from "@/components/ui/use-toast";
|
||||
import AutoForm, {AutoFormSubmit} from "@/components/auto-form";
|
||||
import type {IApiAllOffersRes, IApiDoTradeReq} from "@/interfaces/api.interface";
|
||||
import ApiRequest from "@/services/apiRequest";
|
||||
import {Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||
import Link from "next/link";
|
||||
import {useForm} from "react-hook-form";
|
||||
import {zodResolver} from "@hookform/resolvers/zod";
|
||||
type Props = {
|
||||
cryptoData: ICryptoInWalletInfo
|
||||
};
|
||||
export function BuyModal(props: Props) {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
const [offersList, setOffersList] = useState<IApiAllOffersRes[]>([])
|
||||
const [isLoaded,setIsLoaded] = useState(false)
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
useEffect(() => {
|
||||
if(!isLoaded) {
|
||||
ApiRequest.authenticated.get.json<IApiAllOffersRes[]>(`offer/crypto/${props.cryptoData.id}`).then((response) => {
|
||||
setOffersList(response.data);
|
||||
console.log(`Crypto ${props.cryptoData.name} -> ${response.data.length}`)
|
||||
setIsLoaded(true);
|
||||
return;
|
||||
})
|
||||
}
|
||||
}, [isLoaded]);
|
||||
|
||||
const buyFromServerSchema = z.object({
|
||||
amount: z
|
||||
.number({
|
||||
required_error: "An amount is needed.",
|
||||
})
|
||||
.min(1)
|
||||
.max(props.cryptoData.quantity, "You cant buy more that what is available on the server.")
|
||||
.describe("The amount you want to buy."),
|
||||
});
|
||||
|
||||
const buyFromUserSchema = z.object({
|
||||
offerId: z
|
||||
.string({
|
||||
required_error: "You should select an offer.",
|
||||
})
|
||||
.uuid(),
|
||||
})
|
||||
|
||||
function onBuyFromServerSubmit(data: z.infer<typeof buyFromServerSchema>) {
|
||||
ApiRequest.authenticated.post.json("crypto/buy", {id_crypto: props.cryptoData.id, amount: data.amount}).then((res)=>{
|
||||
if (res.status !== 201) {
|
||||
toast({
|
||||
title: "An error occurred !",
|
||||
description: (<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white text-wrap">{JSON.stringify(res.statusText, null, 2)}</code>
|
||||
</pre>)
|
||||
})
|
||||
return
|
||||
}
|
||||
toast({
|
||||
title: "Transaction accepted.",
|
||||
description: (
|
||||
<p>You will be redirected.</p>
|
||||
)
|
||||
})
|
||||
setTimeout(()=>location.reload(), 1_500)
|
||||
})
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
function onBuyFromUserSubmit(data: z.infer<typeof buyFromUserSchema>) {
|
||||
ApiRequest.authenticated.post.json("trade/create", {id_offer: data.offerId}).then((res)=>{
|
||||
if (res.status !== 201) {
|
||||
toast({
|
||||
title: "An error occurred !",
|
||||
description: (<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white text-wrap">{JSON.stringify(res.statusText, null, 2)}</code>
|
||||
</pre>)
|
||||
})
|
||||
return
|
||||
}
|
||||
toast({
|
||||
title: "Transaction accepted.",
|
||||
description: (
|
||||
<p>You will be redirected.</p>
|
||||
)
|
||||
})
|
||||
setTimeout(()=>location.reload(), 1_500)
|
||||
})
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
const buyFromUserForm = useForm<z.infer<typeof buyFromUserSchema>>({
|
||||
resolver: zodResolver(buyFromUserSchema),
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="light"><DollarSign /></Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px] md:max-w-[800px] flex flex-col justify-start items-center">
|
||||
<Tabs defaultValue="server" className="w-full p-2 my-4">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="user">Buy from user <p className={" ml-1 px-1 bg-primary text-primary-foreground rounded"}>{offersList.length}</p></TabsTrigger>
|
||||
<TabsTrigger value="server">Buy from server</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="user" className={"flex flex-col justify-start items-center"}>
|
||||
<Form {...buyFromUserForm}>
|
||||
<form onSubmit={buyFromUserForm.handleSubmit(onBuyFromUserSubmit)} className="w-full space-y-6">
|
||||
<FormField
|
||||
control={buyFromUserForm.control}
|
||||
name="offerId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Select an offer</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an offer to purchase." />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{offersList.map((offer)=>{
|
||||
return (
|
||||
<SelectItem value={offer.id} key={offer.id}>{`${offer.amount}x ${offer.Crypto.name} - ${offer.User.pseudo}`}</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</TabsContent>
|
||||
<TabsContent value="server" className={"flex flex-col justify-start items-center"}>
|
||||
{!props.cryptoData.quantity && <div
|
||||
className={"bg-destructive text-destructive-foreground border-destructive rounded p-2 flex justify-start items-center gap-2"}>
|
||||
<Ban/>
|
||||
<p>The server dont have stock for the designated cryptos.</p>
|
||||
</div>}
|
||||
<div className={"w-full flex justify-center gap-2 items-center p-2"}>
|
||||
<p>Available quantity on the server :</p>
|
||||
<p className={"p-1 bg-accent text-accent-foreground rounded m-1"}>{props.cryptoData.quantity}</p>
|
||||
</div>
|
||||
{props.cryptoData.quantity && <AutoForm
|
||||
// Pass the schema to the form
|
||||
formSchema={buyFromServerSchema}
|
||||
onSubmit={onBuyFromServerSubmit}
|
||||
>
|
||||
<AutoFormSubmit
|
||||
disabled={!!isLoading}
|
||||
className={"gap-2 disabled:bg-secondary-foreground"}
|
||||
>
|
||||
{/* biome-ignore lint/style/useTemplate: <explanation> */}
|
||||
<RefreshCw className={`animate-spin ${!isLoading && "hidden"}`}/>
|
||||
<p>Buy from the server</p>
|
||||
</AutoFormSubmit>
|
||||
</AutoForm>}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user