Add initial Neptune Frontend setup
Initial setup of the Neptune frontend project, including Dockerfile, environment files, TypeScript configuration, and essential components. Added basic page structures for dashboard and wallet, and configured Tailwind CSS and postcss.
This commit is contained in:
parent
442bebd022
commit
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"]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user