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