Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor UI and State Management for Checkbox and Button Handling #130

Merged
merged 8 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 61 additions & 57 deletions src/Angor/Client/Pages/Investor.razor
Original file line number Diff line number Diff line change
Expand Up @@ -140,70 +140,74 @@


<div class="card-body">
<div class="table-responsive form-control">
<table class="table align-items-center mb-0">
<div class="table-responsive form-control" style="overflow-x: hidden;">
<table class="table table-sm table-striped align-items-center mb-0" style="table-layout: fixed; width: 100%;">
<thead>
<tr>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7">Name</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7">Funding Target (@network.CoinTicker)</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7">Raised (@network.CoinTicker)</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7">Raised (% Target)</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7">Project Status</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7">My Investment (@network.CoinTicker)</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7">Spent by Founder</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7">Available to Founder</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7">In Recovery</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7">Founder Approval</th>
</tr>
<tr>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7" style="padding: 4px;">Name</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7 text-end" style="padding: 4px;">Funding Target (@network.CoinTicker)</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7 text-end" style="padding: 4px;">Raised (@network.CoinTicker)</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7 text-end" style="padding: 4px;">Raised (% Target)</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7" style="padding: 4px;">Project Status</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7 text-end" style="padding: 4px;">My Investment (@network.CoinTicker)</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7 text-end" style="padding: 4px;">Spent by Founder</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7 text-end" style="padding: 4px;">Available to Founder</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7 text-end" style="padding: 4px;">In Recovery</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7" style="padding: 4px;">Founder Approval</th>
</tr>

</thead>
<tbody>

@foreach (var project in projects)
{
Stats.TryGetValue(project.ProjectInfo.ProjectIdentifier, out var stats);
var nostrPubKey = project.ProjectInfo.NostrPubKey;
investmentRequestsMap.TryGetValue(nostrPubKey, out bool hasInvestmentRequests);

<tr>
<td>
<a href=@($"/view/{project.ProjectInfo.ProjectIdentifier}")>@project.Metadata?.Name</a>
</td>
<td>@project.ProjectInfo.TargetAmount @network.CoinTicker</td>
<td>@Money.Satoshis(stats?.AmountInvested ?? 0).ToUnit(MoneyUnit.BTC) @network.CoinTicker </td>
<td>@((stats?.AmountInvested ?? 0) * 100 / Money.Coins(project.ProjectInfo.TargetAmount).Satoshi) %</td>
<td>
@if (project.ProjectInfo.StartDate < DateTime.UtcNow)
{
<span class="text-info">Funding</span>
}
else
{
<span class="text-success">Live</span>
}
</td>
<td>
@Money.Satoshis(project.AmountInvested ?? 0).ToUnit(MoneyUnit.BTC) @network.CoinTicker
@if (!project.SignaturesInfo?.Signatures.Any() ?? false)
{
<a href=@($"/invest/{project.ProjectInfo.ProjectIdentifier}") class="btn btn-link" data-toggle="tooltip" title="Pending"> <i class="oi oi-clock"></i></a>
}
</td>
<td>-</td>
<td>-</td>
<td>@Money.Satoshis(project.AmountInRecovery ?? 0).ToUnit(MoneyUnit.BTC) @network.CoinTicker</td>
<td>
@if (hasInvestmentRequests)
{
<a href="@($"/invest/{project.ProjectInfo.ProjectIdentifier}")" class="text-info">Approved</a>
}

</td>
</tr>
}
@foreach (var project in projects)
{
Stats.TryGetValue(project.ProjectInfo.ProjectIdentifier, out var stats);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice your IDE will align lines incorrectly, this line of code should be pushed one tab to the right, I think its an IDE issue

var nostrPubKey = project.ProjectInfo.NostrPubKey;
investmentRequestsMap.TryGetValue(nostrPubKey, out bool hasInvestmentRequests);

<tr>
<td>
<a href=@($"/view/{project.ProjectInfo.ProjectIdentifier}") class="text-truncate">@project.Metadata?.Name</a>
</td>
<td class="text-end">@project.ProjectInfo.TargetAmount @network.CoinTicker</td>
<td class="text-end">@Money.Satoshis(stats?.AmountInvested ?? 0).ToUnit(MoneyUnit.BTC) @network.CoinTicker</td>
<td class="text-end">@((stats?.AmountInvested ?? 0) * 100 / Money.Coins(project.ProjectInfo.TargetAmount).Satoshi) %</td>
<td>
@if (project.ProjectInfo.StartDate < DateTime.UtcNow)
{
<span class="text-info">Funding</span>
}
else
{
<span class="text-success">Live</span>
}
</td>
<td class="text-end">
@Money.Satoshis(project.AmountInvested ?? 0).ToUnit(MoneyUnit.BTC) @network.CoinTicker
@if (!project.SignaturesInfo?.Signatures.Any() ?? false)
{
<a href=@($"/invest/{project.ProjectInfo.ProjectIdentifier}") class="btn btn-link p-0" data-toggle="tooltip" title="Pending"> <i class="oi oi-clock"></i></a>
}
</td>
<td class="text-end">-</td>
<td class="text-end">-</td>
<td class="text-end">@Money.Satoshis(project.AmountInRecovery ?? 0).ToUnit(MoneyUnit.BTC) @network.CoinTicker</td>
<td>
@if (hasInvestmentRequests)
{
<a href="@($"/invest/{project.ProjectInfo.ProjectIdentifier}")" class="text-info">Approved</a>
}
else
{
<span class="text-warning">Pending</span>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>

</div>

</div>
Expand Down
118 changes: 90 additions & 28 deletions src/Angor/Client/Pages/Spend.razor
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
@if (stageisActive)
{

<button class="btn @((noCoinsToClaim) ? "btn-light" : "btn-success")" disabled="@noCoinsToClaim" @onclick="() => ClaimCoinsCheckPassword(stage.StageIndex)">
<button class="btn @((noCoinsToClaim) ? "btn-light" : "btn-success")" disabled="@((noCoinsToClaim || !IsCheckboxSelectedForStage(stage.StageIndex)))" @onclick="() => ClaimCoinsCheckPassword(stage.StageIndex)">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be much simplified, when we expand another stage we clear the dictionary so there will only be items for a particular stage right? so you just need to check if the dictionary selectedUtxos has items in it, correct me if I am wrong.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noCoinsToClaim is determined by investedCount == 0 or stage.StagePinner == true.
there might be a scenario where selectedUtxos has values for stage 2. In this case, the claimCoins option for stage 1 won't be disabled.
providing the stageIndex and the relevant method, we can handle the logic for each stage appropriately.

@if (stage.StagePinner)
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Expand Down Expand Up @@ -122,22 +122,22 @@
<div class="mt-3">
@foreach (var transaction in stage.Items)
{
bool isTicked = selected.ContainsKey(transaction.Trxid);
string statusClass = transaction.IsSpent ? "bg-warning text-dark" : "bg-success text-light";
string statusText = transaction.IsSpent ? "Spent" : "Unspent";

<div class="d-flex mb-2">
<input id="@transaction.Trxid" type="checkbox" disabled="@(!stageisActive || transaction.IsSpent)" value="@isTicked" @onclick="() => HandleCheckboxChange(transaction.Trxid)" />
<label for="@transaction.Trxid">
@transaction.Amount @network.CoinTicker - utxo :
<span style="cursor: pointer; text-decoration: underline;" @onclick="() => CopyTRXToClipboard(transaction.Trxid)">@transaction.Trxid</span> -@transaction.Outputindex
<span class="p-1 rounded @statusClass">
@statusText
</span>
</label>
</div>

bool isTicked = IsUtxoSelected(transaction.Trxid, transaction.Outputindex);
string statusClass = transaction.IsSpent ? "bg-warning text-dark" : "bg-success text-light";
string statusText = transaction.IsSpent ? "Spent" : "Unspent";

<div class="d-flex mb-2">
<input id="@transaction.Trxid" type="checkbox" disabled="@(!stageisActive || transaction.IsSpent)" checked="@isTicked" @onclick="() => HandleCheckboxChange(transaction.Trxid, transaction.Outputindex)" />
<label for="@transaction.Trxid">
@transaction.Amount @network.CoinTicker - utxo :
<span style="cursor: pointer; text-decoration: underline;" @onclick="() => CopyTRXToClipboard(transaction.Trxid)">@transaction.Trxid</span> -@transaction.Outputindex
<span class="p-1 rounded @statusClass">
@statusText
</span>
</label>
</div>
}

</div>
}
</div>
Expand Down Expand Up @@ -174,11 +174,19 @@
<hr>

<h6 class="mt-3 mb-2">Stages</h6>
@foreach (var item in selected)
@foreach (var utxo in selectedUtxos)
{
<p style="font-size: 0.7em;" class="mb-1">@item.Key</p>
if (utxo.Value) // check if the UTXO is selected
{
var utxoKey = utxo.Key;

var stageIndex = GetStageIndexForUtxo(utxoKey);

<p style="font-size: 0.7em;" class="mb-1">Stage: @stageIndex, Transaction: @utxoKey.Trxid, Output Index: @utxoKey.Outputindex</p>
}
}


<hr>

<p class="mt-3">Are you sure you want to continue?</p>
Expand Down Expand Up @@ -255,7 +263,7 @@

List<(Transaction Transaction, string TrxId)> transactions = new();

Dictionary<string, string> selected = new();
Dictionary<UtxoKey, bool> selectedUtxos = new Dictionary<UtxoKey, bool>();

public class StageData
{
Expand Down Expand Up @@ -480,15 +488,14 @@
{
passwordComponent.ShowPassword(async () =>
{
await ClaimCoins(stageId); ;
await ClaimCoins(stageId);
});
}
}

private async Task ClaimCoins(int stageId)
{
var stage = stageDatas.First(s => s.StageIndex == stageId);

stage.StagePinner = true;

StateHasChanged();
Expand All @@ -507,13 +514,15 @@

founderContext = new FounderContext { ProjectInfo = project, ProjectSeeders = new ProjectSeeders() };

foreach (var item in selected)
foreach (var utxo in selectedUtxos.Where(kv => GetStageIndexForUtxo(kv.Key) == stageId && kv.Value))
{
var trx = transactions.First(f => f.TrxId == item.Key);
var trxId = utxo.Key.Trxid;
var trx = transactions.First(f => f.TrxId == trxId);

founderContext.InvestmentTrasnactionsHex.Add(trx.Transaction.ToHex(network.Consensus.ConsensusFactory));
}


var accountInfo = storage.GetAccountInfo(network.Name);
var address = accountInfo.GetNextReceiveAddress();
var addressScript = BitcoinWitPubKeyAddress.Create(address, network).ScriptPubKey;
Expand All @@ -533,12 +542,19 @@
}
finally
{
CalculateTotalValues();

if (selectedUtxos.Keys.Any(utxoKey => GetStageIndexForUtxo(utxoKey) == selectedStageId))
{
selectedUtxos.Clear();
}
stage.StagePinner = false;
}

StateHasChanged();
}


private async Task FeeRangeChanged(ChangeEventArgs e)
{
var selectedItem = e.Value?.ToString();
Expand Down Expand Up @@ -628,20 +644,39 @@
private void Expand(int stageId)
{
expandedStageId = expandedStageId == stageId ? null : stageId;
selectedUtxos.Clear();
}

private void HandleCheckboxChange(string trxId)
private void HandleCheckboxChange(string trxId, int outputIndex)
{
var key = new UtxoKey(trxId, outputIndex);

if (selected.ContainsKey(trxId))
if (selectedUtxos.ContainsKey(key))
{
selected.Remove(trxId);
selectedUtxos.Remove(key);
}
else
{
selected.Add(trxId, null);
selectedUtxos[key] = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will always be true, we wont have a case that the value is false right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing the key means it no longer exists in the dictionary, so there isn't a false value associated with it?
if so then i think you're right

}
}

private bool IsUtxoSelected(string trxId, int outputIndex)
{
return selectedUtxos.ContainsKey(new UtxoKey(trxId, outputIndex));
}

private int GetStageIndexForUtxo(UtxoKey utxoKey)
{
return stageDatas.First(stage => stage.Items.Any(item => item.Trxid == utxoKey.Trxid && item.Outputindex == utxoKey.Outputindex)).StageIndex;
}


private bool IsCheckboxSelectedForStage(int stageIndex)
{
return selectedUtxos.Any(utxo => GetStageIndexForUtxo(utxo.Key) == stageIndex && utxo.Value);
}


private async Task CopyTRXToClipboard(string trxData)
{
Expand All @@ -653,4 +688,31 @@
await _clipboardService.WriteTextAsync(trxData);
StateHasChanged();
}

public struct UtxoKey
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already such a type its called Outpoint use that instead

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will check it out 👍

{
public string Trxid { get; set; }
public int Outputindex { get; set; }

public UtxoKey(string trxid, int outputindex)
{
Trxid = trxid;
Outputindex = outputindex;
}

public override bool Equals(object obj)
{
if (!(obj is UtxoKey))
return false;

var key = (UtxoKey)obj;
return Trxid == key.Trxid && Outputindex == key.Outputindex;
}

public override int GetHashCode()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah I see you need to compile a hashcode for the dictionary unique key.
instead of using a new type it would be simpler to use a string of "trxid-index", but keep this as it is.

{
return HashCode.Combine(Trxid, Outputindex);
}
}

}
Loading