Remove IDE configuration files and add new selling feature

Removed unnecessary IntelliJ IDEA (`.idea`) configuration files to clean up the repository. Added new sell-related functionality including the SellCryptoPage, SellModal, SellForm, and updated relevant types and UI components.
This commit is contained in:
Mathis H (Avnyr) 2024-11-22 11:06:05 +01:00
parent ea7ab60e5c
commit e3aa219389
Signed by: Mathis
GPG Key ID: DD9E0666A747D126
22 changed files with 329 additions and 84 deletions

8
.idea/.gitignore generated vendored
View File

@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

7
.idea/discord.xml generated
View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
</component>
</project>

View File

@ -1,10 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<Languages>
<language minSize="86" name="TypeScript" />
</Languages>
</inspection_tool>
</profile>
</component>

8
.idea/modules.xml generated
View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/brief-07-front.iml" filepath="$PROJECT_DIR$/.idea/brief-07-front.iml" />
</modules>
</component>
</project>

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated
View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -1,3 +1,16 @@
# neptune-front
The project to validate my DWWM diploma on the front end.
The project to validate my DWWM diploma on the front end.
## Déploiement
- Configurer les variables d'environnements
### En développement
```shell
pnpm dev
```
### En production
```shell
pnpm build
pnpm start
```

View File

@ -5,7 +5,7 @@
"license": "MIT",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "next lint",

View File

@ -10,7 +10,6 @@ import { useEffect, useState } from "react";
export default function DashboardPage() {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [cryptosList, setCryptosList] = useState<ICryptoInWalletInfo[]>([]);
//FIX the loop
useEffect(() => {
ApiRequest.authenticated.get

View File

@ -14,7 +14,7 @@ import type React from "react";
export const metadata: Metadata = {
title: "Neptune Crypto",
description: "A fictive app",
icons: "neptune.svg",
icons: "/neptune.svg",
};
export default function RootLayout({

View File

@ -1,18 +1,18 @@
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Bitcoin, DollarSign, LineChart, Lock, Zap } from "lucide-react"
import Link from "next/link"
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Bitcoin, DollarSign, LineChart, Lock, Zap } from "lucide-react";
import Link from "next/link";
export default function HomePage() {
return (
<div className="flex flex-col min-h-screen">
<main className="flex-1">
<section className="w-full py-12 md:py-16 lg:py-24xl:py-32">
<section className="w-full py-12 md:py-16 lg:py-24 xl:py-32">
<div className="container px-4 md:px-6">
<div className="flex flex-col items-center space-y-4 text-center">
<div className="space-y-2">
<h1 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl lg:text-6xl/none">
<h1 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl lg:text-6xl">
Welcome to Neptune Crypto
</h1>
<p className="mx-auto max-w-[700px] text-gray-500 md:text-xl dark:text-gray-400">
@ -26,27 +26,28 @@ export default function HomePage() {
</div>
</div>
</section>
<section className="w-full py-12 md:py-16 lg:py-20 bg-card rounded">
<div className="container px-4 md:px-6 rounded">
<h2 className="text-3xl font-bold tracking-tighter sm:text-5xl text-center mb-12">Our Features</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader>
<Bitcoin className="h-10 w-10 mb-2" />
<Bitcoin className="h-10 w-10 mb-2"/>
<CardTitle>Multiple Cryptocurrencies</CardTitle>
<CardDescription>Trade a wide variety of popular cryptocurrencies.</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<Lock className="h-10 w-10 mb-2" />
<Lock className="h-10 w-10 mb-2"/>
<CardTitle>Secure Storage</CardTitle>
<CardDescription>Your assets are protected with state-of-the-art security measures.</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<LineChart className="h-10 w-10 mb-2" />
<LineChart className="h-10 w-10 mb-2"/>
<CardTitle>Advanced Trading Tools</CardTitle>
<CardDescription>Access powerful analytics and trading features.</CardDescription>
</CardHeader>
@ -54,24 +55,25 @@ export default function HomePage() {
</div>
</div>
</section>
<section className="w-full py-12 md:py-24 lg:py-32">
<div className="container px-4 md:px-6">
<div className="flex flex-col items-center justify-center space-y-4 text-center">
<div className="space-y-2">
<h2 className="text-3xl font-bold tracking-tighter sm:text-5xl">Start Trading Today</h2>
<p className="mx-auto max-w-[600px] text-gray-500 md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed dark:text-gray-400">
<p className="mx-auto max-w-[600px] text-gray-500 md:text-xl lg:text-base xl:text-xl dark:text-gray-400">
Join thousands of traders and investors on our platform. Get started with as little as $10.
</p>
</div>
<div className="w-full max-w-sm space-y-2">
<Button>
Sign Up
<DollarSign className="ml-2 h-4 w-4" />
</Button>
<Button>
Sign Up
<DollarSign className="ml-2 h-4 w-4"/>
</Button>
<p className="text-xs text-gray-500 dark:text-gray-400">
By signing up, you agree to our{" "}
<Link className="underline underline-offset-2" href="#">
Terms & Conditions
<Link title="Go to legal notice" className="underline underline-offset-2" href="/legal">
Legal Notice
</Link>
</p>
</div>
@ -80,5 +82,5 @@ export default function HomePage() {
</section>
</main>
</div>
)
);
}

View File

@ -0,0 +1,39 @@
"use client"
import {OfferList} from "@/components/sub/OfferList";
import {SellForm} from "@/components/sell-form";
interface SellCryptoPageProps {
params: IParams;
}
interface IParams {
cryptoId: string;
}
export default function SellCryptoPage({ params }: SellCryptoPageProps) {
return (
<main className="flex flex-col sm:flex-row items-center justify-center w-full h-full gap-4 sm:p-4">
<section className={"w-full sm:w-1/3 h-full p-0.5 sm:p-2 flex flex-col items-center justify-center gap-4"}>
{/* Graph demande achat/you/other */}
<div className={"w-full h-1/3 rounded bg-card text-card-foreground"}>
Graph
</div>
{/* stats */}
<div className={"w-full h-2/3 rounded bg-card text-card-foreground"}>
Stats
</div>
</section>
<section className={"w-full sm:w-2/3 h-full p-0.5 sm:p-2 flex flex-col items-center justify-center gap-4"}>
{/* Formulaire */}
<div className={"w-full h-2/3 rounded bg-card text-card-foreground p-2"}>
Formulaire
<SellForm defaultCryptoId={params.cryptoId}/>
</div>
{/* Tab Liste offfres vente existante */}
<OfferList className={"w-full h-1/3"} trades={[]}/>
</section>
</main>
);
}

View File

@ -14,7 +14,6 @@ export default function WalletPage() {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [cryptosList, setCryptosList] = useState<ICryptoInWalletInfo[]>([]);
const userContext = useContext(UserDataContext);
//FIX the loop
useEffect(() => {
console.log(userContext?.userData);

View File

@ -64,6 +64,9 @@ const registerSchema = z.object({
.regex(/[0-9]/, "Password must contain at least one number.")
.regex(/[^a-zA-Z0-9]/, "Password must contain at least one special character.")
.describe("Your account password."),
promoCode: z
.string().max(255, "Your promotional code is too long.").optional(),
});
export function AuthForms() {

View File

@ -3,7 +3,6 @@ import type { DefaultValues } from "react-hook-form";
import type { z } from "zod";
import type { FieldConfig } from "./types";
// TODO: This should support recursive ZodEffects but TypeScript doesn't allow circular type definitions.
export type ZodObjectOrWrapped =
| z.ZodObject<any, any>
| z.ZodEffects<z.ZodObject<any, any>>;

View File

@ -0,0 +1,10 @@
import type {Row} from "@tanstack/react-table";
import type {ICryptoInUserWalletInfo} from "@/interfaces/crypto.interface";
interface SellModalProps {
row: Row<ICryptoInUserWalletInfo>
}
export function SellModal({ row }: SellModalProps) {
}

View File

@ -10,15 +10,11 @@ import { ViewModal } from "@/components/cryptos/view-modal";
import { Button } from "@/components/ui/button";
import { DataTable } from "@/components/ui/data-table";
import type { IUserWallet } from "@/interfaces/userdata.interface";
import {
type ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
import type {
ColumnDef,
} from "@tanstack/react-table";
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];

View File

@ -14,8 +14,8 @@ export function Header({
"flex flex-col md:flex-row justify-between items-center w-full p-1 md:px-3 md:py-2 pb-2 border-b-2"
}
>
<Link href={"/"} className={"flex flex-row justify-center md:justify-start items-center w-fit gap-2"}>
<Image src={"neptune.svg"} alt={"Logo of Neptune"} width={42} height={42} />
<Link title={"Return to home page"} href={"/"} className={"flex flex-row justify-center md:justify-start items-center w-fit gap-2"}>
<Image src={"/neptune.svg"} alt={"Logo of Neptune"} width={42} height={42} />
<h1 className={"font-bold text-xl align-middle text-center text-wrap"}>
{title || "Neptune"}
</h1>

View File

@ -0,0 +1,187 @@
"use client";
import * as z from "zod";
import ApiRequest from "@/services/apiRequest";
import { toast } from "@/components/ui/use-toast";
import * as React from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";
import type { IUserWalletCryptos } from "@/interfaces/crypto.interface";
import type { IApiUserAssetsRes } from "@/interfaces/api.interface";
import { Slider } from "@/components/ui/slider";
import { DollarSign, Equal, X } from "lucide-react";
import { Card } from "@/components/ui/card";
export function SellForm() {
const [currentWallet, setCurrentWallet] = useState<IUserWalletCryptos[]>();
const [sliderValue, setSliderValue] = useState(0);
const [quantity, setQuantity] = useState(0);
const [selectedCrypto, setSelectedCrypto] = useState<string>();
useEffect(() => {
async function fetchAssets() {
const res = await ApiRequest.authenticated.get.json<IApiUserAssetsRes>("user/my-assets");
setCurrentWallet(res.data.UserHasCrypto);
}
fetchAssets();
}, []);
function getMaxSharesForCrypto(cryptoId: string) {
return currentWallet?.find((crypto) => crypto.Crypto.id === cryptoId)?.amount || 0;
}
function getCryptoValue(cryptoId: string) {
return currentWallet?.find((crypto) => crypto.Crypto.id === cryptoId)?.Crypto.value || 0;
}
function getCryptoName(cryptoId: string) {
return currentWallet?.find((crypto) => crypto.Crypto.id === cryptoId)?.Crypto.name || "";
}
const sellToUserSchema = z.object({
cryptoId: z.string({ required_error: "You should select a crypto from your wallet." }).uuid(),
amount: z.number({
required_error: "You should select an amount of the assets that you want to sell.",
}).max(sliderValue),
});
async function onSellToUserSubmit(data: z.infer<typeof sellToUserSchema>) {
const res = await ApiRequest.authenticated.post.json("offer/create", {
id_crypto: data.cryptoId,
amount: data.amount,
});
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;
}
console.log(res);
toast({
title: "Transaction accepted.",
description: <p>The page is going to reload.</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 sellToUserForm = useForm<z.infer<typeof sellToUserSchema>>({
resolver: zodResolver(sellToUserSchema),
});
if (!currentWallet) {
return (
<Card>
<div className="flex items-center justify-center p-2">
<p>Loading...</p>
</div>
</Card>
);
}
return (
<Form {...sellToUserForm}>
<form onSubmit={sellToUserForm.handleSubmit(onSellToUserSubmit)} className="w-full space-y-6">
<FormField
control={sellToUserForm.control}
name="cryptoId"
render={({ field }) => (
<FormItem
onChange={(event) => {
// @ts-ignore
const maxShares = getMaxSharesForCrypto(event.target.value as string);
setSliderValue(maxShares);
setQuantity(maxShares);
// @ts-ignore
setSelectedCrypto(event.target.value as string);
}}
>
<FormLabel>Sell shares of your wallet</FormLabel>
<Select
onValueChange={(val) => {
const maxShares = getMaxSharesForCrypto(val);
setSliderValue(maxShares);
setQuantity(1);
setSelectedCrypto(val);
field.onChange(val);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a crypto from your wallet." />
</SelectTrigger>
</FormControl>
<SelectContent>
{currentWallet?.map((crypto) => (
<SelectItem value={crypto.Crypto.id} key={crypto.Crypto.id}>
{`${crypto.amount}x ${crypto.Crypto.name} - ${crypto.Crypto.value}$/u`}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={sellToUserForm.control}
name="amount"
render={({ field }) => (
<FormItem hidden={!selectedCrypto}>
<FormLabel>Quantity</FormLabel>
<div className="flex items-center justify-between gap-2 p-2">
<p className="bg-accent text-accent-foreground p-1 rounded">{field.value}</p>
<Slider
form="amount"
onValueChange={(val) => {
const singleValue = val[0];
setQuantity(singleValue as number);
field.onChange(singleValue);
}}
value={[quantity]}
min={1}
max={sliderValue}
step={1}
/>
</div>
<FormMessage />
</FormItem>
)}
/>
{selectedCrypto && (
<div className="flex items-center justify-center p-2 gap-2">
<p>{getCryptoName(selectedCrypto)}</p>
<X />
<p>{quantity}</p>
<Equal />
<em className="bg-secondary/35 p-1 rounded">
{(quantity * getCryptoValue(selectedCrypto)).toLocaleString("en-US")}
</em>
<DollarSign />
</div>
)}
<Button type="submit" disabled={!quantity}>
Place sell order
</Button>
</form>
</Form>
);
}

View File

View File

@ -0,0 +1,50 @@
import {Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table";
import {ScrollArea} from "@/components/ui/scroll-area";
import type {IAllTrades} from "@/interfaces/crypto.interface";
import {cn} from "@/lib/utils";
interface OfferListProps {
className?: string;
trades: IAllTrades;
}
interface RenderRowsProps {
data?: IAllTrades;
}
function RenderRows({ data }: RenderRowsProps) {
if (!data || data?.length === 0) return (<TableRow>
<TableCell className="font-medium">{" "}</TableCell>
<TableCell>{" "}</TableCell>
<TableCell>{"No history..."}</TableCell>
<TableCell className="text-right">{" "}</TableCell>
</TableRow>);
return data.map((item, index) => (
<TableRow key={item.Crypto.created_at.toString()}>
<TableCell className="font-medium">{item.Giver.pseudo}</TableCell>
<TableCell>{item.Receiver.pseudo}</TableCell>
<TableCell>{item.Crypto.quantity}</TableCell>
<TableCell className="text-right">${item.Crypto.value}</TableCell>
</TableRow>
));
}
export function OfferList({ className, trades}: OfferListProps) {
return (<div className={cn("bg-card text-card-foreground rounded p-2", className)}>
<Table>
<TableCaption>A list of recent sell offers.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Seller</TableHead>
<TableHead>Buyer</TableHead>
<TableHead>Crypto share amount</TableHead>
<TableHead className="text-right">$</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<RenderRows data={trades}/>
</TableBody>
</Table>
</div>)
}

View File

@ -15,7 +15,6 @@ export interface IUserData {
dollarAvailables: number;
created_at: string;
updated_at: string;
//TODO get on register
wallet: IUserWallet;
}