Compare commits

..

2 Commits

Author SHA1 Message Date
0ead6bd969 refactor(interfaces): add Wallet related properties in User and Crypto interfaces
The change adds Wallet related properties in both User and Crypto interfaces. Specifically, `IUserWallet` interface has been added to `userdata.interface.ts` and 'amount' field has been added to `IUserWalletCryptos` in `crypto.interface.ts`. Also, an extra type `ICryptoInUserWalletInfo` extending `ICryptoInWalletInfo` has been added with `owned_amount` field.
2024-06-14 10:08:44 +02:00
747cc1cdb4 feat(ui-component): add new copy button component
This commit introduces a new CopyButton component in the ui components. It also includes a feature to copy multiple choices. The button changes its icon after copying to clipboard, indicating that the copying has been done.
2024-06-14 10:07:58 +02:00
7 changed files with 196 additions and 217 deletions

View File

@@ -4,13 +4,7 @@ import { AccountInfo } from "@/components/account-info";
import { UserDataContext } from "@/components/providers/userdata-provider"; import { UserDataContext } from "@/components/providers/userdata-provider";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import type { IUserData } from "@/interfaces/userdata.interface"; import type { IUserData } from "@/interfaces/userdata.interface";
import { import {Dispatch, SetStateAction, useContext, useEffect, useState} from "react";
type Dispatch,
type SetStateAction,
useContext,
useEffect,
useState,
} from "react";
export function AccountDialog() { export function AccountDialog() {
const userContext = useContext(UserDataContext); const userContext = useContext(UserDataContext);
@@ -18,44 +12,18 @@ export function AccountDialog() {
if (!userContext?.userData) { if (!userContext?.userData) {
userContext?.setUserData({ userContext?.setUserData({
firstName: "Mathis", age: 0,
lastName: "Herriot",
age: 23,
city: "Chambéry", city: "Chambéry",
created_at: "jaj", created_at: "jaj",
dollarAvailables: 34, dollarAvailables: 34,
email: "mherriot@tutanota.com", email: "mherriot@tutanota.com",
id: "098807e1-6681-407a-a2b4-e6b7e5ec1919", id: "",
isActive: true, isActive: false,
lastName: "Herriot",
pseudo: "Avnyr", pseudo: "Avnyr",
roleId: "092fb1eb-4ce4-4e3e-9df1-d00d467c6a43", roleId: "",
updated_at: "", updated_at: "",
wallet: { firstName: "Mathis",
uat: Date.now(),
update_interval: 30_000,
owned_cryptos: [
{
id: "1c237bcc-d2fa-4c4b-9a3a-f0efb4569e1e",
name: "Spectral",
value: 1.61051,
image:
"https://images.unsplash.com/photo-1640833906651-6bd1af7aeea3?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTh8fGNyeXB0b3xlbnwwfHwwfHx8MA%3D%3D",
quantity: 999,
created_at: "2024-06-08T22:42:14.113Z",
updated_at: "2024-06-11T12:48:07.001Z",
},
{
id: "6c0edc95-a968-4c2f-9169-33acfdc73575",
name: "SOL",
value: 30.91268053287073,
image:
"https://images.unsplash.com/photo-1523961131990-5ea7c61b2107?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Nnx8Y3J5cHRvfGVufDB8fDB8fHww",
quantity: 998,
created_at: "2024-06-07T09:30:31.282Z",
updated_at: "2024-06-08T23:44:54.242Z",
},
],
},
}); });
} }
@@ -71,12 +39,7 @@ export function AccountDialog() {
return ( return (
<div> <div>
<AccountInfo <AccountInfo userData={userContext?.userData as IUserData} setUserData={userContext?.setUserData as Dispatch<SetStateAction<IUserData | undefined>>} />
userData={userContext?.userData as IUserData}
setUserData={
userContext?.setUserData as Dispatch<SetStateAction<IUserData | undefined>>
}
/>
</div> </div>
); );
} }

View File

@@ -13,11 +13,8 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import type { IUserData } from "@/interfaces/userdata.interface"; import type { IUserData } from "@/interfaces/userdata.interface";
import { CopyButton } from "@/components/ui/copy-button"; import { Landmark, Unplug, User, Wallet } from "lucide-react";
import {Bitcoin, Fingerprint, Landmark, RefreshCw, Unplug, User, Wallet} from "lucide-react";
import type React from "react"; import type React from "react";
import {useState} from "react";
import {refreshUserData} from "@/services/account.handler";
export function AccountInfo({ export function AccountInfo({
userData, userData,
@@ -26,14 +23,6 @@ export function AccountInfo({
userData: IUserData; userData: IUserData;
setUserData: React.Dispatch<React.SetStateAction<IUserData | undefined>>; setUserData: React.Dispatch<React.SetStateAction<IUserData | undefined>>;
}) { }) {
const [refreshOngoing, setRefreshOngoing] = useState<boolean>(false)
function doingRefresh() {
setRefreshOngoing(true);
refreshUserData().then(()=>setRefreshOngoing(false))
}
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
@@ -44,59 +33,19 @@ export function AccountInfo({
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-[425px]"> <DialogContent className="sm:max-w-[425px]">
<DialogHeader> <DialogHeader>
<DialogTitle>{`${userData.firstName} ${userData.lastName}`}</DialogTitle> <DialogTitle>{`Your account - ${userData.firstName} ${userData.lastName}`}</DialogTitle>
<DialogDescription>Your personal account data.</DialogDescription> <DialogDescription>{userData.city}</DialogDescription>
</DialogHeader> </DialogHeader>
<div className={"flex flex-col items-center justify-center w-full"}> <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-row justify-evenly items-center"}>
<div <div className={"flex flex-row gap-1 justify-center items-center mx-auto"}>
className={ <Landmark />
"flex flex-col md:flex-row gap-2 justify-center md:justify-evenly items-start md:items-center w-full" <p>{userData.dollarAvailables} $</p>
}
>
<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 dont have cryptos.
</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></div>
</div> </div>
</div> </div>
<DialogFooter className={"flex justify-evenly items-center w-full gap-2"}> <DialogFooter>
{!refreshOngoing && <Button variant={"outline"} className={"gap-2 px-2"} onClick={()=>doingRefresh()}>
<RefreshCw />
</Button>}
<Button variant={"secondary"} className={"gap-2 px-2"}> <Button variant={"secondary"} className={"gap-2 px-2"}>
<Wallet /> <Wallet />
<p>My wallet</p> <p>My wallet</p>

View File

@@ -0,0 +1,112 @@
"use client";
import type { DropdownMenuTriggerProps } from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ClipboardIcon } from "lucide-react";
import { Button, type ButtonProps } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { useCallback, useEffect, useState } from "react";
interface CopyButtonProps extends ButtonProps {
value: string;
src?: string;
event?: Event["NONE"];
}
interface Value {
data: string;
title: string;
}
export async function copyToClipboardWithMeta(value: string) {
await window?.navigator.clipboard.writeText(value);
}
export function CopyButton({
value,
className,
src,
variant = "ghost",
event,
...props
}: CopyButtonProps) {
const [hasCopied, setHasCopied] = useState(false);
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
setTimeout(() => {
setHasCopied(false);
}, 2000);
}, [hasCopied]);
return (
<Button
size="icon"
variant={variant}
className={cn(
"relative z-10 h-6 w-6 text-zinc-50 hover:bg-zinc-700 hover:text-zinc-50 [&_svg]:size-3",
className,
)}
onClick={() => {
copyToClipboardWithMeta(value).then(() => setHasCopied(true));
}}
{...props}
>
<span className="sr-only">Copy</span>
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
</Button>
);
}
interface CopyMultipleChoiceButtonProps extends DropdownMenuTriggerProps {
values: Value[];
}
export function CopyMultipleChoiceButton({
values,
className,
...props
}: CopyMultipleChoiceButtonProps) {
const [hasCopied, setHasCopied] = useState(false);
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
setTimeout(() => {
setHasCopied(false);
}, 2000);
}, [hasCopied]);
const copyCommand = useCallback((value: string) => {
copyToClipboardWithMeta(value).then(() => setHasCopied(true));
}, []);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
variant="ghost"
className={cn(
"relative z-10 h-6 w-6 text-zinc-50 hover:bg-zinc-700 hover:text-zinc-50",
className,
)}
>
{hasCopied ? (
<CheckIcon className="h-3 w-3" />
) : (
<ClipboardIcon className="h-3 w-3" />
)}
<span className="sr-only">Copy</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => copyCommand("npm")}>npm</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -1,5 +1,6 @@
export interface IUserWalletCryptos { export interface IUserWalletCryptos {
Crypto?: ICryptoInWalletInfo; Crypto: ICryptoInWalletInfo;
amount: number;
} }
export interface ICryptoInWalletInfo { export interface ICryptoInWalletInfo {
@@ -12,6 +13,10 @@ export interface ICryptoInWalletInfo {
updated_at: string; updated_at: string;
} }
export interface ICryptoInUserWalletInfo extends ICryptoInWalletInfo {
owned_amount: number;
}
export type IAllTrades = ITrade[]; export type IAllTrades = ITrade[];
export interface ITrade { export interface ITrade {

View File

@@ -1,3 +1,9 @@
import {
ICryptoInUserWalletInfo,
type ICryptoInWalletInfo,
IUserWalletCryptos,
} from "@/interfaces/crypto.interface";
export interface IUserData { export interface IUserData {
id: string; id: string;
firstName: string; firstName: string;
@@ -11,4 +17,12 @@ export interface IUserData {
age: number; age: number;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
//TODO get on register
wallet: IUserWallet;
}
export interface IUserWallet {
uat: number;
update_interval: number;
owned_cryptos: ICryptoInUserWalletInfo[];
} }

View File

@@ -4,19 +4,18 @@ import type {
IApiLoginReq, IApiLoginReq,
IApiLoginRes, IApiLoginRes,
IApiRegisterReq, IApiRegisterReq,
IApiRegisterRes, IApiUserAssetsRes, IApiRegisterRes,
} from "@/interfaces/api.interface"; } from "@/interfaces/api.interface";
import type {IUserData, IUserWallet} from "@/interfaces/userdata.interface"; import type { IUserData } from "@/interfaces/userdata.interface";
import ApiRequest from "@/services/apiRequest"; import ApiRequest from "@/services/apiRequest";
import { useEncodedLocalStorage } from "@/services/localStorage"; import { useEncodedLocalStorage } from "@/services/localStorage";
import {createContext, type Dispatch, type SetStateAction, useContext, useEffect, useState} from "react"; import { createContext, useContext, useState } from "react";
import {type ICryptoInUserWalletInfo, ICryptoInWalletInfo} from "@/interfaces/crypto.interface";
const UserDataContext = createContext<IUserData | null>(null);
const [userData, setUserData] = useEncodedLocalStorage<IUserData | null>( const [userData, setUserData] = useEncodedLocalStorage<IUserData | null>(
"user_data", "user_data",
null, null,
); );
const UserDataContext = createContext<readonly [IUserData | null, Dispatch<SetStateAction<IUserData | null>>]>([userData, setUserData]);
//TODO Run register task //TODO Run register task
export async function doRegister( export async function doRegister(
@@ -32,10 +31,7 @@ export async function doRegister(
if (ReqRes.data.user) { if (ReqRes.data.user) {
setUserData(ReqRes.data.user); setUserData(ReqRes.data.user);
} }
// biome-ignore lint/complexity/noForEach: <explanation>
ReqRes.data.message?.forEach((err) => console.warn(err)); ReqRes.data.message?.forEach((err) => console.warn(err));
return ReqRes.data; return ReqRes.data;
} catch (error) { } catch (error) {
console.error("Error during registration:", error); console.error("Error during registration:", error);
@@ -54,8 +50,6 @@ export async function doLogin(loginData: IApiLoginReq) {
//if (ReqRes.data.user) { //if (ReqRes.data.user) {
// setUserData(ReqRes.data.user) // setUserData(ReqRes.data.user)
//} //}
// biome-ignore lint/complexity/noForEach: <explanation>
ReqRes.data.message?.forEach((err) => console.warn(err)); ReqRes.data.message?.forEach((err) => console.warn(err));
return ReqRes.data; return ReqRes.data;
} catch (err) { } catch (err) {
@@ -77,38 +71,3 @@ export function doDisconnect() {
} }
//TODO Run update user data //TODO Run update user data
export async function refreshUserData() {
if (!userData) return;
const isTokenAlive =typeof window !== "undefined" ? window.localStorage.getItem("sub") : null;
if (!isTokenAlive) return;
const _reqData = await ApiRequest.authenticated.get.json<IApiUserAssetsRes>("user/my-assets")
const old_userData = userData
// biome-ignore lint/style/useConst: <explanation>
let new_userData: IUserData = old_userData
let _owned_cryptos: ICryptoInUserWalletInfo[] = []
if (_reqData.data.UserHasCrypto) {
_owned_cryptos = _reqData.data.UserHasCrypto.map((el): ICryptoInUserWalletInfo =>{
return {
...el.Crypto,
owned_amount: el.amount
}
})
}
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
new_userData.wallet = {
uat: Date.now(),
update_interval: 30_000,
owned_cryptos: _owned_cryptos,
}
if (Date.now() >= old_userData.wallet.uat + (new_userData.wallet.update_interval / 1_000)) {
console.log("New update on userData.")
console.trace(new_userData)
setUserData(new_userData)
}
}

View File

@@ -39,77 +39,54 @@ export function useLocalStorage<T>(
}, [key, storedValue]); }, [key, storedValue]);
return [storedValue, setStoredValue]; return [storedValue, setStoredValue];
} }
/** /**
* Custom hook that provides an encoded version of a state value and a function to update that value. * Custom hook that provides a way to store and retrieve encoded values in local storage.
* *
* @template T - The type of the state value. * @template T - The type of the value to be stored.
* @param {string} key - The key under which the state value is stored in the local storage. *
* @param {T} initialValue - The initial value of the state. * @param {string} key - The key to be used for storing the encoded value in local storage.
* @return {[T, React.Dispatch<React.SetStateAction<T>>]} - A tuple containing the encoded state value and the function to update it. * @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, initialValue: T): readonly [T, React.Dispatch<React.SetStateAction<T>>] { export function useEncodedLocalStorage<T>(
const [encodedState, setEncodedState] = useState<T>(() => getFromLocalStorage(key, initialValue)); 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(() => { useEffect(() => {
saveToLocalStorage(key, encodedState); console.log({ encodedValue });
}, [encodedState, key]); if (!b64ValEqual(prevValue.current, encodedValue)) {
localStorage?.setItem(key, safelyStringify(encodedValue));
return [encodedState, setEncodedState] as const; }
} prevValue.current = encodedValue; // Set ref to current value
}, [key, encodedValue]);
/** return [encodedValue, setEncodedValue] as const;
* Retrieves a value from the localStorage with the given key.
* function safelyParse(stored: string, fallback: T): T {
* @param {string} key - The key to retrieve the value from. try {
* @param {T} initialValue - The initial value to be returned if the value does not exist in the localStorage. return JSON.parse(atob(stored));
* @returns {T} - The retrieved value from the localStorage or the initial value if it does not exist. } catch {
*/ return fallback;
function getFromLocalStorage<T>(key: string, initialValue: T): T { }
try { }
const localStorageValue = localStorage?.getItem(key);
if (localStorageValue) { function b64ValEqual<T>(v1: T, v2: T): boolean {
return decodeFromBase64(localStorageValue); 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));
} }
return initialValue;
} catch {
return initialValue;
} }
} }
/**
* Save a value to the local storage.
*
* @param {string} key - The key to store the value.
* @param {*} value - The value to be stored.
* @return {void}
*/
function saveToLocalStorage<T>(key: string, value: T): void {
try {
const encoded = encodeToBase64(value);
localStorage?.setItem(key, encoded);
} catch {}
}
/**
* Decodes a string value from Base64 encoding.
*
* @param {string} value - The Base64 encoded string to decode.
* @template T - The type of the decoded value.
* @returns {T} - The decoded value.
*/
function decodeFromBase64<T>(value: string) {
const decoded = atob(value);
return JSON.parse(decoded) as T;
}
/**
* Encodes the given value to Base64.
*
* @param {T} value - The value to be encoded.
* @returns {string} - The Base64 encoded string.
*/
function encodeToBase64<T>(value: T): string {
const serializedState = JSON.stringify(value);
return btoa(serializedState);
}