Compare commits
1 Commits
442bebd022
...
old-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
a75a87f683
|
1
.env.development.example
Normal file
1
.env.development.example
Normal file
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_API_BASE_URL: ""
|
||||
1
.env.production.example
Normal file
1
.env.production.example
Normal file
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_API_BASE_URL: "localhost:3333"
|
||||
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
.env.production
|
||||
.env.development
|
||||
.env*.local
|
||||
/.env
|
||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# 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
Normal file
7
.idea/discord.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?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>
|
||||
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<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
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?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>
|
||||
12
.idea/neptune-frontend.iml
generated
Normal file
12
.idea/neptune-frontend.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?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
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
31
Dockerfile
Normal file
31
Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
# Étape 1 : Build
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
RUN npm install -g pnpm
|
||||
# Copier package.json et installer les dépendances
|
||||
COPY pnpm-lock.yaml ./
|
||||
COPY package.json ./
|
||||
RUN pnpm install
|
||||
|
||||
# Copier le reste du code et construire l'application
|
||||
COPY . .
|
||||
RUN pnpm run build
|
||||
|
||||
# Étape 2 : Serveur de production
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copier uniquement les fichiers nécessaires
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/pnpm-lock.yaml ./
|
||||
COPY --from=builder /app/.next ./.next
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
|
||||
# Exposer le port de l'application
|
||||
EXPOSE 3000
|
||||
|
||||
# Commande de démarrage
|
||||
CMD ["npm", "start"]
|
||||
37
biome.json
Normal file
37
biome.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"files": {
|
||||
"include": [
|
||||
"./src/**/*.ts",
|
||||
"./src/**/*.tsx"
|
||||
]
|
||||
},
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"performance": {
|
||||
"recommended": true,
|
||||
"noDelete": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "warn"
|
||||
},
|
||||
"complexity": {
|
||||
"useLiteralKeys": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"formatter": {
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 90
|
||||
}
|
||||
}
|
||||
17
components.json
Normal file
17
components.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "stone",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
4
next.config.mjs
Normal file
4
next.config.mjs
Normal file
@@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
79
package.json
Normal file
79
package.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "neptune-back",
|
||||
"version": "0.1.0",
|
||||
"author": "Mathis HERRIOT",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"check": "biome check --skip-errors --write src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/kode-mono": "^5.1.0",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@radix-ui/react-accordion": "^1.2.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
||||
"@radix-ui/react-avatar": "^1.1.1",
|
||||
"@radix-ui/react-checkbox": "^1.1.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.1",
|
||||
"@radix-ui/react-context-menu": "^2.2.2",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@radix-ui/react-hover-card": "^1.1.2",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-menubar": "^1.1.2",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.1",
|
||||
"@radix-ui/react-popover": "^1.1.2",
|
||||
"@radix-ui/react-progress": "^1.1.0",
|
||||
"@radix-ui/react-radio-group": "^1.2.1",
|
||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slider": "^1.2.1",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.1",
|
||||
"@radix-ui/react-toast": "^1.2.2",
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.4",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"axios": "^1.7.7",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.4",
|
||||
"date-fns": "^3.6.0",
|
||||
"embla-carousel-react": "^8.3.1",
|
||||
"framer-motion": "^11.11.15",
|
||||
"input-otp": "^1.4.1",
|
||||
"lightweight-charts": "^4.2.1",
|
||||
"lucide-react": "^0.395.0",
|
||||
"next": "14.2.10",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.53.2",
|
||||
"react-resizable-panels": "^2.1.6",
|
||||
"sonner": "^1.7.0",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^0.9.9",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.8.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.17.6",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"jest": "^29.7.0",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
5364
pnpm-lock.yaml
generated
Normal file
5364
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
8
postcss.config.mjs
Normal file
8
postcss.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
public/neptune.png
Normal file
BIN
public/neptune.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
54
public/neptune.svg
Normal file
54
public/neptune.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.1 KiB |
10
src/app/auth/page.tsx
Normal file
10
src/app/auth/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { AuthForms } from "@/components/auth-form";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function AuthPage() {
|
||||
return (
|
||||
<main className="flex flex-col items-center justify-start h-full w-full">
|
||||
<AuthForms />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
9
src/app/dashboard/admin/page.tsx
Normal file
9
src/app/dashboard/admin/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
export default function AdminPage() {
|
||||
return (
|
||||
<>
|
||||
<section>
|
||||
<h1>Welcome to the Dashboard</h1>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
59
src/app/dashboard/page.tsx
Normal file
59
src/app/dashboard/page.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { CryptosTable } from "@/components/data-tables/cryptos-table";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type { IApiAllOffersRes } from "@/interfaces/api.interface";
|
||||
import type { ICryptoInWalletInfo } from "@/interfaces/crypto.interface";
|
||||
import ApiRequest from "@/services/apiRequest";
|
||||
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
|
||||
.json<ICryptoInWalletInfo[]>("crypto/all")
|
||||
.then((response) => {
|
||||
if (response.data.length <= 0) {
|
||||
setCryptosList([]);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
const resp = response.data;
|
||||
setCryptosList(resp);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"container flex flex-col justify-center items-center min-h-64 my-6 gap-4"
|
||||
}
|
||||
>
|
||||
<h1 className={"text-2xl font-bold"}>Available cryptos</h1>
|
||||
<Skeleton
|
||||
className={
|
||||
"container flex flex-col justify-center items-center min-h-64 my-6 gap-4"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section
|
||||
className={
|
||||
"container flex flex-col justify-center items-center min-h-64 my-6 gap-4"
|
||||
}
|
||||
>
|
||||
<h1 className={"text-2xl font-bold"}>Available cryptos</h1>
|
||||
<CryptosTable cryptosArray={cryptosList} />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
65
src/app/globals.css
Normal file
65
src/app/globals.css
Normal file
@@ -0,0 +1,65 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
font-family: 'Kode Mono Variable', monospace;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 289 20.6% 90.87%;
|
||||
--foreground: 289 71% 3%;
|
||||
--muted: 291.15 0% 100%;
|
||||
--muted-foreground: 109 0% 39%;
|
||||
--popover: 289 62% 97%;
|
||||
--popover-foreground: 289 71% 2%;
|
||||
--card: 286.67 60% 97.06%;
|
||||
--card-foreground: 289 71% 2%;
|
||||
--border: 289 0% 78.21%;
|
||||
--input: 289 14.91% 46.64%;
|
||||
--primary: 271.89 62.45% 55.1%;
|
||||
--primary-foreground: 289 39% 96%;
|
||||
--secondary: 271.83 81.56% 27.65%;
|
||||
--secondary-foreground: 0 0% 100%;
|
||||
--accent: 36.14 100% 67.45%;
|
||||
--accent-foreground: 0 0% 12.94%;
|
||||
--destructive: 8 89% 28%;
|
||||
--destructive-foreground: 8 89% 88%;
|
||||
--ring: 289 39% 36%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 289 47.53% 5.21%;
|
||||
--foreground: 289 6.69% 90.11%;
|
||||
--muted: 271.83 26.29% 22.62%;
|
||||
--muted-foreground: 0 0% 73.85%;
|
||||
--popover: 289 17.57% 17.45%;
|
||||
--popover-foreground: 0 0% 89.23%;
|
||||
--card: 289 50.44% 6.99%;
|
||||
--card-foreground: 300 5.88% 90%;
|
||||
--border: 289 5.38% 31.15%;
|
||||
--input: 289 12.37% 29.89%;
|
||||
--primary: 271.89 62.45% 55.1%;
|
||||
--primary-foreground: 289 39% 96%;
|
||||
--secondary: 271.83 81.56% 27.65%;
|
||||
--secondary-foreground: 300 5.88% 90%;
|
||||
--accent: 36.14 100% 67.45%;
|
||||
--accent-foreground: 0 0% 12.94%;
|
||||
--destructive: 350.05 93.43% 41.76%;
|
||||
--destructive-foreground: 0 0% 92.55%;
|
||||
--ring: 289 39% 36%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
46
src/app/layout.tsx
Normal file
46
src/app/layout.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Metadata } from "next";
|
||||
import "@fontsource-variable/kode-mono";
|
||||
import "./globals.css";
|
||||
import { Footer } from "@/components/footer";
|
||||
import { Header } from "@/components/header";
|
||||
import { PrimaryNavigationMenu } from "@/components/primary-nav";
|
||||
import { Providers } from "@/components/providers/providers";
|
||||
import { ThemeProvider } from "@/components/providers/theme-provider";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import Link from "next/link";
|
||||
import type React from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Neptune Crypto",
|
||||
description: "A fictive app",
|
||||
icons: "neptune.svg",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={"w-full min-h-screen flex flex-col items-center justify-between"}>
|
||||
<Providers>
|
||||
<Header>
|
||||
<div className={"flex flex-row flex-wrap md:flex-nowrap gap-2"}>
|
||||
<Button asChild variant={"light"}>
|
||||
<Link href={"/wallet"}><p className={"lg:text-lg"}>Wallet</p></Link>
|
||||
</Button>
|
||||
<Button asChild variant={"light"}>
|
||||
<Link href={"/dashboard"}><p className={"lg:text-lg"}>Explore cryptos</p></Link>
|
||||
</Button>
|
||||
</div>
|
||||
</Header>
|
||||
{children}
|
||||
<Toaster />
|
||||
<Footer />
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
84
src/app/page.tsx
Normal file
84
src/app/page.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
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">
|
||||
<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">
|
||||
Welcome to Neptune Crypto
|
||||
</h1>
|
||||
<p className="mx-auto max-w-[700px] text-gray-500 md:text-xl dark:text-gray-400">
|
||||
Your gateway to the world of cryptocurrencies. Trade, invest, and grow your digital assets with ease.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-x-4">
|
||||
<Button>Get Started</Button>
|
||||
<Button variant="outline">Learn More</Button>
|
||||
</div>
|
||||
</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" />
|
||||
<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" />
|
||||
<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" />
|
||||
<CardTitle>Advanced Trading Tools</CardTitle>
|
||||
<CardDescription>Access powerful analytics and trading features.</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</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">
|
||||
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>
|
||||
<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>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
57
src/app/wallet/page.tsx
Normal file
57
src/app/wallet/page.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { CryptosTable } from "@/components/data-tables/cryptos-table";
|
||||
import { WalletTable } from "@/components/data-tables/wallet-table";
|
||||
import { UserDataContext } from "@/components/providers/userdata-provider";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type { IApiAllOffersRes } from "@/interfaces/api.interface";
|
||||
import type { ICryptoInWalletInfo } from "@/interfaces/crypto.interface";
|
||||
import type { IUserWallet } from "@/interfaces/userdata.interface";
|
||||
import ApiRequest from "@/services/apiRequest";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
|
||||
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);
|
||||
if (userContext?.userData) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [userContext]);
|
||||
|
||||
if (isLoading || !userContext?.userData) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"container flex flex-col justify-center items-center min-h-64 my-6 gap-4"
|
||||
}
|
||||
>
|
||||
<h1 className={"text-2xl font-bold"}>Loading in progress ...</h1>
|
||||
<Skeleton
|
||||
className={
|
||||
"container flex flex-col justify-center items-center min-h-64 my-6 gap-4"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section
|
||||
className={
|
||||
"container flex flex-col justify-center items-center min-h-64 my-6 gap-4 rounded sm:p-2 sm:bg-card"
|
||||
}
|
||||
>
|
||||
<h1 className={"text-xl sm:text-2xl lg:text-3xl font-bold"}>Cryptos in your wallet</h1>
|
||||
<WalletTable
|
||||
walletArray={userContext.userData.wallet as unknown as IUserWallet}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
53
src/components/account-dialog.tsx
Normal file
53
src/components/account-dialog.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { AccountInfo } from "@/components/account-info";
|
||||
import { UserDataContext } from "@/components/providers/userdata-provider";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type { IUserData } from "@/interfaces/userdata.interface";
|
||||
import {
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
const localStorage = typeof window !== "undefined" ? window.localStorage : null;
|
||||
|
||||
export function AccountDialog() {
|
||||
const userContext = useContext(UserDataContext);
|
||||
const token = localStorage?.getItem("sub") || "";
|
||||
const haveToken = token.length >= 16 || false;
|
||||
console.log(haveToken);
|
||||
const [isLoaded, setIsLoaded] = useState<boolean>(false);
|
||||
|
||||
if (!userContext) {
|
||||
return (
|
||||
<div>
|
||||
<p>No account</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (userContext?.userData) {
|
||||
setIsLoaded(true);
|
||||
}
|
||||
}, [userContext?.userData]);
|
||||
|
||||
if (!isLoaded) {
|
||||
return <Skeleton className="w-14 h-10 rounded" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AccountInfo
|
||||
userData={userContext?.userData as IUserData}
|
||||
setUserData={
|
||||
userContext?.setUserData as Dispatch<SetStateAction<IUserData | undefined>>
|
||||
}
|
||||
isDisconnected={!haveToken}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
174
src/components/account-info.tsx
Normal file
174
src/components/account-info.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import type { IUserData, IUserWallet } from "@/interfaces/userdata.interface";
|
||||
|
||||
import { CopyButton } from "@/components/ui/copy-button";
|
||||
import {
|
||||
type ICryptoInUserWalletInfo,
|
||||
ICryptoInWalletInfo,
|
||||
} from "@/interfaces/crypto.interface";
|
||||
import { doDisconnect, getWallet } from "@/services/account.handler";
|
||||
import { Bitcoin, Fingerprint, Key, Landmark, Unplug, User, Wallet } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function AccountInfo({
|
||||
userData,
|
||||
setUserData,
|
||||
isDisconnected,
|
||||
}: {
|
||||
userData: IUserData;
|
||||
setUserData: React.Dispatch<React.SetStateAction<IUserData | undefined>>;
|
||||
isDisconnected: boolean;
|
||||
}) {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoaded) {
|
||||
getWallet().then((res) => {
|
||||
const wallet: IUserWallet = {
|
||||
uat: Date.now(),
|
||||
update_interval: 30_000,
|
||||
owned_cryptos:
|
||||
res.resolved?.UserHasCrypto?.map((el): ICryptoInUserWalletInfo => {
|
||||
return {
|
||||
id: el.Crypto.id,
|
||||
name: el.Crypto.name,
|
||||
value: el.Crypto.value,
|
||||
image: el.Crypto.image,
|
||||
quantity: el.Crypto.quantity,
|
||||
owned_amount: el.amount,
|
||||
created_at: el.Crypto.created_at,
|
||||
updated_at: el.Crypto.updated_at,
|
||||
};
|
||||
}) || [],
|
||||
};
|
||||
|
||||
delete res.resolved?.UserHasCrypto;
|
||||
//@ts-ignore
|
||||
setUserData({
|
||||
...userData,
|
||||
...res.resolved,
|
||||
wallet: wallet,
|
||||
});
|
||||
console.log(userData);
|
||||
setIsLoaded(true);
|
||||
});
|
||||
}
|
||||
}, [isLoaded, userData, setUserData]);
|
||||
if (isDisconnected) {
|
||||
return (
|
||||
<div className={"flex flex-col justify-center items-center h-10 p-2 text-xs mt-2"}>
|
||||
<div
|
||||
className={
|
||||
"flex flex-row justify-center items-center gap-1 text-destructive to-red-900 animate-pulse"
|
||||
}
|
||||
>
|
||||
<Unplug className={"w-4"} />
|
||||
<p>Disconnected</p>
|
||||
</div>
|
||||
<div>
|
||||
<Link
|
||||
href={"/auth"}
|
||||
className={
|
||||
"hover:text-primary flex justify-evenly items-center gap-1 p-1 text-nowrap"
|
||||
}
|
||||
>
|
||||
<Key className={"w-3"} /> Link account
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className={"gap-2 px-2"}>
|
||||
<p>{userData.firstName}</p>
|
||||
<User />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px] md:max-w-[720px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{`Your account - ${userData.firstName} ${userData.lastName}`}</DialogTitle>
|
||||
<DialogDescription>{userData.pseudo}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className={"flex flex-col items-center justify-center w-full"}>
|
||||
<div className={"flex flex-col justify-evenly items-center gap-2"}>
|
||||
<div
|
||||
className={
|
||||
"flex flex-col md:flex-row gap-2 justify-center md:justify-evenly items-start md:items-center w-full"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"flex gap-1 justify-start md:justify-center items-center mx-auto w-full md:w-fit"
|
||||
}
|
||||
>
|
||||
<Landmark />
|
||||
<p className={"rounded bg-accent text-accent-foreground p-1"}>
|
||||
{userData.dollarAvailables} $
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"flex gap-1 justify-start md:justify-center items-center mx-auto w-full md:w-fit"
|
||||
}
|
||||
>
|
||||
<Bitcoin />
|
||||
<p className={"rounded bg-accent text-accent-foreground p-1"}>
|
||||
{`You currently have ${userData.wallet.owned_cryptos.length} crypto(s)`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={"flex flex-col gap-3 justify-center items-start mx-auto mt-4"}
|
||||
>
|
||||
<div className={"flex flex-row text-nowrap flex-nowrap gap-1 text-primary"}>
|
||||
<Fingerprint />
|
||||
<h2>Your identity</h2>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"font-light text-xs md:text-sm flex flex-row items-center justify-start gap-1 bg-accent p-2 rounded"
|
||||
}
|
||||
>
|
||||
<p>{userData.id}</p>
|
||||
<CopyButton value={userData.id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant={"secondary"} className={"gap-2 px-2"} asChild>
|
||||
<Link href={"/wallet"}>
|
||||
<Wallet />
|
||||
<p>My wallet</p>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
className={"gap-2 px-2 mb-2"}
|
||||
onClick={() => doDisconnect()}
|
||||
>
|
||||
<Unplug />
|
||||
<p>Disconnect</p>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
259
src/components/auth-form.tsx
Normal file
259
src/components/auth-form.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
"use client";
|
||||
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
import AutoForm, { AutoFormSubmit } from "@/components/auto-form";
|
||||
import { UserDataContext } from "@/components/providers/userdata-provider";
|
||||
import { ToastBox, toastType } from "@/components/ui/toast-box";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import type {
|
||||
IApiLoginReq,
|
||||
IApiLoginRes,
|
||||
IApiRegisterReq,
|
||||
IApiRegisterRes,
|
||||
} from "@/interfaces/api.interface";
|
||||
import { EReturnState, type IStandardisedReturn } from "@/interfaces/general.interface";
|
||||
import type { IUserData } from "@/interfaces/userdata.interface";
|
||||
import ApiRequest from "@/services/apiRequest";
|
||||
import { useLocalStorage } from "@/services/localStorage";
|
||||
import { Bug, RefreshCw } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import React, { type Dispatch, type SetStateAction, useContext, useState } from "react";
|
||||
import * as z from "zod";
|
||||
import Image from "next/image";
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z
|
||||
.string({
|
||||
required_error: "Email is needed.",
|
||||
})
|
||||
.email({
|
||||
message: "Should be a valid email.",
|
||||
})
|
||||
.describe("Your account email."),
|
||||
password: z
|
||||
.string({
|
||||
required_error: "Password is needed.",
|
||||
})
|
||||
.describe("Your account password."),
|
||||
});
|
||||
|
||||
const registerSchema = z.object({
|
||||
firstName: z.string({
|
||||
required_error: "First name is required.",
|
||||
}).describe("First name"),
|
||||
lastName: z.string({
|
||||
required_error: "Last name is required.",
|
||||
}).describe("Last name"),
|
||||
pseudo: z.string({
|
||||
required_error: "Nickname is required.",
|
||||
}).describe("Nickname"),
|
||||
email: z
|
||||
.string({
|
||||
required_error: "Email is required.",
|
||||
})
|
||||
.email("Email must be valid."),
|
||||
password: z
|
||||
.string({
|
||||
required_error: "Password is required.",
|
||||
})
|
||||
.min(8, "Password must be at least 8 characters.")
|
||||
.max(32, "Password must be less than 32 characters.")
|
||||
.regex(/[A-Z]/, "Password must contain at least one uppercase letter.")
|
||||
.regex(/[a-z]/, "Password must contain at least one lowercase letter.")
|
||||
.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."),
|
||||
});
|
||||
|
||||
export function AuthForms() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [sub, setSub] = useLocalStorage<string | undefined>("sub", "");
|
||||
const userContext = useContext(UserDataContext);
|
||||
const { toast } = useToast();
|
||||
|
||||
async function doRegister(
|
||||
registerData: IApiRegisterReq,
|
||||
userDataSetter: Dispatch<SetStateAction<IUserData | null>>,
|
||||
): Promise<IStandardisedReturn<IApiRegisterRes>> {
|
||||
console.trace(registerData);
|
||||
try {
|
||||
const ReqRes = await ApiRequest.standard.post.json<
|
||||
IApiRegisterReq,
|
||||
IApiRegisterRes
|
||||
>("auth/sign-up", registerData);
|
||||
console.trace(ReqRes.data);
|
||||
if (ReqRes.data.user) {
|
||||
userDataSetter({
|
||||
...ReqRes.data.user,
|
||||
wallet: {
|
||||
uat: Date.now(),
|
||||
update_interval: 30_000,
|
||||
owned_cryptos: [],
|
||||
},
|
||||
});
|
||||
|
||||
setSub(ReqRes.data.access_token);
|
||||
}
|
||||
console.debug(ReqRes.data.message || "Not additional message from request");
|
||||
return {
|
||||
state: EReturnState.done,
|
||||
resolved: ReqRes.data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error during registration:", error);
|
||||
return {
|
||||
state: EReturnState.serverError,
|
||||
message: error as string,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function doLogin(
|
||||
loginData: IApiLoginReq,
|
||||
): Promise<IStandardisedReturn<IApiLoginRes>> {
|
||||
try {
|
||||
const ReqRes = await ApiRequest.standard.post.json<IApiLoginReq, IApiLoginRes>(
|
||||
"auth/sign-in",
|
||||
loginData,
|
||||
);
|
||||
console.trace(ReqRes.data);
|
||||
if (ReqRes.data.access_token) {
|
||||
setSub(ReqRes.data.access_token);
|
||||
}
|
||||
return {
|
||||
state: EReturnState.done,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Error during login:", err);
|
||||
return {
|
||||
state: EReturnState.serverError,
|
||||
message: err as string,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!userContext || !userContext.setUserData) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"bg-destructive text-destructive-foreground p-3 gap-2 border rounded flex flex-row justify-center items-center"
|
||||
}
|
||||
>
|
||||
<Bug />
|
||||
<p>It seems that the context is missing..</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={"flex flex-col gap-6 items-center justify-center"}>
|
||||
<Link href={"/"} className={"hidden sm:visible sm:flex flex-row justify-center md:justify-start items-center w-fit gap-2"}>
|
||||
<Image src={"neptune.svg"} alt={"Logo of Neptune"} className={"w-24 h-24"} width={128} height={128} />
|
||||
<h1 className={"font-bold text-xl lg:text-2xl align-middle text-center text-wrap"}>
|
||||
Neptune Crypto
|
||||
</h1>
|
||||
</Link>
|
||||
<Tabs defaultValue="login" className="w-full p-2 gap-4 flex flex-col justify-center sm:p-4 rounded bg-card text-card-foreground md:w-[400px] my-4">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="login">Login</TabsTrigger>
|
||||
<TabsTrigger value="register">Register</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="login">
|
||||
<AutoForm
|
||||
// I pass the Zod schema to the form
|
||||
formSchema={loginSchema}
|
||||
onSubmit={(data: IApiLoginReq) => {
|
||||
setIsLoading(true);
|
||||
doLogin(data).then((res) => {
|
||||
if (res.state !== EReturnState.done) {
|
||||
toast({
|
||||
description: res.message || "An unexpected error occurred..",
|
||||
variant: "destructive",
|
||||
});
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
//toast.custom(<ToastBox message={"Login successful ! \n You will be redirected."} type={toastType.success}/>)
|
||||
toast({
|
||||
description: "Login successful ! \n You will be redirected.",
|
||||
});
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
location.href = "/";
|
||||
console.log("Moving to home.");
|
||||
}, 3_000);
|
||||
});
|
||||
}}
|
||||
fieldConfig={{
|
||||
password: {
|
||||
inputProps: {
|
||||
type: "password",
|
||||
placeholder: "••••••••",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AutoFormSubmit
|
||||
disabled={!!isLoading}
|
||||
className={"gap-2 disabled:bg-secondary"}
|
||||
>
|
||||
{/* biome-ignore lint/style/useTemplate: <explanation> */}
|
||||
<RefreshCw className={"animate-spin" + isLoading && "hidden"} />
|
||||
<p>Login</p>
|
||||
</AutoFormSubmit>
|
||||
</AutoForm>
|
||||
</TabsContent>
|
||||
<TabsContent value="register">
|
||||
<AutoForm
|
||||
// Pass the schema to the form
|
||||
formSchema={registerSchema}
|
||||
onSubmit={(data: IApiRegisterReq) => {
|
||||
setIsLoading(true);
|
||||
doRegister(
|
||||
data,
|
||||
userContext.setUserData as Dispatch<SetStateAction<IUserData | null>>,
|
||||
).then((res) => {
|
||||
if (res.state !== EReturnState.done) {
|
||||
//toast.custom(<ToastBox message={res.message || "An unexpected error occurred.."} type={toastType.error}/>)
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
//toast.custom(<ToastBox message={"Register successful ! \n You will be redirected."} type={toastType.success}/>)
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
//location.href = "/"
|
||||
console.log("Moving to home.");
|
||||
}, 5_000);
|
||||
});
|
||||
}}
|
||||
fieldConfig={{
|
||||
password: {
|
||||
inputProps: {
|
||||
type: "password",
|
||||
placeholder: "••••••••",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AutoFormSubmit
|
||||
disabled={!!isLoading}
|
||||
className={"gap-2 disabled:bg-secondary"}
|
||||
>
|
||||
{/* biome-ignore lint/style/useTemplate: <explanation> */}
|
||||
<RefreshCw className={"animate-spin" + !isLoading && "hidden"} />
|
||||
<p>Register</p>
|
||||
</AutoFormSubmit>
|
||||
<p className="text-gray-500 text-sm">
|
||||
By submitting this form, you agree to our{" "}
|
||||
<Link href="#" className="text-primary underline">
|
||||
terms and conditions
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</AutoForm>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
243
src/components/cryptos/buy-modal.tsx
Normal file
243
src/components/cryptos/buy-modal.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
import AutoForm, { AutoFormSubmit } from "@/components/auto-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { toast } from "@/components/ui/use-toast";
|
||||
import type { IApiAllOffersRes, IApiDoTradeReq } from "@/interfaces/api.interface";
|
||||
import type { ICryptoInWalletInfo } from "@/interfaces/crypto.interface";
|
||||
import ApiRequest from "@/services/apiRequest";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Ban, DollarSign, RefreshCw } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import * as React from "react";
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "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) => {
|
||||
if (response.data) {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) => {
|
||||
if (!offer) return;
|
||||
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"}
|
||||
>
|
||||
<RefreshCw className={`animate-spin ${!isLoading && "hidden"}`} />
|
||||
<p>Buy from the server</p>
|
||||
</AutoFormSubmit>
|
||||
</AutoForm>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
32
src/components/cryptos/view-modal.tsx
Normal file
32
src/components/cryptos/view-modal.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { LineChart } from "lucide-react";
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
type Props = {
|
||||
targetedCryptoId: string;
|
||||
};
|
||||
export function ViewModal(props: Props) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<LineChart />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px] md:max-w-[800px]">
|
||||
Test 1,2 ! //From here
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
85
src/components/data-tables/cryptos-table.tsx
Normal file
85
src/components/data-tables/cryptos-table.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
import type { ICryptoInWalletInfo } from "@/interfaces/crypto.interface";
|
||||
import * as React from "react";
|
||||
|
||||
import { BuyModal } from "@/components/cryptos/buy-modal";
|
||||
import { ViewModal } from "@/components/cryptos/view-modal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DataTable } from "@/components/ui/data-table";
|
||||
import {
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} 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>[];
|
||||
data: TData[];
|
||||
}
|
||||
|
||||
type Props = {
|
||||
cryptosArray: ICryptoInWalletInfo[];
|
||||
};
|
||||
|
||||
export function CryptosTable(props: Props) {
|
||||
const router = useRouter();
|
||||
const cryptos = props.cryptosArray;
|
||||
|
||||
const columns: ColumnDef<ICryptoInWalletInfo, any>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
},
|
||||
{
|
||||
accessorKey: "value",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Value - USD
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "quantity",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Available from server
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const payment = row.original;
|
||||
|
||||
return (
|
||||
<div className={"flex gap-2"}>
|
||||
<BuyModal cryptoData={row.original} />
|
||||
<ViewModal targetedCryptoId={row.original.id} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable columns={columns} data={cryptos} fieldToFilter={"name"} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
0
src/components/data-tables/offers-table.tsx
Normal file
0
src/components/data-tables/offers-table.tsx
Normal file
0
src/components/data-tables/trades-table.tsx
Normal file
0
src/components/data-tables/trades-table.tsx
Normal file
88
src/components/data-tables/wallet-table.tsx
Normal file
88
src/components/data-tables/wallet-table.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
import type {
|
||||
ICryptoInUserWalletInfo,
|
||||
ICryptoInWalletInfo,
|
||||
} from "@/interfaces/crypto.interface";
|
||||
import * as React from "react";
|
||||
|
||||
import { BuyModal } from "@/components/cryptos/buy-modal";
|
||||
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,
|
||||
} 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>[];
|
||||
data: TData[];
|
||||
}
|
||||
|
||||
type Props = {
|
||||
walletArray: IUserWallet;
|
||||
};
|
||||
|
||||
export function WalletTable(props: Props) {
|
||||
const router = useRouter();
|
||||
const wallet = props.walletArray.owned_cryptos;
|
||||
|
||||
const columns: ColumnDef<ICryptoInUserWalletInfo, any>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
},
|
||||
{
|
||||
accessorKey: "value",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Value - USD
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "owned_amount",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Amount owned
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const payment = row.original;
|
||||
|
||||
return (
|
||||
<div className={"flex gap-2"}>
|
||||
<p className={"font-light italic text-xs"}>Soon here : Sell, History</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable columns={columns} data={wallet} fieldToFilter={"name"} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
55
src/components/footer.tsx
Normal file
55
src/components/footer.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Copyright } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer
|
||||
className={
|
||||
"flex flex-col-reverse md:flex-row justify-between gap-2 md:gap-1 self-end order-6 items-center p-2 border-t-2 w-full"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"flex flex-row gap-1 items-center justify-center md:justify-start md:w-1/3 opacity-50"
|
||||
}
|
||||
>
|
||||
<Copyright className={"w-4"} />
|
||||
<h4 className={"pr-2"}>Yidhra Studio</h4>
|
||||
<p>
|
||||
MIT <em>2024</em>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"flex flex-col flex-wrap md:flex-row max-h-24 md:flex-nowrap gap-1 md:gap-2 items-center justify-evenly w-full md:w-1/3"
|
||||
}
|
||||
>
|
||||
<Link
|
||||
href={"#"}
|
||||
className={"p-1 hover:-translate-y-1.5 hover:text-primary w-1/2"}
|
||||
>
|
||||
<h3 className={"text-nowrap text-center"}>Data privacy</h3>
|
||||
</Link>
|
||||
<Link
|
||||
href={"#"}
|
||||
className={"p-1 hover:-translate-y-1.5 hover:text-primary w-1/2"}
|
||||
>
|
||||
<h3 className={"text-nowrap text-center"}>Terms and conditions</h3>
|
||||
</Link>
|
||||
<Link
|
||||
href={"#"}
|
||||
className={"p-1 hover:-translate-y-1.5 hover:text-primary w-1/2"}
|
||||
>
|
||||
<h3 className={"text-nowrap text-center"}>Legal notice</h3>
|
||||
</Link>
|
||||
<Link
|
||||
href={"#"}
|
||||
className={"p-1 hover:-translate-y-1.5 hover:text-primary w-1/2"}
|
||||
>
|
||||
<h3 className={"text-nowrap text-center"}>Support Center</h3>
|
||||
</Link>
|
||||
</div>
|
||||
<div />
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
38
src/components/header.tsx
Normal file
38
src/components/header.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { AccountDialog } from "@/components/account-dialog";
|
||||
import { ThemeBtnSelector } from "@/components/theme-btn-selector";
|
||||
import Image from "next/image";
|
||||
import type React from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
export function Header({
|
||||
title,
|
||||
children,
|
||||
}: { title?: string; children?: React.ReactNode }) {
|
||||
return (
|
||||
<header
|
||||
className={
|
||||
"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} />
|
||||
<h1 className={"font-bold text-xl align-middle text-center text-wrap"}>
|
||||
{title || "Neptune"}
|
||||
</h1>
|
||||
</Link>
|
||||
<div
|
||||
className={"flex flex-col md:flex-row w-fit justify-center items-center"}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"w-1/3 flex flex-row justify-center md:justify-end md:w-fit gap-2 items-center"
|
||||
}
|
||||
>
|
||||
<AccountDialog />
|
||||
<ThemeBtnSelector />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
154
src/components/primary-nav.tsx
Normal file
154
src/components/primary-nav.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle,
|
||||
} from "@/components/ui/navigation-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Boxes, Info } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
const components: { title: string; href: string; description: string }[] = [
|
||||
{
|
||||
title: "Alert Dialog",
|
||||
href: "/docs/primitives/alert-dialog",
|
||||
description:
|
||||
"A modal dialog that interrupts the user with important content and expects a response.",
|
||||
},
|
||||
{
|
||||
title: "Hover Card",
|
||||
href: "/docs/primitives/hover-card",
|
||||
description: "For sighted users to preview content available behind a link.",
|
||||
},
|
||||
{
|
||||
title: "Progress",
|
||||
href: "/docs/primitives/progress",
|
||||
description:
|
||||
"Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.",
|
||||
},
|
||||
{
|
||||
title: "Scroll-area",
|
||||
href: "/docs/primitives/scroll-area",
|
||||
description: "Visually or semantically separates content.",
|
||||
},
|
||||
{
|
||||
title: "Tabs",
|
||||
href: "/docs/primitives/tabs",
|
||||
description:
|
||||
"A set of layered sections of content—known as tab panels—that are displayed one at a time.",
|
||||
},
|
||||
{
|
||||
title: "Tooltip",
|
||||
href: "/docs/primitives/tooltip",
|
||||
description:
|
||||
"A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.",
|
||||
},
|
||||
];
|
||||
|
||||
export function PrimaryNavigationMenu() {
|
||||
return (
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList className={"flex flex-row flex-wrap md:flex-nowrap"}>
|
||||
<NavigationMenuItem className={"relative"}>
|
||||
<NavigationMenuTrigger className={"gap-1"}>
|
||||
<Info className={"w-4"} />
|
||||
Getting started
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent className={""}>
|
||||
<ul className="grid gap-3 p-6 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]">
|
||||
<li className="row-span-3">
|
||||
<NavigationMenuLink asChild>
|
||||
<a
|
||||
className="flex h-full w-full select-none flex-col justify-end rounded-md bg-gradient-to-b from-muted/50 to-muted p-6 no-underline outline-none focus:shadow-md"
|
||||
href="/"
|
||||
>
|
||||
<Image
|
||||
src={"logo-red.svg"}
|
||||
alt={"Logo of Yidhra Studio"}
|
||||
width={64}
|
||||
height={64}
|
||||
/>
|
||||
<div className="mb-2 mt-4 text-lg font-medium">shadcn/ui</div>
|
||||
<p className="text-sm leading-tight text-muted-foreground">
|
||||
Beautifully designed components that you can copy and paste into
|
||||
your apps. Accessible. Customizable. Open Source.
|
||||
</p>
|
||||
</a>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
<ListItem href="/docs" title="Introduction">
|
||||
Re-usable components built using Radix UI and Tailwind CSS.
|
||||
</ListItem>
|
||||
<ListItem href="/docs/installation" title="Installation">
|
||||
How to install dependencies and structure your app.
|
||||
</ListItem>
|
||||
<ListItem href="/docs/primitives/typography" title="Typography">
|
||||
Styles for headings, paragraphs, lists...etc
|
||||
</ListItem>
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger className={"gap-1"}>
|
||||
<Boxes className={"w-4"} />
|
||||
Features
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
|
||||
{components.map((component) => (
|
||||
<ListItem
|
||||
key={component.title}
|
||||
title={component.title}
|
||||
href={component.href}
|
||||
>
|
||||
{component.description}
|
||||
</ListItem>
|
||||
))}
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<Link href="/docs" legacyBehavior passHref>
|
||||
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
|
||||
Documentation
|
||||
</NavigationMenuLink>
|
||||
</Link>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
);
|
||||
}
|
||||
|
||||
const ListItem = React.forwardRef<
|
||||
React.ElementRef<"a">,
|
||||
React.ComponentPropsWithoutRef<"a">
|
||||
>(({ className, title, children, ...props }, ref) => {
|
||||
return (
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<a
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="text-sm font-medium leading-none">{title}</div>
|
||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||
{children}
|
||||
</p>
|
||||
</a>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
ListItem.displayName = "ListItem";
|
||||
14
src/components/providers/providers.tsx
Normal file
14
src/components/providers/providers.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
import { Footer } from "@/components/footer";
|
||||
import { Header } from "@/components/header";
|
||||
import { ThemeProvider } from "@/components/providers/theme-provider";
|
||||
import { UserDataProvider } from "@/components/providers/userdata-provider";
|
||||
import type React from "react";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<UserDataProvider>{children}</UserDataProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
9
src/components/providers/theme-provider.tsx
Normal file
9
src/components/providers/theme-provider.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import type { ThemeProviderProps } from "next-themes/dist/types";
|
||||
import * as React from "react";
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
25
src/components/providers/userdata-provider.tsx
Normal file
25
src/components/providers/userdata-provider.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { IUserData } from "@/interfaces/userdata.interface";
|
||||
import { useEncodedLocalStorage } from "@/services/localStorage";
|
||||
import React from "react";
|
||||
|
||||
export interface IUserDataProvider {
|
||||
userData: IUserData | undefined;
|
||||
setUserData: React.Dispatch<React.SetStateAction<IUserData | undefined>>;
|
||||
}
|
||||
|
||||
export const UserDataContext = React.createContext<IUserDataProvider | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export const UserDataProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [userData, setUserData] = useEncodedLocalStorage<IUserData | undefined>(
|
||||
"user_data",
|
||||
undefined,
|
||||
);
|
||||
|
||||
return (
|
||||
<UserDataContext.Provider value={{ userData, setUserData }}>
|
||||
{children}
|
||||
</UserDataContext.Provider>
|
||||
);
|
||||
};
|
||||
34
src/components/theme-btn-selector.tsx
Normal file
34
src/components/theme-btn-selector.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { MoonStar, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import * as React from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function ThemeBtnSelector() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<MoonStar className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
92
src/interfaces/api.interface.ts
Normal file
92
src/interfaces/api.interface.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
type ICryptoInWalletInfo,
|
||||
ITrade,
|
||||
type IUserWalletCryptos,
|
||||
} from "@/interfaces/crypto.interface";
|
||||
import type { IUserData } from "@/interfaces/userdata.interface";
|
||||
|
||||
// ----- Request -----
|
||||
|
||||
export interface IApiRegisterReq {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
pseudo: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface IApiLoginReq {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface IApiTradeCreateRq {
|
||||
id_offer: string;
|
||||
}
|
||||
|
||||
export interface IApiOfferCreateReq {
|
||||
id_crypto: string;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface IApiCreateReferralCodeReq {
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface IApiDoTradeReq {
|
||||
id_offer: string;
|
||||
}
|
||||
|
||||
export interface IApiDoOfferReq {
|
||||
id_crypto: string;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
// ----- Response -----
|
||||
|
||||
export interface IAbstractApiResponse {
|
||||
message?: Array<string> | string;
|
||||
error?: string;
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
export interface IApiRegisterRes extends IAbstractApiResponse {
|
||||
access_token?: string;
|
||||
user?: IUserData;
|
||||
}
|
||||
|
||||
export interface IApiLoginRes extends IAbstractApiResponse {
|
||||
access_token?: string;
|
||||
}
|
||||
|
||||
export interface IApiUserAssetsRes extends IAbstractApiResponse {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
dollarAvailables?: number;
|
||||
pseudo?: string;
|
||||
UserHasCrypto?: IUserWalletCryptos[];
|
||||
}
|
||||
|
||||
export interface IApiAllTradesRes extends IAbstractApiResponse {}
|
||||
|
||||
export interface IAllRankRes extends IAbstractApiResponse {}
|
||||
|
||||
export interface IAllReferralCodeRes extends IAbstractApiResponse {}
|
||||
|
||||
export interface ICreateReferralCodeRes extends IAbstractApiResponse {}
|
||||
|
||||
export interface IReferralCodeUpdateRes extends IAbstractApiResponse {}
|
||||
|
||||
export interface IReferralCodeDeleteRes extends IAbstractApiResponse {}
|
||||
|
||||
export interface IApiAllOffersRes extends IAbstractApiResponse {
|
||||
id: string;
|
||||
User: {
|
||||
pseudo: string;
|
||||
};
|
||||
amount: number;
|
||||
created_at: string;
|
||||
id_user: string;
|
||||
Crypto: ICryptoInWalletInfo;
|
||||
}
|
||||
50
src/interfaces/crypto.interface.ts
Normal file
50
src/interfaces/crypto.interface.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
export interface IUserWalletCryptos {
|
||||
Crypto: ICryptoInWalletInfo;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface ICryptoInWalletInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
value: number;
|
||||
image: string;
|
||||
quantity: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ICryptoInUserWalletInfo extends ICryptoInWalletInfo {
|
||||
owned_amount: number;
|
||||
}
|
||||
|
||||
export type IAllTrades = ITrade[];
|
||||
|
||||
export interface ITrade {
|
||||
Giver: ISellerIdentity;
|
||||
Receiver: IBuyerIdentity;
|
||||
Crypto: ITradedCrypto;
|
||||
}
|
||||
|
||||
export interface ISellerIdentity {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
pseudo: string;
|
||||
dollarAvailables: number;
|
||||
}
|
||||
|
||||
export interface IBuyerIdentity {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
pseudo: string;
|
||||
dollarAvailables: number;
|
||||
}
|
||||
|
||||
export interface ITradedCrypto {
|
||||
id: string;
|
||||
name: string;
|
||||
value: number;
|
||||
image: string;
|
||||
quantity: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
13
src/interfaces/general.interface.ts
Normal file
13
src/interfaces/general.interface.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export interface IStandardisedReturn<T> {
|
||||
state: EReturnState;
|
||||
message?: string;
|
||||
resolved?: T;
|
||||
}
|
||||
|
||||
export enum EReturnState {
|
||||
unauthorized = 0,
|
||||
clientError = 1,
|
||||
serverError = 2,
|
||||
done = 3,
|
||||
queued = 4,
|
||||
}
|
||||
26
src/interfaces/userdata.interface.ts
Normal file
26
src/interfaces/userdata.interface.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
type ICryptoInUserWalletInfo,
|
||||
type ICryptoInWalletInfo,
|
||||
IUserWalletCryptos,
|
||||
} from "@/interfaces/crypto.interface";
|
||||
|
||||
export interface IUserData {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
pseudo: string;
|
||||
email: string;
|
||||
roleId: string;
|
||||
isActive: boolean;
|
||||
dollarAvailables: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
//TODO get on register
|
||||
wallet: IUserWallet;
|
||||
}
|
||||
|
||||
export interface IUserWallet {
|
||||
uat: number;
|
||||
update_interval: number;
|
||||
owned_cryptos: ICryptoInUserWalletInfo[];
|
||||
}
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
54
src/services/account.handler.ts
Normal file
54
src/services/account.handler.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import type {
|
||||
IAbstractApiResponse,
|
||||
IAllReferralCodeRes,
|
||||
IApiAllTradesRes,
|
||||
IApiDoTradeReq,
|
||||
IApiUserAssetsRes,
|
||||
ICreateReferralCodeRes,
|
||||
} from "@/interfaces/api.interface";
|
||||
import { ICryptoInWalletInfo, IUserWalletCryptos } from "@/interfaces/crypto.interface";
|
||||
import { EReturnState, type IStandardisedReturn } from "@/interfaces/general.interface";
|
||||
import type { IUserData, IUserWallet } from "@/interfaces/userdata.interface";
|
||||
import ApiRequest from "@/services/apiRequest";
|
||||
import { AxiosResponse } from "axios";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
//TODO Run disconnect task
|
||||
export function doDisconnect() {
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.removeItem("sub");
|
||||
//Redirect to homepage
|
||||
window.location.href = "/";
|
||||
return true;
|
||||
}
|
||||
console.log(
|
||||
"Whut ? Why trying to remove an item from the localStorage when running in SSR ?",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function getWallet(): Promise<IStandardisedReturn<IApiUserAssetsRes>> {
|
||||
try {
|
||||
const ReqRes =
|
||||
await ApiRequest.authenticated.get.json<IStandardisedReturn<IApiUserAssetsRes>>(
|
||||
"user/my-assets",
|
||||
);
|
||||
console.log(ReqRes.data);
|
||||
|
||||
if (ReqRes.status !== 200) {
|
||||
return {
|
||||
state: EReturnState.clientError,
|
||||
};
|
||||
}
|
||||
return {
|
||||
state: EReturnState.done,
|
||||
resolved: ReqRes.data,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
state: EReturnState.serverError,
|
||||
};
|
||||
}
|
||||
}
|
||||
83
src/services/apiRequest.ts
Normal file
83
src/services/apiRequest.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import axios, { type AxiosResponse } from "axios";
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3333/";
|
||||
|
||||
const AxiosConfigs = {
|
||||
authenticated: {
|
||||
json: () => {
|
||||
return {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${typeof window !== "undefined" ? JSON.parse(window.localStorage.getItem("sub") || "not-ssr") : "not-ssr"}`,
|
||||
},
|
||||
validateStatus: (status: number) => {
|
||||
return status < 500; // Resolve only if the status code is less than 500
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
standard: {
|
||||
json: () => {
|
||||
return {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
validateStatus: (status: number) => {
|
||||
return status < 500; // Resolve only if the status code is less than 500
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async function doAuthenticatedJsonPostReq<ReqT, ResT>(
|
||||
route: string,
|
||||
body: ReqT,
|
||||
): Promise<AxiosResponse<ResT>> {
|
||||
return await axios.post(baseUrl + route, body, AxiosConfigs.authenticated.json());
|
||||
}
|
||||
|
||||
async function doAuthenticatedGetReq<ResT>(route: string): Promise<AxiosResponse<ResT>> {
|
||||
return await axios.get(baseUrl + route, AxiosConfigs.authenticated.json());
|
||||
}
|
||||
|
||||
async function doAuthenticatedPatchReq<ReqT, ResT>(
|
||||
route: string,
|
||||
body: ReqT,
|
||||
): Promise<AxiosResponse<ResT>> {
|
||||
return await axios.patch(baseUrl + route, body, AxiosConfigs.authenticated.json());
|
||||
}
|
||||
|
||||
async function doAuthenticatedDelReq<ResT>(route: string): Promise<AxiosResponse<ResT>> {
|
||||
return await axios.delete(baseUrl + route, AxiosConfigs.authenticated.json());
|
||||
}
|
||||
|
||||
//TODO form/multipart req
|
||||
|
||||
async function doJsonPostReq<ReqT, ResT>(
|
||||
route: string,
|
||||
body: ReqT,
|
||||
): Promise<AxiosResponse<ResT>> {
|
||||
return await axios.post(baseUrl + route, body, AxiosConfigs.standard.json());
|
||||
}
|
||||
|
||||
async function doJsonGetReq<ResT>(route: string): Promise<AxiosResponse<ResT>> {
|
||||
return await axios.get(baseUrl + route, AxiosConfigs.standard.json());
|
||||
}
|
||||
|
||||
const ApiRequest = {
|
||||
authenticated: {
|
||||
post: { json: doAuthenticatedJsonPostReq },
|
||||
patch: { json: doAuthenticatedPatchReq },
|
||||
delete: { json: doAuthenticatedDelReq },
|
||||
get: { json: doAuthenticatedGetReq },
|
||||
},
|
||||
standard: {
|
||||
post: { json: doJsonPostReq },
|
||||
get: { json: doJsonGetReq },
|
||||
},
|
||||
};
|
||||
|
||||
export default ApiRequest;
|
||||
0
src/services/exchange.handler.ts
Normal file
0
src/services/exchange.handler.ts
Normal file
92
src/services/localStorage.ts
Normal file
92
src/services/localStorage.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import type React from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const localStorage = typeof window !== "undefined" ? window.localStorage : null;
|
||||
|
||||
/**
|
||||
* A custom React hook that allows you to store and retrieve data in the browser's localStorage.
|
||||
*
|
||||
* @param {string} key - The key under which the data should be stored in localStorage.
|
||||
* @param {T} initial - The initial value for the stored data.
|
||||
* @returns {[T, React.Dispatch<React.SetStateAction<T>>]} - An array containing the stored value and a function to update the stored value.
|
||||
*/
|
||||
export function useLocalStorage<T>(
|
||||
key: string,
|
||||
initial: T,
|
||||
): [T, React.Dispatch<React.SetStateAction<T>>] {
|
||||
const readValue = () => {
|
||||
const item = localStorage?.getItem(key);
|
||||
if (item) {
|
||||
try {
|
||||
return JSON.parse(item);
|
||||
} catch (error) {
|
||||
console.warn(`Error reading localStorage key “${key}”:`, error);
|
||||
}
|
||||
}
|
||||
return initial;
|
||||
};
|
||||
|
||||
const [storedValue, setStoredValue] = useState<T>(readValue);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage?.setItem(key, JSON.stringify(storedValue));
|
||||
} catch (error) {
|
||||
console.warn(`Error setting localStorage key “${key}”:`, error);
|
||||
}
|
||||
}, [key, storedValue]);
|
||||
return [storedValue, setStoredValue];
|
||||
}
|
||||
/**
|
||||
* Custom hook that provides a way to store and retrieve encoded values in local storage.
|
||||
*
|
||||
* @template T - The type of the value to be stored.
|
||||
*
|
||||
* @param {string} key - The key to be used for storing the encoded value in local storage.
|
||||
* @param {T} fallbackValue - The fallback value to be used if no value is found in local storage.
|
||||
*
|
||||
* @return {readonly [T, React.Dispatch<React.SetStateAction<T>>]} - An array containing the encoded value and a function to update the encoded value.
|
||||
*/
|
||||
export function useEncodedLocalStorage<T>(
|
||||
key: string,
|
||||
fallbackValue: T,
|
||||
): readonly [T, React.Dispatch<React.SetStateAction<T>>] {
|
||||
console.log("Pong !");
|
||||
const [encodedValue, setEncodedValue] = useState<T>(() => {
|
||||
const stored = localStorage?.getItem(key);
|
||||
return stored ? safelyParse(stored, fallbackValue) : fallbackValue;
|
||||
});
|
||||
|
||||
const prevValue = useRef(encodedValue);
|
||||
|
||||
useEffect(() => {
|
||||
console.log({ encodedValue });
|
||||
if (!b64ValEqual(prevValue.current, encodedValue)) {
|
||||
localStorage?.setItem(key, safelyStringify(encodedValue));
|
||||
}
|
||||
prevValue.current = encodedValue; // Set ref to current value
|
||||
}, [key, encodedValue]);
|
||||
return [encodedValue, setEncodedValue] as const;
|
||||
|
||||
function safelyParse(stored: string, fallback: T): T {
|
||||
try {
|
||||
return JSON.parse(atob(stored));
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function b64ValEqual<T>(v1: T, v2: T): boolean {
|
||||
return btoa(JSON.stringify(v1)) === btoa(JSON.stringify(v2));
|
||||
}
|
||||
|
||||
function safelyStringify(value: T): string {
|
||||
try {
|
||||
return btoa(JSON.stringify(value));
|
||||
} catch {
|
||||
return btoa(JSON.stringify(fallbackValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
95
tailwind.config.ts
Normal file
95
tailwind.config.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { Config } from "tailwindcss"
|
||||
|
||||
// @ts-ignore
|
||||
import {default as flattenColorPalette} from "tailwindcss/lib/util/flattenColorPalette";
|
||||
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate"), addVariablesForColors],
|
||||
} satisfies Config
|
||||
|
||||
// This plugin adds each Tailwind color as a global CSS variable, e.g. var(--gray-200).
|
||||
function addVariablesForColors({ addBase, theme }: any) {
|
||||
let allColors = flattenColorPalette(theme("colors"));
|
||||
let newVars = Object.fromEntries(
|
||||
Object.entries(allColors).map(([key, val]) => [`--${key}`, val])
|
||||
);
|
||||
|
||||
addBase({
|
||||
":root": newVars,
|
||||
});
|
||||
}
|
||||
|
||||
export default config
|
||||
400
temp.ts
Normal file
400
temp.ts
Normal file
@@ -0,0 +1,400 @@
|
||||
////////apiTypes.ts
|
||||
|
||||
|
||||
|
||||
export interface ResponseSuccess {
|
||||
|
||||
data: any
|
||||
|
||||
status: number
|
||||
|
||||
statusText: string
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface ResponseFailed {
|
||||
|
||||
code: string
|
||||
|
||||
message: string
|
||||
|
||||
name: string
|
||||
|
||||
response: {
|
||||
|
||||
data: {
|
||||
|
||||
error: string
|
||||
|
||||
message: string
|
||||
|
||||
statusCode: number
|
||||
|
||||
}
|
||||
|
||||
status: number
|
||||
|
||||
statusText: string
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
///////////////cryptoTypes.ts
|
||||
|
||||
|
||||
|
||||
export enum RoleName {
|
||||
|
||||
user = 'user',
|
||||
|
||||
admin = 'admin',
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type Role = {
|
||||
|
||||
id: string
|
||||
|
||||
name: RoleName
|
||||
|
||||
created_at?: string
|
||||
|
||||
updated_at?: string
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type PromoCode = {
|
||||
|
||||
id: string
|
||||
|
||||
name: string
|
||||
|
||||
value: number
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type CryptoHistory = {
|
||||
|
||||
id: string
|
||||
|
||||
id_crypto: string
|
||||
|
||||
value: number
|
||||
|
||||
created_at: string
|
||||
|
||||
updated_at: string
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface Offer {
|
||||
|
||||
id: string
|
||||
|
||||
User: {
|
||||
|
||||
pseudo: string
|
||||
|
||||
}
|
||||
|
||||
amount: number
|
||||
|
||||
created_at: string
|
||||
|
||||
id_user: string
|
||||
|
||||
Crypto: CryptoData
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface UserAssets {
|
||||
|
||||
firstName: string
|
||||
|
||||
lastName: string
|
||||
|
||||
dollarAvailables: number
|
||||
|
||||
pseudo: string
|
||||
|
||||
age: number
|
||||
|
||||
UserHasCrypto: CryptoData[]
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface Signin {
|
||||
|
||||
access_token: string
|
||||
|
||||
user: UserExtended
|
||||
|
||||
Role: Role
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface CryptoData {
|
||||
|
||||
id: string
|
||||
|
||||
name: string
|
||||
|
||||
value: number
|
||||
|
||||
image: string
|
||||
|
||||
quantity: number
|
||||
|
||||
created_at: string
|
||||
|
||||
updated_at: string
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface MyCryptoData {
|
||||
|
||||
Crypto: CryptoData
|
||||
|
||||
amount: number
|
||||
|
||||
id: string
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface Trade {
|
||||
|
||||
Giver: User
|
||||
|
||||
Receiver: User
|
||||
|
||||
Crypto: CryptoData
|
||||
|
||||
id: string
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface User {
|
||||
|
||||
firstName: string
|
||||
|
||||
lastName: string
|
||||
|
||||
pseudo: string
|
||||
|
||||
dollarAvailables: number
|
||||
|
||||
}
|
||||
|
||||
export interface UserHasCrypto {
|
||||
|
||||
id: string
|
||||
|
||||
id_user: string
|
||||
|
||||
id_crypto: string
|
||||
|
||||
amount: number
|
||||
|
||||
createdAt: string
|
||||
|
||||
updated_at: string
|
||||
|
||||
Crypto: CryptoData
|
||||
|
||||
}
|
||||
|
||||
export interface UserExtended extends User {
|
||||
|
||||
id: string
|
||||
|
||||
hash: string
|
||||
|
||||
email: string
|
||||
|
||||
roleId: string
|
||||
|
||||
isActive: boolean
|
||||
|
||||
city: string
|
||||
|
||||
age: number
|
||||
|
||||
created_at: string
|
||||
|
||||
updated_at: string
|
||||
|
||||
UserHasCrypto?: UserHasCrypto
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface MyTrade {
|
||||
|
||||
id: string
|
||||
|
||||
id_giver: string
|
||||
|
||||
id_receiver: string
|
||||
|
||||
id_crypto: string
|
||||
|
||||
amount_traded: number
|
||||
|
||||
created_at: string
|
||||
|
||||
updated_at: string
|
||||
|
||||
Crypto: CryptoData
|
||||
|
||||
Giver: UserExtended
|
||||
|
||||
Receiver: UserExtended
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface AuthData {
|
||||
|
||||
access_token: string
|
||||
|
||||
user: {
|
||||
|
||||
id: string
|
||||
|
||||
firstName: string
|
||||
|
||||
lastName: string
|
||||
|
||||
pseudo: string
|
||||
|
||||
hash: null | any
|
||||
|
||||
email: string
|
||||
|
||||
roleId: string
|
||||
|
||||
isActive: boolean
|
||||
|
||||
city: string
|
||||
|
||||
dollarAvailables: number
|
||||
|
||||
age: number
|
||||
|
||||
created_at: string
|
||||
|
||||
updated_at: string
|
||||
|
||||
UserHasCrypto: UserHasCrypto[]
|
||||
|
||||
Role: Role
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
////////////formTypes.ts
|
||||
|
||||
|
||||
|
||||
export type RegisterInput = {
|
||||
|
||||
firstName: string
|
||||
|
||||
lastName: string
|
||||
|
||||
pseudo: string
|
||||
|
||||
city: string
|
||||
|
||||
email: string
|
||||
|
||||
password: string
|
||||
|
||||
confirmPassword: string
|
||||
|
||||
promoCode: string
|
||||
|
||||
age: number
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type LoginInput = {
|
||||
|
||||
email: string
|
||||
|
||||
password: string
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type RoleInput = {
|
||||
|
||||
name: string
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type PromoCodeInput = {
|
||||
|
||||
name: string
|
||||
|
||||
value: number
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type TradeInput = {
|
||||
|
||||
id_offer: string
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type OfferInput = {
|
||||
|
||||
id_crypto: string
|
||||
|
||||
amount: number
|
||||
|
||||
}
|
||||
|
||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user