Skip to content
Snippets Groups Projects
Commit 20186026 authored by Dima Rifai's avatar Dima Rifai
Browse files

Issue 394 - Create the logic for balance history charts

parent b60681da
No related branches found
No related tags found
2 merge requests!507Delrifai/#394 2 add charts to balance history,!503Delrifai/#394 add charts to balance history
Pipeline #112238 canceled
This commit is part of merge request !507. Comments created here will be created in the context of that merge request.
import React, { useState, useMemo } from "react";
import { ArrowDown, ArrowUp } from "lucide-react";
import { Card, CardContent, CardHeader } from "../ui/card";
import Explorer from "@/types/Explorer";
import Link from "next/link";
import {
Tooltip,
TooltipProvider,
TooltipTrigger,
TooltipContent,
} from "@radix-ui/react-tooltip";
import useBalanceHistory from "@/hooks/api/balanceHistory/useBalanceHistory";
import BalanceHistoryChart from "../balanceHistory/BalanceHistoryChart";
import moment from "moment";
import { useRouter } from "next/router";
// Define the type for balance operation data
type AccountBalanceHistoryCardProps = {
header: string;
userDetails: Explorer.FormattedAccountDetails;
};
const AccountBalanceHistoryCard: React.FC<AccountBalanceHistoryCardProps> = ({
header,
userDetails,
}) => {
const [isBalancesHidden, setIsBalancesHidden] = useState(false);
const defaultFromDate = React.useMemo(
() => moment().subtract(1, "month").toDate(),
[]
);
const router = useRouter();
const accountNameFromRoute = (router.query.accountName as string)?.slice(1);
const {
accountBalanceHistory: hiveBalanceHistory,
isAccountBalanceHistoryLoading: hiveBalanceHistoryLoading,
isAccountBalanceHistoryError: hiveBalanceHistoryError,
} = useBalanceHistory(
accountNameFromRoute,
"HIVE",
undefined,
undefined,
"asc",
defaultFromDate
);
const {
accountBalanceHistory: vestsBalanceHistory,
isAccountBalanceHistoryLoading: vestsBalanceHistoryLoading,
isAccountBalanceHistoryError: vestsBalanceHistoryError,
} = useBalanceHistory(
accountNameFromRoute,
"VESTS",
undefined,
undefined,
"asc",
defaultFromDate
);
const {
accountBalanceHistory: hbdBalanceHistory,
isAccountBalanceHistoryLoading: hbdBalanceHistoryLoading,
isAccountBalanceHistoryError: hbdBalanceHistoryError,
} = useBalanceHistory(
accountNameFromRoute,
"HBD",
undefined,
undefined,
"asc",
defaultFromDate
);
const staticData = [
{ timestamp: "2023-01-01", balance: 100 },
{ timestamp: "2023-02-01", balance: 110 },
{ timestamp: "2023-03-01", balance: 120 },
];
const handleBalancesVisibility = () => {
setIsBalancesHidden(!isBalancesHidden);
};
return (
<Card data-testid="properties-dropdown" className="overflow-hidden pb-0">
<CardHeader className="p-0">
<div
onClick={handleBalancesVisibility}
className="flex justify-between items-center p-2 hover:bg-rowHover cursor-pointer px-4"
>
<div className="text-lg">{header}</div>
<div className="flex">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Link
href={`/balanceHistory/@${userDetails.name}`}
data-testid="balance-history-link"
className="text-link text-sm underline"
onClick={(e) => e.stopPropagation()}
>
<span>Details</span>
</Link>
</TooltipTrigger>
<TooltipContent
side="top"
align="start"
sideOffset={5}
alignOffset={10}
className="border-0"
>
<div className="bg-theme text-text p-2 text-sm">
<p>Click Here for Balance History</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<span>{isBalancesHidden ? <ArrowDown /> : <ArrowUp />}</span>
</div>
</div>
</CardHeader>
<CardContent hidden={isBalancesHidden} data-testid="balance-history-content">
<BalanceHistoryChart
hiveBalanceHistoryData={hiveBalanceHistory?.operations_result || []}
vestsBalanceHistoryData={vestsBalanceHistory?.operations_result || []}
hbdBalanceHistoryData={hbdBalanceHistory?.operations_result || []}
quickView={true}
className="h-[340px]"
/>
</CardContent>
</Card>
);
};
export default AccountBalanceHistoryCard;
......@@ -15,7 +15,7 @@ import AccountVestingDelegationsCard from "./AccountVestingDelegationsCard";
import AccountRcDelegationsCard from "./AccountRcDelegationsCard";
import AccountBalanceCard from "./AccountBalanceCard";
import Explorer from "@/types/Explorer";
import AccountBalanceHistoryCard from "./AccountBalanceHistoryCard";
interface AccountDetailsSectionProps {
accountName: string;
refetchAccountOperations: QueryObserverResult<Hive.AccountOperationsResponse>["refetch"];
......@@ -66,6 +66,11 @@ const AccountDetailsSection: React.FC<AccountDetailsSectionProps> = ({
header="Wallet"
userDetails={accountDetails}
/>
<AccountBalanceHistoryCard
header="Balance History"
userDetails={accountDetails}
/>
<AccountDetailsCard
header="Properties"
userDetails={accountDetails}
......
import React, { useState, useEffect } from "react";
import { Line, LineChart, CartesianGrid, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, Brush } from "recharts";
import { formatNumber } from "@/lib/utils";
import { cn } from "@/lib/utils";
import moment from "moment";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowDown,faArrowUp } from "@fortawesome/free-solid-svg-icons";
interface BalanceHistoryChartProps {
hiveBalanceHistoryData?: { timestamp: string; balance_change: number; balance: number }[];
vestsBalanceHistoryData?: { timestamp: string; balance_change: number; balance: number }[];
hbdBalanceHistoryData?: { timestamp: string; balance_change: number; balance: number }[];
className?: string;
quickView?: boolean;
}
const BalanceHistoryChart: React.FC<BalanceHistoryChartProps> = ({
hiveBalanceHistoryData,
vestsBalanceHistoryData,
hbdBalanceHistoryData,
className = "",
quickView = false,
}) => {
const availableCoins: string[] = [];
if (hiveBalanceHistoryData && hiveBalanceHistoryData.length > 0) availableCoins.push("HIVE");
if (vestsBalanceHistoryData && vestsBalanceHistoryData.length > 0) availableCoins.push("VESTS");
if (hbdBalanceHistoryData && hbdBalanceHistoryData.length > 0) availableCoins.push("HBD");
// Ensure we render a message only if there's no data available at all
/* if (availableCoins.length === 0) {
return (
<div className="w-full text-center py-8">
<p className="text-sm">No balance information found.</p>
</div>
);
}*/
const defaultSelectedCoinType = availableCoins.length === 1 ? availableCoins[0] : "HIVE";
const [selectedCoinType, setSelectedCoinType] = useState<string>(defaultSelectedCoinType);
const [isMobile, setIsMobile] = useState<boolean>(window.innerWidth < 480);
const [hiddenDataKeys, setHiddenDataKeys] = useState<string[]>([]);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 480);
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
/*useEffect(() => {
if (availableCoins.length === 1) {
setSelectedCoinType(availableCoins[0]);
} else if (availableCoins.length > 1 && !availableCoins.includes(selectedCoinType)) {
setSelectedCoinType(availableCoins[0]);
}
}, [availableCoins,selectedCoinType]);*/
const colorMap: Record<string, string> = {
"HIVE": "#8884d8",
"VESTS": "#82ca9d",
"HBD": "#ff7300",
};
const dataMap: Record<string, { timestamp: string; balance_change: number; balance: number }[]> = {
HIVE: hiveBalanceHistoryData || [],
VESTS: vestsBalanceHistoryData || [],
HBD: hbdBalanceHistoryData || [],
};
const handleCoinTypeChange = (coinType: string) => {
setSelectedCoinType(coinType);
};
const CustomTooltip = ({
active,
payload,
label,
}: {
active?: boolean;
payload?: any[];
label?: string;
}) => {
if (quickView || !active || !payload || payload.length === 0) return null;
// Get the actual balance from the data
const actualBalance = dataMap[selectedCoinType]?.find(
(item) => item.timestamp === label
)?.balance ?? 0;
const balanceChange = payload[0]?.payload.balance_change ?? 0;
// Determine if the balance change is positive or negative
const isPositiveChange = balanceChange > 0;
return (
<div className="bg-theme dark:bg-theme p-2 rounded border border-explorer-light-gray">
<p className="font-bold">{`Date: ${label}`}</p>
{payload.map((pld, index) => (
<div key={index} style={{ color: pld.stroke }}>
<div>
{/* Displaying arrow icon instead of balance change text */}
{isPositiveChange ? (
<FontAwesomeIcon
icon={faArrowUp}
size="sm"
className="bg-green-400 p-[1.2px]" // Green for positive
/>
) : (
<FontAwesomeIcon
icon={faArrowDown}
size="sm"
color="red"
className="bg-red-400 p-[1.2px]" // Red for negative
/>
)}
{` ${formatNumber(
balanceChange,
selectedCoinType === "VESTS",
false
)}`}
</div>
{/* Show the actual balance */}
<div>{`Balance: ${formatNumber(
actualBalance,
selectedCoinType === "VESTS",
false
)}`}</div>
</div>
))}
</div>
);
};
const renderCoinButtons = () => {
return availableCoins.map((coinType) => (
<button
key={coinType}
onClick={() => handleCoinTypeChange(coinType)}
className={cn(
"px-2 py-1 text-sm rounded m-[1px]",
selectedCoinType === coinType
? "bg-blue-500 text-white"
: "bg-gray-200 text-black hover:bg-gray-300 dark:bg-gray-600 dark:text-white hover:dark:bg-gray-500"
)}
>
{coinType}
</button>
));
};
const getMinMax = (data: { balance: number }[]) => {
const balance = data.map(item => item.balance);
const minValue = Math.min(...balance);
const maxValue = Math.max(...balance);
return [minValue, maxValue];
};
if (!selectedCoinType) {
return null;
}
const [minValue, maxValue] = getMinMax(dataMap[selectedCoinType]);
// Determine the interval based on the length of the data
const tickInterval = Math.ceil(dataMap[selectedCoinType].length / 10);
return (
<div className={cn("w-full", className)}>
{availableCoins.length > 1 && (
<div className="flex justify-end mb-4">{renderCoinButtons()}</div>
)}
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={dataMap[selectedCoinType] || []}
margin={{ top: 20, right: 30, left: 20, bottom: isMobile ? 100 : 60 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="timestamp"
tick={quickView ? false : true}
interval={tickInterval}
tickFormatter={(value) => moment(value).format("MMM D")} // Formatting dates to be more readable
style={{ fontSize: "10px" }}
angle={isMobile ? -90 : 0}
dx={isMobile ? -8 : 0}
dy={isMobile ? 20 : 10}
/>
<YAxis
domain={[minValue, maxValue]}
tickFormatter={(tick) => {
if (selectedCoinType === "VESTS") {
const valueInK = tick / 1000;
let formattedValue = formatNumber(valueInK, true, false);
formattedValue = formattedValue.split(".")[0];
return `${formattedValue} K`;
}
// For other coin types, use the usual formatting
return formatNumber(tick, selectedCoinType === "VESTS", false);
}}
style={{ fontSize: "10px" }}
tickCount={6}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="balance"
stroke={colorMap[selectedCoinType]}
activeDot={{ r: 6 }}
name={selectedCoinType}
dot={false}
hide={hiddenDataKeys.includes("balance")}
/>
<Legend
onClick={(event) => {
const { dataKey } = event;
const isHidden = hiddenDataKeys.includes(dataKey);
if (isHidden) {
setHiddenDataKeys(
hiddenDataKeys.filter((key) => key !== dataKey)
);
} else {
setHiddenDataKeys([...hiddenDataKeys, dataKey]);
}
}}
/>
{!quickView && (
<Brush
dataKey="timestamp"
height={30}
stroke="var(--color-switch-off)"
fill="var(--color-background)"
travellerWidth={10}
tickFormatter={(value) => moment(value).format("MMM D")}
y={380}
className="text-xs"
/>
)}
</LineChart>
</ResponsiveContainer>
</div>
);
};
export default BalanceHistoryChart;
\ No newline at end of file
......@@ -6,6 +6,8 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import useSearchRanges from "@/hooks/common/useSearchRanges";
import useURLParams from "@/hooks/common/useURLParams";
import OperationTypesDialog from "@/components/OperationTypesDialog";
import useAccountOperationTypes from "@/hooks/api/accountPage/useAccountOperationTypes";
interface AccountSearchParams {
accountName?: string | undefined;
......@@ -19,7 +21,7 @@ interface AccountSearchParams {
rangeSelectKey: string | undefined;
page: number | undefined;
filters: boolean[];
coinType?: string;
coinType?: string;
}
const defaultSearchParams: AccountSearchParams = {
......@@ -42,6 +44,30 @@ const BalanceHistorySearch = () => {
const COIN_TYPES = ["HIVE", "VESTS", "HBD"];
const router = useRouter();
const accountNameFromRoute = (router.query.accountName as string)?.slice(1);
const { accountOperationTypes } =
useAccountOperationTypes(accountNameFromRoute);
const [selectedOperationTypes, setSelectedOperationTypes] = useState<
number[]
>([]);
const [singleOperationTypeId, setSingleOperationTypeId] = useState<
number | undefined
>(undefined);
const [fieldContent, setFieldContent] = useState<string>("");
const [selectedKeys, setSelectedKeys] = useState<string[] | undefined>(
undefined
);
const [selectedIndex, setSelectedIndex] = useState<string>("");
const changeSelectedOperationTypes = (operationTypesIds: number[]) => {
if (operationTypesIds.length === 1) {
setSingleOperationTypeId(operationTypesIds[0]);
} else {
setSingleOperationTypeId(undefined);
}
setSelectedKeys(undefined);
setFieldContent("");
setSelectedOperationTypes(operationTypesIds);
};
const { paramsState, setParams } = useURLParams(
{
......@@ -65,7 +91,6 @@ const BalanceHistorySearch = () => {
const [initialSearch, setInitialSearch] = useState<boolean>(false);
const [filters, setFilters] = useState<boolean[]>([]);
const searchRanges = useSearchRanges();
......@@ -129,8 +154,11 @@ const BalanceHistorySearch = () => {
const handleCoinTypeChange = (newCoinType: string) => {
setCoinType(newCoinType);
setParams({ ...paramsState, coinType: newCoinType });
setParams({
...paramsState,
coinType: newCoinType,
page: undefined, // Reset the page when the coin type changes
});
};
const handleFilterClear = () => {
......@@ -144,48 +172,66 @@ const BalanceHistorySearch = () => {
setFilters([]);
};
useEffect(() => {
useEffect(() => {
if (paramsState.coinType) {
setCoinType(paramsState.coinType);
}
}
if (paramsState && !initialSearch) {
handleSearch();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [paramsState]);
return (
<>
<p className="m-2 mb-6 mt-6">
Find balance history of given account by coin and range.
</p>
<Card className="mb-4">
<CardHeader>
<CardTitle>Filters</CardTitle>
<CardTitle className="">Filters</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center mb-3">
<select
value={coinType}
onChange={(e) => handleCoinTypeChange(e.target.value)}
className="w-[180px] border border-gray-300 p-2 rounded bg-theme dark:bg-theme"
>
{COIN_TYPES.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
<select
value={coinType}
onChange={(e) => handleCoinTypeChange(e.target.value)}
className="w-[180px] border border-gray-300 p-2 rounded bg-theme dark:bg-theme"
>
{COIN_TYPES.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div>
<SearchRanges rangesProps={searchRanges} />
<div className="flex items-center justify-between m-2">
<Button onClick={() => handleSearch(true)} data-testid="apply-filters">
{/* Operations Types commented for now
<div className="flex items-center mb-10 mt-2">
<OperationTypesDialog
operationTypes={accountOperationTypes}
selectedOperations={selectedOperationTypes}
setSelectedOperations={/*changeSelectedOperationTypes}
buttonClassName="bg-gray-500"
triggerTitle={/*getOperationButtonTitle(
selectedOperationTypes,
accountOperationTypes
)}
/>
</div> */}
<div>
<Button
onClick={() => handleSearch(true)}
data-testid="apply-filters"
>
<span>Apply filters</span>
</Button>
<Button onClick={() => handleFilterClear()} data-testid="clear-filters">
<Button
onClick={() => handleFilterClear()}
data-testid="clear-filters"
className="ml-2"
>
<span>Clear filters</span>
</Button>
</div>
......
......@@ -20,7 +20,7 @@ const useBalanceHistory = (
return await fetchingService.geAccounttBalanceHistory(
accountName,
coinType,
page ? page : 1,
page,
pageSize,
direction,
fromDate ? fromDate : undefined,
......
......@@ -2,7 +2,8 @@ import { useRouter } from "next/router";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import React from "react";
import moment from "moment";
import { config } from "@/Config";
import { Loader2 } from "lucide-react";
......@@ -17,13 +18,66 @@ import { getHiveAvatarUrl } from "@/utils/HiveBlogUtils";
import BalanceHistoryTable from "@/components/balanceHistory/BalanceHistoryTable";
import BalanceHistorySearch from "@/components/home/searches/BalanceHistorySearch";
import { Card, CardHeader } from "@/components/ui/card";
import BalanceHistoryChart from "@/components/balanceHistory/BalanceHistoryChart";
interface Operation {
timestamp: number; // Timestamp in seconds
balance: number; // Balance associated with the operation
}
const prepareData = (operations: Operation[]) => {
if (!operations || operations.length === 0) return [];
// Create a map to store the balance and balance change for each day
const dailyData = new Map<string, { balance: number; balance_change: number }>();
operations.forEach((operation: any) => { // Adjusted the type to match the structure of the data you provided
let date;
if (typeof operation.timestamp === 'string') {
date = new Date(operation.timestamp);
} else if (typeof operation.timestamp === 'number') {
date = new Date(operation.timestamp * 1000);
} else {
return; // Skip this operation if the timestamp is invalid
}
if (!isNaN(date.getTime())) {
const dateString = date.toISOString().split('T')[0]; // Get date in YYYY-MM-DD format
let balance_change = parseInt(operation.balance_change, 10);
let balance = parseInt(operation.balance, 10);
// Update the map with the latest balance and balance change for the day
if (dailyData.has(dateString)) {
dailyData.get(dateString)!.balance_change += balance_change; // Accumulate balance changes for the same day
dailyData.get(dateString)!.balance = balance; // Update the balance for the day
} else {
dailyData.set(dateString, { balance, balance_change });
}
}
});
// Convert the map to an array of objects with the required fields
const preparedData = Array.from(dailyData.entries()).map(([date, data]) => ({
timestamp: date, // Use the date string directly
balance: data.balance,
balance_change: data.balance_change, // The sum of balance changes for the day
}));
return preparedData;
};
export default function BalanceHistory() {
const router = useRouter();
const accountNameFromRoute = (router.query.accountName as string)?.slice(1);
// Fetch account details
const { accountDetails, isAccountDetailsLoading, isAccountDetailsError ,notFound } = useAccountDetails(accountNameFromRoute, false);
const {
accountDetails,
isAccountDetailsLoading,
isAccountDetailsError,
notFound,
} = useAccountDetails(accountNameFromRoute, false);
interface BalanceHistorySearchParams {
accountName?: string;
......@@ -70,7 +124,8 @@ export default function BalanceHistory() {
page,
} = paramsState;
let effectiveFromBlock = paramsState.fromBlock || fromDateParam;
const defaultFromDate = React.useMemo(() => moment().subtract(1, "month").toDate(), []);
let effectiveFromBlock = paramsState.fromBlock || fromDateParam || defaultFromDate;
let effectiveToBlock = paramsState.toBlock || toDateParam;
if (
......@@ -95,13 +150,39 @@ export default function BalanceHistory() {
effectiveToBlock
);
const defaultChartSize = 6000;
const chartPageSize = (accountBalanceHistory?.total_operations !== undefined && accountBalanceHistory.total_operations !== 0 && accountBalanceHistory.total_operations < defaultChartSize)
? accountBalanceHistory.total_operations
: defaultChartSize;
const chartData = useBalanceHistory(
accountNameFromRoute,
paramsState.coinType,
undefined,
chartPageSize,
"asc",
effectiveFromBlock,
effectiveToBlock
);
const preparedData = chartData.accountBalanceHistory
? prepareData(chartData.accountBalanceHistory.operations_result)
: [];
// Determine the message to display based on the filters
let message = "";
if (effectiveFromBlock === defaultFromDate && !fromBlockParam && !toBlockParam) {
message = "Showing Results for the last month.";
} else {
message = "Showing Results with applied filters.";
}
return (
<>
<Head>
<title>@{accountNameFromRoute} - Hive Explorer</title>
</Head>
{/* Loading state for account details */}
{isAccountDetailsLoading ? (
<div className="flex justify-center text-center items-center">
<Loader2 className="animate-spin mt-1 text-black h-12 w-12 ml-3" />
......@@ -114,7 +195,6 @@ export default function BalanceHistory() {
<Card data-testid="account-details">
<CardHeader>
<div className="flex flex-wrap items-center justify-between gap-4 bg-theme dark:bg-theme">
{/* Avatar and Name */}
<div className="flex items-center gap-4">
<Image
className="rounded-full border-2 border-explorer-orange"
......@@ -125,15 +205,8 @@ export default function BalanceHistory() {
data-testid="user-avatar"
/>
<div>
<h2
className="text-lg font-semibold text-gray-800 dark:text-white"
data-testid="account-name"
>
<Link
className="text-link"
href={`/@${accountNameFromRoute}`}
>
{" "}
<h2 className="text-lg font-semibold text-gray-800 dark:text-white" data-testid="account-name">
<Link className="text-link" href={`/@${accountNameFromRoute}`}>
{accountNameFromRoute}
</Link>
/ <span className="text-text">Balance History</span>
......@@ -144,10 +217,8 @@ export default function BalanceHistory() {
</CardHeader>
</Card>
{/* Filter Options (Always visible) */}
<BalanceHistorySearch />
{/* Show Error Message if No Balance History and No Loading State */}
{!isAccountBalanceHistoryLoading && !accountBalanceHistory?.total_operations ? (
<div className="w-full my-4 text-center">
No operations were found.
......@@ -157,15 +228,34 @@ export default function BalanceHistory() {
<Loader2 className="animate-spin mt-1 h-12 w-12 ml-3" />
</div>
) : (
// Show the table when balance history exists
<BalanceHistoryTable
operations={convertBalanceHistoryResultsToTableOperations(
accountBalanceHistory
<>
<Card data-testid="account-details">
{/* Display the message */}
{message && (
<div className="p-4 bg-gray-100 dark:bg-gray-700 rounded-lg mb-4 text-center text-sm text-gray-500">
{message}<br/>
Results are limited to {defaultChartSize} records and grouped by day.<br/>
</div>
)}
total_operations={accountBalanceHistory.total_operations}
total_pages={accountBalanceHistory.total_pages}
current_page={paramsState.page}
/>
<BalanceHistoryChart
hiveBalanceHistoryData={(!paramsState.coinType || paramsState.coinType === "HIVE") ? preparedData : undefined}
vestsBalanceHistoryData={paramsState.coinType === "VESTS" ? preparedData : undefined}
hbdBalanceHistoryData={paramsState.coinType === "HBD" ? preparedData : undefined}
quickView={false}
className="h-[450px] mb-10 mr-0 pr-1 pb-6"
/>
</Card>
<BalanceHistoryTable
operations={convertBalanceHistoryResultsToTableOperations(accountBalanceHistory)}
total_operations={accountBalanceHistory.total_operations}
total_pages={accountBalanceHistory.total_pages}
current_page={paramsState.page}
/>
</>
)}
</div>
)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment