Skip to content

Commit

Permalink
enhance founder dashboard, with notifying on new investments
Browse files Browse the repository at this point in the history
  • Loading branch information
itailiors committed Dec 27, 2024
1 parent 7b76bd8 commit 7b10ad9
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 57 deletions.
38 changes: 22 additions & 16 deletions src/Angor/Client/Components/FounderProjectItem.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,26 @@
@using Angor.Client.Models
@using Angor.Client.Storage
@using System.Text.RegularExpressions

@using Blockcore.NBitcoin


@inject IRelayService RelayService;
@inject IClientStorage Storage;
@inject IHtmlStripperService HtmlStripperService;



<div class="col d-flex align-items-stretch">
<div class="card mt-4 w-100 project-card">
<a class="d-block">

<div class="banner-container">
<img class="banner-image" src="@(FounderProject?.Metadata?.Banner ?? "/assets/img/no-image.jpg")" alt="@(@FounderProject?.Metadata?.Banner != null ? "" : "no-image")" onerror="this.onerror=null; this.src='/assets/img/no-image.jpg';" />
<img class="banner-image" src="@(FounderProject?.Metadata?.Banner ?? "/assets/img/no-image.jpg")" alt="@(@FounderProject?.Metadata?.Banner != null ? "" : "no-image")" onerror="this.onerror=null; this.src='/assets/img/no-image.jpg';"/>
<div class="profile-container">
<img class="profile-image" src="@(FounderProject?.Metadata?.Picture ?? "/assets/img/no-image.jpg")" alt="@(FounderProject?.Metadata?.Banner != null ? "" : "no-image")" onerror="this.onerror=null; this.src='/assets/img/no-image.jpg';" />
<img class="profile-image" src="@(FounderProject?.Metadata?.Picture ?? "/assets/img/no-image.jpg")" alt="@(FounderProject?.Metadata?.Banner != null ? "" : "no-image")" onerror="this.onerror=null; this.src='/assets/img/no-image.jpg';"/>
</div>
</div>

</a>

<div class="card-body pb-0">



<div class="d-flex align-items-center mb-4">
<span class="user-select-none">
<Icon IconName="view" Height="24" Width="24"></Icon>
Expand All @@ -39,12 +34,25 @@
</div>
<p class="mb-0 line-clamp-3">@(ConvertToMarkupString(FounderProject.Metadata.About))</p>



<!-- Add Investment Stats Section -->
<div class="investment-stats mt-3">
<div class="d-flex align-items-center mb-2">
<Icon IconName="users"></Icon>
<p class="mb-0 ms-2">
<b>Total Investors:</b> @FounderProject.Stats.InvestorCount
</p>
</div>
<div class="d-flex align-items-center">
<Icon IconName="calculator"></Icon>
<p class="mb-0 ms-2">
<b>Total Raised:</b> @Money.Satoshis(FounderProject.Stats.AmountInvested).ToUnit(MoneyUnit.BTC) BTC
</p>
</div>
</div>
</div>

<div class="card-footer pt-0">
<hr class="horizontal light mt-3">

<a role="button" class="d-flex align-items-center btn btn-border w-100-m" href=@($"/view/{FounderProject.ProjectInfo.ProjectIdentifier}")>
<span class="user-select-none">
<Icon IconName="view-project" Height="24" Width="24"></Icon>
Expand All @@ -69,16 +77,14 @@
</div>
</a>
}

</div>
</div>
</div>


@code {

[Parameter]
public FounderProject FounderProject { get; set; }
[Parameter] public FounderProject FounderProject { get; set; }

public bool InvestmentRequests { get; set; }

Expand All @@ -95,11 +101,11 @@
});
}



public MarkupString ConvertToMarkupString(string input)
{
string sanitizedInput = HtmlStripperService.StripHtmlTags(input);
return new MarkupString(sanitizedInput);
}

}
2 changes: 2 additions & 0 deletions src/Angor/Client/Models/FounderProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ public bool NostrApplicationSpecificDataCreated()
{
return !string.IsNullOrEmpty(ProjectInfoEventId);
}
public ProjectStats Stats { get; set; } = new ProjectStats();

}
141 changes: 100 additions & 41 deletions src/Angor/Client/Pages/Founder.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@using Angor.Client.Storage
@using Angor.Shared.Models
@using Angor.Shared.Services
@using Blockcore.NBitcoin
@using Nostr.Client.Messages

@inject NavigationManager NavigationManager
Expand Down Expand Up @@ -38,7 +39,8 @@
<button
class="btn btn-border"
@onclick="NavigateToCreateProject"
disabled="@(scanningForProjects || (founderProjects.Count >= 14) ? true : null)"> <i>
disabled="@(scanningForProjects || (founderProjects.Count >= 14) ? true : null)">
<i>
<Icon IconName="add"></Icon>
</i>
<span class="nav-link-text ms-1">
Expand Down Expand Up @@ -108,6 +110,7 @@
if (hasWallet)
{
founderProjects = storage.GetFounderProjects().Where(_ => !string.IsNullOrEmpty(_.CreationTransactionId)).ToList();
await ScanForNewInvestmentsAsync();
}
}

Expand All @@ -125,7 +128,7 @@

var indexerProject = await _IndexerService.GetProjectByIdAsync(key.ProjectIdentifier);

if (indexerProject != null) //TODO we need to talk about supporting projects that are created with gaps
if (indexerProject != null) // TODO: Support projects with gaps
founderProjectsToLookup.Add(key.NostrPubKey, indexerProject);
}

Expand All @@ -138,48 +141,62 @@
RelayService.RequestProjectCreateEventsByPubKey(
e =>
{
switch (e)
// Run the async logic in a fire-and-forget task
_ = Task.Run(async () =>
{
case { Kind: NostrKind.Metadata }:
var nostrMetadata = serializer.Deserialize<ProjectMetadata>(e.Content);
var existingProject = founderProjects.FirstOrDefault(_ => _.ProjectInfo.NostrPubKey == e.Pubkey);

if (existingProject != null)
{
existingProject.Metadata ??= nostrMetadata;
}
else
{
var founderProject = CreateFounderProject(founderProjectsToLookup, e);
founderProject.Metadata = nostrMetadata;
founderProjects.Add(founderProject);
}

break;

case { Kind: NostrKind.ApplicationSpecificData }:

if (e.Id != founderProjectsToLookup[e.Pubkey].NostrEventId)
return;

var projectInfo = serializer.Deserialize<ProjectInfo>(e.Content);
var project = founderProjects.FirstOrDefault(_ => _.ProjectInfo.NostrPubKey == e.Pubkey);

if (project != null)
{
if (!string.IsNullOrEmpty(project.ProjectInfo.ProjectIdentifier))
switch (e)
{
case { Kind: NostrKind.Metadata }:
var nostrMetadata = serializer.Deserialize<ProjectMetadata>(e.Content);
var existingProject = founderProjects.FirstOrDefault(_ => _.ProjectInfo.NostrPubKey == e.Pubkey);

if (existingProject != null)
{
existingProject.Metadata ??= nostrMetadata;
}
else
{
var founderProject = CreateFounderProject(founderProjectsToLookup, e);
founderProject.Metadata = nostrMetadata;

// Initialize stats for the new project
founderProject.Stats = await _IndexerService.GetProjectStatsAsync(founderProject.ProjectInfo.ProjectIdentifier) ?? new ProjectStats();

founderProjects.Add(founderProject);
}

break;

case { Kind: NostrKind.ApplicationSpecificData }:
if (e.Id != founderProjectsToLookup[e.Pubkey].NostrEventId)
return;

project.ProjectInfo = projectInfo;
}
else
{
project ??= CreateFounderProject(founderProjectsToLookup, e, projectInfo);
founderProjects.Add(project);
}
var projectInfo = serializer.Deserialize<ProjectInfo>(e.Content);
var project = founderProjects.FirstOrDefault(_ => _.ProjectInfo.NostrPubKey == e.Pubkey);

break;
}
if (project != null)
{
if (!string.IsNullOrEmpty(project.ProjectInfo.ProjectIdentifier))
return;

project.ProjectInfo = projectInfo;

// Update stats for the existing project
project.Stats = await _IndexerService.GetProjectStatsAsync(project.ProjectInfo.ProjectIdentifier) ?? new ProjectStats();
}
else
{
project = CreateFounderProject(founderProjectsToLookup, e, projectInfo);

// Initialize stats for the new project
project.Stats = await _IndexerService.GetProjectStatsAsync(project.ProjectInfo.ProjectIdentifier) ?? new ProjectStats();

founderProjects.Add(project);
}

break;
}
});
},
() =>
{
Expand All @@ -204,7 +221,8 @@
founderProjectsToLookup.Keys.ToArray());
}

private FounderProject CreateFounderProject(Dictionary<string, ProjectIndexerData> founderProjectsToLookup,

private FounderProject CreateFounderProject(Dictionary<string, ProjectIndexerData> founderProjectsToLookup,
NostrEvent e, ProjectInfo? projectInfo = null)
{
var keys = _walletStorage.GetFounderKeys();
Expand All @@ -228,6 +246,7 @@

NavigationManager.NavigateTo("/create");
}

private string GetCreateButtonTooltip()
{
if (founderProjects.Count >= 15)
Expand All @@ -237,5 +256,45 @@
return "Create a new project.";
}

private async Task ScanForNewInvestmentsAsync()
{
try
{
foreach (var project in founderProjects)
{
if (project?.ProjectInfo == null)
continue;

// Fetch current stats from the IndexerService
var currentStats = await _IndexerService.GetProjectStatsAsync(project.ProjectInfo.ProjectIdentifier);

if (currentStats == null)
continue;

// Check for new investments
if (currentStats.InvestorCount > project.Stats.InvestorCount ||
currentStats.AmountInvested > project.Stats.AmountInvested)
{
// Update project stats
project.Stats.InvestorCount = (int)currentStats.InvestorCount;
project.Stats.AmountInvested = currentStats.AmountInvested;

// Notify the founder
notificationComponent.ShowNotificationMessage(
$"New investment detected in project '{project.ProjectInfo.ProjectIdentifier}': {currentStats.InvestorCount} investors, {Money.Satoshis(currentStats.AmountInvested).ToUnit(MoneyUnit.BTC)} BTC raised.", 5);

// Save updated project to storage
storage.UpdateFounderProject(project);
}
}

StateHasChanged();
}
catch (Exception ex)
{
Console.WriteLine($"Error scanning for investments: {ex.Message}");
}
}


}
17 changes: 17 additions & 0 deletions src/Angor/Client/Storage/ClientStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ namespace Angor.Client.Storage;
public class ClientStorage : IClientStorage, INetworkStorage
{
private const string CurrencyDisplaySettingKey = "currencyDisplaySetting";
private const string InvestmentStatsKeyPattern = "project:{0}:stats";


private const string utxoKey = "utxo:{0}";
private readonly ISyncLocalStorageService _storage;
Expand Down Expand Up @@ -222,6 +224,21 @@ public void DeleteSignatures()

_storage.RemoveItem("recovery-signatures");
}

public void SaveInvestmentStats(string projectId, ProjectStats stats)
{
_storage.SetItem(string.Format(InvestmentStatsKeyPattern, projectId), stats);
}

public void ClearInvestmentStats(string projectId)
{
_storage.RemoveItem(string.Format(InvestmentStatsKeyPattern, projectId));
}

public ProjectStats? GetInvestmentStats(string projectIdentifier)
{
var projects = GetFounderProjects();
return projects.FirstOrDefault(p => p.ProjectInfo.ProjectIdentifier == projectIdentifier)?.Stats;
}

}
4 changes: 4 additions & 0 deletions src/Angor/Client/Storage/IClientStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,9 @@ public interface IClientStorage

string GetCurrencyDisplaySetting();
void SetCurrencyDisplaySetting(string setting);

void SaveInvestmentStats(string projectId, ProjectStats stats);
ProjectStats? GetInvestmentStats(string projectId);
void ClearInvestmentStats(string projectId);
}
}

0 comments on commit 7b10ad9

Please sign in to comment.