From 45d6d0ec4f113be526cd5e87b033601b3a208403 Mon Sep 17 00:00:00 2001 From: SuperJMN Date: Wed, 14 Jan 2026 11:34:49 +0100 Subject: [PATCH] Add "Fund" project type to Project Creation Wizard --- .../CreateProject/CreateProjectFlowV2.cs | 53 ++++-- .../FundReviewAndDeployViewModel.cs | 76 +++++++++ .../FundReviewAndDeployViewModelSample.cs | 33 ++++ .../Wizard/FundProject/GoalView.axaml | 156 +++++++++++++++++ .../Wizard/FundProject/GoalView.axaml.cs | 12 ++ .../Wizard/FundProject/GoalViewModel.cs | 75 +++++++++ .../Wizard/FundProject/GoalViewModelSample.cs | 22 +++ .../FundProject/Helpers/PayoutGenerator.cs | 53 ++++++ .../IFundReviewAndDeployViewModel.cs | 15 ++ .../Wizard/FundProject/IGoalViewModel.cs | 12 ++ .../Mappers/CreateProjectDtoMapper.cs | 77 +++++++++ .../FundProject/Model/FundProjectConfig.cs | 105 ++++++++++++ .../Model/FundProjectConfigSample.cs | 41 +++++ .../Model/FundProjectConfigSampleEmpty.cs | 28 +++ .../FundProject/Model/IFundProjectConfig.cs | 22 +++ .../Wizard/FundProject/Model/IPayoutConfig.cs | 11 ++ .../Wizard/FundProject/Model/PayoutConfig.cs | 30 ++++ .../FundProject/Model/PayoutFrequency.cs | 8 + .../FundProject/Payouts/IPayoutsViewModel.cs | 18 ++ .../FundProject/Payouts/PayoutsList.axaml | 51 ++++++ .../FundProject/Payouts/PayoutsList.axaml.cs | 48 ++++++ .../FundProject/Payouts/PayoutsView.axaml | 159 ++++++++++++++++++ .../FundProject/Payouts/PayoutsView.axaml.cs | 12 ++ .../FundProject/Payouts/PayoutsViewModel.cs | 94 +++++++++++ .../Payouts/PayoutsViewModelSample.cs | 36 ++++ .../FundProject/ReviewAndDeployView.axaml | 118 +++++++++++++ .../FundProject/ReviewAndDeployView.axaml.cs | 12 ++ .../CreateProject/Wizard/IProjectConfig.cs | 18 ++ .../CreateProject/Wizard/IProjectProfile.cs | 20 +++ .../FundingConfigurationView.axaml | 25 ++- .../FundingConfigurationViewModel.cs | 11 +- .../IReviewAndDeployViewModel.cs | 2 +- .../Mappers/CreateProjectDtoMapper.cs | 12 +- .../Model/IFundingStageConfig.cs | 3 +- .../Model/IInvestmentProjectConfig.cs | 18 +- .../Model/InvestmentProjectConfigBase.cs | 22 +-- .../Model/InvestmentProjectConfigSample.cs | 4 +- .../ProjectDeploymentOrchestrator.cs | 4 +- .../InvestmentProject/ProjectImagesView.axaml | 6 +- .../ProjectImagesViewModel.cs | 6 +- .../ProjectProfileViewModel.cs | 5 +- .../ReviewAndDeployView.axaml | 16 +- .../ReviewAndDeployViewModel.cs | 10 +- .../ReviewAndDeployViewModelSample.cs | 2 +- .../Stages/StagesSimpleEditor.axaml | 4 +- .../Wizard/ProjectTypeView.axaml | 8 +- .../Wizard/ProjectTypeViewModel.cs | 15 +- .../CreateProject/Wizard/WelcomeView.axaml | 2 +- .../UI/Sections/Home/HomeSectionView.axaml | 5 +- .../UI/Shared/Controls/AngorConverters.cs | 68 ++++++++ .../AngorApp/UI/Shared/Styles/Styles.axaml | 2 +- .../AngorApp/UI/Themes/New/Containers.axaml | 78 +++++++++ .../UI/Themes/New/Custom/ListBox.axaml | 6 +- .../AngorApp/UI/Themes/New/Icons.axaml | 3 + .../AngorApp/UI/Themes/New/NewTheme.axaml | 66 +++++++- .../UI/Themes/New/Templates/Showcase.axaml | 2 + 56 files changed, 1701 insertions(+), 119 deletions(-) create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/FundReviewAndDeployViewModel.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/FundReviewAndDeployViewModelSample.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/GoalView.axaml create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/GoalView.axaml.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/GoalViewModel.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/GoalViewModelSample.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Helpers/PayoutGenerator.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/IFundReviewAndDeployViewModel.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/IGoalViewModel.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Mappers/CreateProjectDtoMapper.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/FundProjectConfig.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/FundProjectConfigSample.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/FundProjectConfigSampleEmpty.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/IFundProjectConfig.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/IPayoutConfig.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/PayoutConfig.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/PayoutFrequency.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/IPayoutsViewModel.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsList.axaml create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsList.axaml.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsView.axaml create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsView.axaml.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsViewModel.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsViewModelSample.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/ReviewAndDeployView.axaml create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/ReviewAndDeployView.axaml.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/IProjectConfig.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/IProjectProfile.cs create mode 100644 src/Angor/Avalonia/AngorApp/UI/Themes/New/Containers.axaml diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/CreateProjectFlowV2.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/CreateProjectFlowV2.cs index cce41d94c..e62f76db0 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/CreateProjectFlowV2.cs +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/CreateProjectFlowV2.cs @@ -7,13 +7,16 @@ using AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject; using AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject.Model; using AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject.Stages; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Payouts; using AngorApp.UI.Shared.Controls.Common.Success; using Serilog; using Zafiro.Avalonia.Controls.Wizards.Slim; using Zafiro.UI.Navigation; using Zafiro.UI.Wizards.Slim; using Zafiro.UI.Wizards.Slim.Builder; -using ProjectType = AngorApp.UI.Flows.CreateProject.Wizard.ProjectType; +using Avalonia.Threading; namespace AngorApp.UI.Flows.CreateProject { @@ -41,7 +44,7 @@ private async Task>> Create(WalletId walletId, ProjectSeedD .StartWith(() => new WelcomeViewModel()).NextCommand(model => model.Start) .Then(_ => new ProjectTypeViewModel()) .NextCommand(vm => CreateProjectOftype( - vm.ProjectType, + vm, walletId, seed)) .Then(txId => new SuccessViewModel($"Project {txId} created successfully!"), "Success").Next((_, s) => s, "Finish").Always() @@ -51,20 +54,23 @@ private async Task>> Create(WalletId walletId, ProjectSeedD } private IEnhancedCommand> CreateProjectOftype( - ProjectType projectType, + ProjectTypeViewModel vm, WalletId walletId, ProjectSeedDto seed ) { - // TODO: We support only investment projects for now. That's why we ignore projectType. - return CreateInvestmentProject(walletId, seed); - } + var canExecute = vm.WhenAnyValue(x => x.ProjectType).Select(x => x != null); - private IEnhancedCommand> CreateInvestmentProject(WalletId walletId, ProjectSeedDto seed) - { - return ReactiveCommand - .CreateFromTask(() => CreateInvestmentProjectWizard(walletId, seed).Navigate(navigator).ToResult("Wizard was cancelled by user")) - .Enhance(); + return ReactiveCommand.CreateFromTask(async () => + { + var projectType = vm.ProjectType; + return await Dispatcher.UIThread.InvokeAsync(() => projectType.Name switch + { + "Investment" => CreateInvestmentProjectWizard(walletId, seed).Navigate(navigator).ToResult("Wizard was cancelled by user"), + "Fund" => CreateFundProjectWizard(walletId, seed).Navigate(navigator).ToResult("Wizard was cancelled by user"), + _ => throw new NotImplementedException($"Project type {projectType.Name} not implemented") + }); + }, canExecute).Enhance(); } private SlimWizard CreateInvestmentProjectWizard(WalletId walletId, ProjectSeedDto seed) @@ -93,6 +99,31 @@ private SlimWizard CreateInvestmentProjectWizard(WalletId walletId, Proj return wizard; } + private SlimWizard CreateFundProjectWizard(WalletId walletId, ProjectSeedDto seed) + { + var newProject = new FundProjectConfig(); + + SlimWizard wizard = WizardBuilder + .StartWith(() => new ProjectProfileViewModel(newProject)).NextUnit().WhenValid() + .Then(_ => new ProjectImagesViewModel(newProject)).NextUnit().Always() + .Then(_ => new GoalViewModel(newProject)).NextUnit().WhenValid() + .Then(_ => new FundPayoutsViewModel(newProject)).NextUnit().WhenValid() + .Then(_ => new FundReviewAndDeployViewModel( + newProject, + new ProjectDeploymentOrchestrator( + projectAppService, + founderAppService, + uiServices, + logger), + walletId, + seed, + uiServices)) + .NextCommand(review => review.DeployCommand) + .WithCommitFinalStep(); + + return wizard; + } + private async Task> GetProjectSeed(WalletId walletId) { var result = await founderAppService.CreateProjectKeys(new CreateProjectKeys.CreateProjectKeysRequest(walletId)); diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/FundReviewAndDeployViewModel.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/FundReviewAndDeployViewModel.cs new file mode 100644 index 000000000..d76204344 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/FundReviewAndDeployViewModel.cs @@ -0,0 +1,76 @@ +using System.Reactive.Disposables; +using Angor.Sdk.Common; +using Angor.Sdk.Funding.Founder.Dtos; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Mappers; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model; +using AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject; +using System.Collections.ObjectModel; +using DynamicData; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Helpers; +using System.Linq; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject +{ + public class FundReviewAndDeployViewModel : IFundReviewAndDeployViewModel, IDisposable + { + private readonly ProjectDeploymentOrchestrator orchestrator; + private readonly WalletId walletId; + private readonly ProjectSeedDto projectSeed; + private readonly CompositeDisposable disposables = new(); + + public IFundProjectConfig NewProject { get; } + public IEnhancedCommand> DeployCommand { get; } + + public FundReviewAndDeployViewModel( + IFundProjectConfig newProject, + ProjectDeploymentOrchestrator orchestrator, + WalletId walletId, + ProjectSeedDto projectSeed, + UIServices uiServices) + { + NewProject = newProject; + this.orchestrator = orchestrator; + this.walletId = walletId; + this.projectSeed = projectSeed; + + DeployCommand = ReactiveCommand.CreateFromTask(Deploy).Enhance("Deploy").DisposeWith(disposables); + DeployCommand.HandleErrorsWith(uiServices.NotificationService, "Failed to deploy project").DisposeWith(disposables); + + + var payoutsSource = new SourceList(); + payoutsSource.Connect().Bind(out var payouts).Subscribe().DisposeWith(disposables); + Payouts = payouts; + + if (newProject.PayoutFrequency != null) + { + var maxInstallments = newProject.SelectedInstallments.SelectedItems.DefaultIfEmpty(0).Max(); + if (maxInstallments > 0) + { + var generated = PayoutGenerator.Generate( + newProject.PayoutFrequency.Value, + maxInstallments, + DateTime.Now, + newProject.MonthlyPayoutDate, + newProject.WeeklyPayoutDay + ); + payoutsSource.AddRange(generated); + } + } + } + + public ReadOnlyObservableCollection Payouts { get; } + + private async Task> Deploy() + { + var dto = NewProject.ToDto(); + return await orchestrator.Deploy(walletId, dto, projectSeed); + } + + public IObservable Title => Observable.Return("Review & Deploy"); + + public void Dispose() + { + disposables.Dispose(); + } + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/FundReviewAndDeployViewModelSample.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/FundReviewAndDeployViewModelSample.cs new file mode 100644 index 000000000..dd566c2c1 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/FundReviewAndDeployViewModelSample.cs @@ -0,0 +1,33 @@ +using System.Collections.ObjectModel; +using Angor.Sdk.Common; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject; + + +public class FundReviewAndDeployViewModelSample : IFundReviewAndDeployViewModel +{ + public FundReviewAndDeployViewModelSample() + { + NewProject = new FundProjectConfigSample(); + + + var samplePayouts = new ObservableCollection + { + new PayoutConfigSample { Percent = 0.25m, PayoutDate = DateTime.Now.AddMonths(1) }, + new PayoutConfigSample { Percent = 0.25m, PayoutDate = DateTime.Now.AddMonths(2) }, + new PayoutConfigSample { Percent = 0.25m, PayoutDate = DateTime.Now.AddMonths(3) }, + new PayoutConfigSample { Percent = 0.25m, PayoutDate = DateTime.Now.AddMonths(4) } + }; + + Payouts = new ReadOnlyObservableCollection(samplePayouts); + } + + public IEnhancedCommand> DeployCommand { get; } = null!; + + public IFundProjectConfig NewProject { get; } + + public ReadOnlyObservableCollection Payouts { get; } + + public IObservable Title => Observable.Return("Review & Deploy"); +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/GoalView.axaml b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/GoalView.axaml new file mode 100644 index 000000000..1588e2c7c --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/GoalView.axaml @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + Configure your funding goal and project nature. + + + + + + + + + + + + + + + Goal (BTC) * + + + + + + BTC + + + + + + The amount you want to get funded + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BTC + + + + + + Supporters who contribute over + + will require approval by you + + + + + + + + \ No newline at end of file diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/GoalView.axaml.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/GoalView.axaml.cs new file mode 100644 index 000000000..7b1036ab0 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/GoalView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject +{ + public partial class GoalView : UserControl + { + public GoalView() + { + InitializeComponent(); + } + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/GoalViewModel.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/GoalViewModel.cs new file mode 100644 index 000000000..2f116117c --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/GoalViewModel.cs @@ -0,0 +1,75 @@ +using System.Linq; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model; +using AngorApp.UI.Shared; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject +{ + public class GoalViewModel : ReactiveObject, IHaveTitle, IGoalViewModel, IValidatable + { + + private const decimal DefaultThresholdBtc = 0.001m; + private const long SatsPerBtc = 100_000_000; + + public IFundProjectConfig FundProject { get; } + + public GoalViewModel(IFundProjectConfig fundProject) + { + FundProject = fundProject; + + if (FundProject.GoalAmount == null) + { + var defaultPreset = AmountPresets.FirstOrDefault(); + if (defaultPreset != null) + { + FundProject.GoalAmount = new AmountUI(defaultPreset.Sats); + } + } + + + if (FundProject.Threshold == null) + { + FundProject.Threshold = new AmountUI((long)(DefaultThresholdBtc * SatsPerBtc)); + } + } + + + public long? SelectedPresetSats + { + get => FundProject.GoalAmount?.Sats; + set + { + if (value.HasValue) + { + FundProject.GoalAmount = new AmountUI(value.Value); + } + } + } + + + public decimal? ThresholdBtc + { + get => FundProject.Threshold?.Btc; + set + { + var btc = value ?? DefaultThresholdBtc; + var sats = (long)(btc * SatsPerBtc); + FundProject.Threshold = new AmountUI(sats); + this.RaisePropertyChanged(); + } + } + + public IObservable Title => Observable.Return("Goal"); + + public IEnumerable AmountPresets { get; } = + [ + AmountUI.FromBtc(0.25), + AmountUI.FromBtc(0.5), + AmountUI.FromBtc(1), + AmountUI.FromBtc(2.5), + ]; + + public IObservable IsValid => this.FundProject.WhenValid( + x => x.GoalAmount + ); + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/GoalViewModelSample.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/GoalViewModelSample.cs new file mode 100644 index 000000000..9e8b9d42f --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/GoalViewModelSample.cs @@ -0,0 +1,22 @@ +using AngorApp.Model.Amounts; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject +{ + public class GoalViewModelSample : IGoalViewModel + { + public IFundProjectConfig FundProject { get; } = new FundProjectConfigSample(); + public long? SelectedPresetSats { get; set; } = AmountUI.FromBtc(1).Sats; + public IEnumerable AmountPresets { get; } = + [ + AmountUI.FromBtc(0.25), + AmountUI.FromBtc(0.5), + AmountUI.FromBtc(1), + AmountUI.FromBtc(2.5), + ]; + + public IObservable Title => Observable.Return("Goal"); + public IObservable IsValid => Observable.Return(true); + public decimal? ThresholdBtc { get; set; } = 0.001m; + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Helpers/PayoutGenerator.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Helpers/PayoutGenerator.cs new file mode 100644 index 000000000..ceb84f07b --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Helpers/PayoutGenerator.cs @@ -0,0 +1,53 @@ +using System.Linq; +using Angor.Shared.Models; +using Angor.Shared.Utilities; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Helpers; + +public static class PayoutGenerator +{ + public static IEnumerable Generate( + PayoutFrequency frequency, + int installmentCount, + DateTime startDate, + int? monthlyPayoutDay, + DayOfWeek? weeklyPayoutDay) + { + StageFrequency stageFrequency; + PayoutDayType payoutDayType; + int payoutDay = 0; + + if (frequency == PayoutFrequency.Monthly) + { + stageFrequency = StageFrequency.Monthly; + payoutDayType = PayoutDayType.SpecificDayOfMonth; + payoutDay = monthlyPayoutDay ?? 1; + } + else + { + stageFrequency = StageFrequency.Weekly; + payoutDayType = PayoutDayType.SpecificDayOfWeek; + payoutDay = (int)(weeklyPayoutDay ?? DayOfWeek.Monday); + } + + var pattern = new DynamicStagePattern + { + Frequency = stageFrequency, + PayoutDayType = payoutDayType, + PayoutDay = payoutDay + }; + + var percent = 1m / installmentCount; + + return Enumerable.Range(0, installmentCount).Select(i => + { + var date = DynamicStageCalculator.CalculateDynamicStageReleaseDate(startDate, pattern, i); + return new PayoutConfig + { + Percent = percent, + PayoutDate = date + }; + }); + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/IFundReviewAndDeployViewModel.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/IFundReviewAndDeployViewModel.cs new file mode 100644 index 000000000..024f73362 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/IFundReviewAndDeployViewModel.cs @@ -0,0 +1,15 @@ +using Angor.Sdk.Common; +using AngorApp.UI.Shared; +using System.Collections.ObjectModel; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject +{ + + public interface IFundReviewAndDeployViewModel : IHaveTitle + { + IEnhancedCommand> DeployCommand { get; } + IFundProjectConfig NewProject { get; } + ReadOnlyObservableCollection Payouts { get; } + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/IGoalViewModel.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/IGoalViewModel.cs new file mode 100644 index 000000000..6d52c776c --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/IGoalViewModel.cs @@ -0,0 +1,12 @@ +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject +{ + public interface IGoalViewModel : IHaveTitle, IValidatable + { + IFundProjectConfig FundProject { get; } + long? SelectedPresetSats { get; set; } + IEnumerable AmountPresets { get; } + decimal? ThresholdBtc { get; set; } + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Mappers/CreateProjectDtoMapper.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Mappers/CreateProjectDtoMapper.cs new file mode 100644 index 000000000..779f61d00 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Mappers/CreateProjectDtoMapper.cs @@ -0,0 +1,77 @@ +using System.Linq; +using Angor.Sdk.Funding.Projects.Domain; +using Angor.Sdk.Funding.Projects.Dtos; +using Angor.Shared.Models; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Mappers; + +public static class CreateProjectDtoMapper +{ + public static CreateProjectDto ToDto(this IFundProjectConfig fundProject) + { + var satsValue = fundProject.GoalAmount?.Sats ?? 0; + + return new CreateProjectDto + { + + ProjectName = fundProject.Name, + Description = fundProject.Description, + WebsiteUri = fundProject.Website, + AvatarUri = fundProject.AvatarUri, + BannerUri = fundProject.BannerUri, + Nip05 = fundProject.Nip05, + Lud16 = fundProject.Lud16, + Nip57 = fundProject.Nip57, + + + ProjectType = Angor.Shared.Models.ProjectType.Fund, + Sats = satsValue, + StartDate = DateTime.Now, + + EndDate = null, + + TargetAmount = new Amount(satsValue), + PenaltyDays = 0, + + + + Stages = Enumerable.Empty(), + SelectedPatterns = CreateDynamicStagePatterns( + fundProject.PayoutFrequency, + fundProject.SelectedInstallments.SelectedItems + ), + PayoutDay = fundProject.MonthlyPayoutDate + }; + } + + private static List? CreateDynamicStagePatterns( + PayoutFrequency? frequency, + IEnumerable installmentCounts) + { + if (!frequency.HasValue || installmentCounts == null || !installmentCounts.Any()) + { + return null; + } + + var stageFrequency = frequency.Value == PayoutFrequency.Monthly + ? StageFrequency.Monthly + : StageFrequency.Weekly; + + var standardPatterns = DynamicStagePattern.GetStandardPatterns(); + + return installmentCounts.Select(count => + { + var matchingStandard = standardPatterns.FirstOrDefault(p => p.Frequency == stageFrequency && p.StageCount == count); + + return new DynamicStagePattern + { + Name = matchingStandard?.Name ?? $"{frequency.Value} {count} Installments", + StageCount = count, + Frequency = stageFrequency, + PatternId = matchingStandard?.PatternId ?? 0 + + }; + }).ToList(); + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/FundProjectConfig.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/FundProjectConfig.cs new file mode 100644 index 000000000..7814fbe6d --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/FundProjectConfig.cs @@ -0,0 +1,105 @@ +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia.Controls.Selection; +using DynamicData; +using ReactiveUI.Validation.Extensions; +using ReactiveUI.Validation.Helpers; +using ReactiveUI; +using Zafiro.Avalonia.Misc; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model +{ + public partial class FundProjectConfig : ReactiveValidationObject, IFundProjectConfig, IDisposable + { + protected readonly CompositeDisposable Disposables = new(); + + [Reactive] private string name = string.Empty; + [Reactive] private string description = string.Empty; + [Reactive] private string website = string.Empty; + + + [Reactive] private IAmountUI? goalAmount; + [Reactive] private IAmountUI? threshold; + + [Reactive] private string avatarUri = string.Empty; + [Reactive] private string bannerUri = string.Empty; + [Reactive] private string nip05 = string.Empty; + [Reactive] private string lud16 = string.Empty; + [Reactive] private string nip57 = string.Empty; + + + [Reactive] private PayoutFrequency? payoutFrequency; + [Reactive] private int? monthlyPayoutDate; + [Reactive] private DayOfWeek? weeklyPayoutDay; + + public FundProjectConfig() + { + SelectionModel selectionModel = new() { SingleSelect = false }; + SelectedInstallments = new ReactiveSelection(selectionModel, i => i) + .DisposeWith(Disposables); + + + this.ValidationRule(x => x.Name, x => !string.IsNullOrWhiteSpace(x), "Project name is required.") + .DisposeWith(Disposables); + this.ValidationRule(x => x.Description, x => !string.IsNullOrWhiteSpace(x), "Project description is required.") + .DisposeWith(Disposables); + this.ValidationRule(x => x.Website, x => string.IsNullOrWhiteSpace(x) || (Uri.TryCreate(x, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)), "Website must be a valid URL (http or https).") + .DisposeWith(Disposables); + + + this.ValidationRule( + this.WhenAnyValue( + x => x.GoalAmount, + x => x.GoalAmount!.Sats, + (amount, sats) => amount != null && sats > 0 + ), + isValid => isValid, + _ => "Goal amount must be greater than 0.") + .DisposeWith(Disposables); + this.ValidationRule(x => x.GoalAmount, x => x != null, _ => "Goal amount is required.") + .DisposeWith(Disposables); + + + this.ValidationRule(x => x.PayoutFrequency, x => x != null, "Payout frequency is required.") + .DisposeWith(Disposables); + this.ValidationRule(x => x.SelectedInstallments, SelectedInstallments.SelectionCount, i => i > 0, _ => "At least one installment count is required.") + .DisposeWith(Disposables); + + + var monthlyDateValid = Observable.CombineLatest( + this.WhenAnyValue(x => x.PayoutFrequency), + this.WhenAnyValue(x => x.MonthlyPayoutDate), + (freq, date) => freq != AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model.PayoutFrequency.Monthly || date != null + ); + this.ValidationRule(x => x.MonthlyPayoutDate, monthlyDateValid, "Monthly payout date is required for monthly frequency.") + .DisposeWith(Disposables); + + this.ValidationRule(x => x.MonthlyPayoutDate, x => x is null || (x >= 1 && x <= 29), "Monthly payout date must be between 1 and 29.") + .DisposeWith(Disposables); + + + var weeklyDayValid = Observable.CombineLatest( + this.WhenAnyValue(x => x.PayoutFrequency), + this.WhenAnyValue(x => x.WeeklyPayoutDay), + (freq, day) => freq != AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model.PayoutFrequency.Weekly || day != null + ); + this.ValidationRule(x => x.WeeklyPayoutDay, weeklyDayValid, "Weekly payout day is required for weekly frequency.") + .DisposeWith(Disposables); + } + + public ReactiveSelection SelectedInstallments { get; } + + public IEnumerable AvailableInstallmentCounts { get; } = [3, 6, 12]; + + public new void Dispose() + { + Disposables.Dispose(); + + base.Dispose(); + } + + public IObservable IsValid => this.IsValid(); + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/FundProjectConfigSample.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/FundProjectConfigSample.cs new file mode 100644 index 000000000..2eb2a3790 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/FundProjectConfigSample.cs @@ -0,0 +1,41 @@ +using System.Linq; +using AngorApp.Model.Amounts; +using Avalonia.Controls.Selection; +using ReactiveUI.Validation.Helpers; +using Zafiro.Avalonia.Misc; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model +{ + public class FundProjectConfigSample : ReactiveValidationObject, IFundProjectConfig + { + public string Name { get; set; } = "Sample Fund Project"; + public string Description { get; set; } = "This is a sample fund project for design-time preview"; + public string Website { get; set; } = "https://example.com"; + public IAmountUI? GoalAmount { get; set; } = AmountUI.FromBtc(1.0); + + public string AvatarUri { get; set; } = string.Empty; + public string BannerUri { get; set; } = string.Empty; + public string Nip05 { get; set; } = string.Empty; + public string Lud16 { get; set; } = string.Empty; + public string Nip57 { get; set; } = string.Empty; + + public PayoutFrequency? PayoutFrequency { get; set; } = Model.PayoutFrequency.Monthly; + public ReactiveSelection SelectedInstallments { get; set; } = new(new SelectionModel([3, 6, 12]), i => i); + public int? MonthlyPayoutDate { get; set; } = 15; + public DayOfWeek? WeeklyPayoutDay { get; set; } = DayOfWeek.Monday; + public IEnumerable AvailableInstallmentCounts { get; } = [3, 6, 12]; + public IAmountUI? Threshold { get; set; } = AmountUI.FromBtc(0.001); + public IObservable IsValid => Observable.Return(true); + } + + public class PayoutConfigSample : IPayoutConfig + { + public decimal? Percent { get; set; } + public DateTime? PayoutDate { get; set; } + public IObservable IsValid => Observable.Return(true); + public ReactiveUI.Validation.Contexts.IValidationContext ValidationContext => throw new NotImplementedException(); + public bool HasErrors => false; + public System.Collections.IEnumerable GetErrors(string? propertyName) => Enumerable.Empty(); + public event EventHandler? ErrorsChanged; + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/FundProjectConfigSampleEmpty.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/FundProjectConfigSampleEmpty.cs new file mode 100644 index 000000000..8e539e8e9 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/FundProjectConfigSampleEmpty.cs @@ -0,0 +1,28 @@ +using System.Collections.ObjectModel; +using Avalonia.Controls.Selection; +using ReactiveUI.Validation.Helpers; +using Zafiro.Avalonia.Misc; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model +{ + public partial class FundProjectConfigSampleEmpty : ReactiveValidationObject, IFundProjectConfig + { + [Reactive] private PayoutFrequency? payoutFrequency; + + public IObservable IsValid { get; } = Observable.Return(false); + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string Website { get; set; } = string.Empty; + public IAmountUI? GoalAmount { get; set; } + public string AvatarUri { get; set; } = string.Empty; + public string BannerUri { get; set; } = string.Empty; + public string Nip05 { get; set; } = string.Empty; + public string Lud16 { get; set; } = string.Empty; + public string Nip57 { get; set; } = string.Empty; + public ReactiveSelection SelectedInstallments { get; } = new(new SelectionModel() { SingleSelect = false, }, i => i); + public int? MonthlyPayoutDate { get; set; } + public DayOfWeek? WeeklyPayoutDay { get; set; } + public IEnumerable AvailableInstallmentCounts { get; } = [3, 6, 12]; + public IAmountUI? Threshold { get; set; } = AmountUI.FromBtc(0.01); + } +} \ No newline at end of file diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/IFundProjectConfig.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/IFundProjectConfig.cs new file mode 100644 index 000000000..5383f2bf7 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/IFundProjectConfig.cs @@ -0,0 +1,22 @@ +using System.Collections.ObjectModel; +using ReactiveUI.Validation.Abstractions; +using Zafiro.Avalonia.Misc; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model +{ + + public interface IFundProjectConfig : IProjectProfile + { + + IAmountUI? GoalAmount { get; set; } + + + PayoutFrequency? PayoutFrequency { get; set; } + ReactiveSelection SelectedInstallments { get; } + int? MonthlyPayoutDate { get; set; } + DayOfWeek? WeeklyPayoutDay { get; set; } + IEnumerable AvailableInstallmentCounts { get; } + IAmountUI? Threshold { get; set; } + } +} + diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/IPayoutConfig.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/IPayoutConfig.cs new file mode 100644 index 000000000..6a122e72b --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/IPayoutConfig.cs @@ -0,0 +1,11 @@ +using ReactiveUI.Validation.Abstractions; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model +{ + public interface IPayoutConfig : IValidatableViewModel + { + decimal? Percent { get; set; } + DateTime? PayoutDate { get; set; } + IObservable IsValid { get; } + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/PayoutConfig.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/PayoutConfig.cs new file mode 100644 index 000000000..18af07557 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/PayoutConfig.cs @@ -0,0 +1,30 @@ +using System.Reactive.Disposables; +using ReactiveUI.Validation.Extensions; +using ReactiveUI.Validation.Helpers; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model +{ + public partial class PayoutConfig : ReactiveValidationObject, IPayoutConfig + { + private readonly CompositeDisposable disposable = new(); + [Reactive] private DateTime? payoutDate; + [Reactive] private decimal? percent; + + public PayoutConfig() + { + this.ValidationRule(x => x.PayoutDate, x => x != null, "Payout date is required") + .DisposeWith(disposable); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + disposable.Dispose(); + } + base.Dispose(disposing); + } + + public IObservable IsValid => this.IsValid(); + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/PayoutFrequency.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/PayoutFrequency.cs new file mode 100644 index 000000000..f44951d69 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Model/PayoutFrequency.cs @@ -0,0 +1,8 @@ +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model +{ + public enum PayoutFrequency + { + Monthly, + Weekly + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/IPayoutsViewModel.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/IPayoutsViewModel.cs new file mode 100644 index 000000000..f0018542c --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/IPayoutsViewModel.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Payouts +{ + public interface IPayoutsViewModel : IHaveTitle, IValidatable + { + IFundProjectConfig FundProject { get; } + ReactiveCommand GeneratePayouts { get; } + ReactiveCommand ClearPayouts { get; } + ReadOnlyObservableCollection Payouts { get; } + IEnumerable AvailableFrequencies { get; } + IEnumerable AvailableInstallmentCounts { get; } + IEnumerable AvailablePayoutDates { get; } + IEnumerable AvailableDaysOfWeek { get; } + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsList.axaml b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsList.axaml new file mode 100644 index 000000000..cede37178 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsList.axaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsList.axaml.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsList.axaml.cs new file mode 100644 index 000000000..1cfaa68c4 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsList.axaml.cs @@ -0,0 +1,48 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model; +using System.Collections.Generic; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Payouts +{ + public partial class PayoutsList : UserControl + { + public static readonly StyledProperty> PayoutsProperty = + AvaloniaProperty.Register>(nameof(Payouts)); + + public static readonly StyledProperty TotalSatsProperty = + AvaloniaProperty.Register(nameof(TotalSats)); + + public static readonly StyledProperty FrequencyProperty = + AvaloniaProperty.Register(nameof(Frequency)); + + public IEnumerable Payouts + { + get => GetValue(PayoutsProperty); + set => SetValue(PayoutsProperty, value); + } + + public long? TotalSats + { + get => GetValue(TotalSatsProperty); + set => SetValue(TotalSatsProperty, value); + } + + public PayoutFrequency? Frequency + { + get => GetValue(FrequencyProperty); + set => SetValue(FrequencyProperty, value); + } + + public PayoutsList() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsView.axaml b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsView.axaml new file mode 100644 index 000000000..253abc3d9 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsView.axaml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + Select your payout pattern and schedule + + + + + + Payout Frequency + + + + + + + + + + + + + + + + + + + + Number of Installments + + + + + + + + + + + + + + + + + + + + + + + + Monthly Offer Payout Date + + + + + + + + + + + + + + + + + + + + Weekly Payout Day + + + + + + + + + + + + + + + + + + Generate Payout Schedule + + + + + + + + Delete + + + Payout Schedule + + + + + + + + + diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsView.axaml.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsView.axaml.cs new file mode 100644 index 000000000..15880c579 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Payouts +{ + public partial class PayoutsView : UserControl + { + public PayoutsView() + { + InitializeComponent(); + } + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsViewModel.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsViewModel.cs new file mode 100644 index 000000000..24a0d562d --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsViewModel.cs @@ -0,0 +1,94 @@ +using System.Linq; +using System.Reactive.Disposables; +using System.Collections.ObjectModel; +using System.Reactive.Linq; +using ReactiveUI; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Helpers; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model; +using Avalonia.Controls.Selection; +using DynamicData; +using Zafiro.Avalonia.Misc; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Payouts +{ + public class FundPayoutsViewModel : ReactiveObject, IHaveTitle, IPayoutsViewModel, IDisposable, IValidatable + { + public IFundProjectConfig FundProject { get; } + private readonly ReadOnlyObservableCollection payouts; + private readonly SourceList payoutsSource = new(); + private readonly CompositeDisposable disposables = new(); + + public FundPayoutsViewModel(IFundProjectConfig fundProject) + { + FundProject = fundProject; + + payoutsSource.Connect() + .Bind(out payouts) + .Subscribe() + .DisposeWith(disposables); + + var canGeneratePayouts = this.WhenAnyValue( + model => model.FundProject.PayoutFrequency, + model => model.FundProject.MonthlyPayoutDate, + model => model.FundProject.WeeklyPayoutDay, + mod => mod.FundProject.SelectedInstallments.SelectedItems.Count, + (frequency, monthDate, dayOfWeek, installmentCount) => + { + if (installmentCount == 0) + return false; + if (frequency == null) + return false; + if (frequency == PayoutFrequency.Monthly && monthDate == null) + return false; + if (frequency == PayoutFrequency.Weekly && dayOfWeek == null) + return false; + return true; + }) + .ObserveOn(RxApp.MainThreadScheduler); + + GeneratePayouts = ReactiveCommand.Create(DoGeneratePayouts, canGeneratePayouts); + ClearPayouts = ReactiveCommand.Create(() => payoutsSource.Clear()); + } + + private void DoGeneratePayouts() + { + if (FundProject.PayoutFrequency == null) + return; + var maxInstallments = FundProject.SelectedInstallments.SelectedItems.DefaultIfEmpty(0).Max(); + if (maxInstallments == 0) + return; + + var generated = PayoutGenerator.Generate( + FundProject.PayoutFrequency.Value, + maxInstallments, + DateTime.Now, + FundProject.MonthlyPayoutDate, + FundProject.WeeklyPayoutDay + ); + + payoutsSource.Edit(list => + { + list.Clear(); + list.AddRange(generated); + }); + } + + public IEnumerable AvailableFrequencies { get; } = System.Enum.GetValues(); + public IEnumerable AvailableInstallmentCounts => FundProject.AvailableInstallmentCounts; + public IEnumerable AvailablePayoutDates { get; } = Enumerable.Range(1, 29).ToList(); + public IEnumerable AvailableDaysOfWeek { get; } = System.Enum.GetValues(); + + public ReactiveCommand GeneratePayouts { get; } + public ReactiveCommand ClearPayouts { get; } + public ReadOnlyObservableCollection Payouts => payouts; + + public IObservable Title => Observable.Return("Payouts"); + + public void Dispose() + { + disposables.Dispose(); + } + + public IObservable IsValid => Observable.Return(true); + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsViewModelSample.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsViewModelSample.cs new file mode 100644 index 000000000..c5f318d45 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/Payouts/PayoutsViewModelSample.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Payouts +{ + public class PayoutsViewModelSample : IPayoutsViewModel + { + public IFundProjectConfig FundProject { get; set; } = new FundProjectConfigSample(); + public ReactiveCommand GeneratePayouts { get; } = ReactiveCommand.Create(() => { }); + public ReactiveCommand ClearPayouts { get; } = ReactiveCommand.Create(() => { }); + public IEnumerable AvailableFrequencies { get; } = new[] { PayoutFrequency.Monthly, PayoutFrequency.Weekly }; + public IEnumerable AvailableInstallmentCounts { get; } = new[] { 3, 6, 9 }; + public IEnumerable AvailablePayoutDates { get; } = new[] { 1, 15, 25 }; + public IEnumerable AvailableDaysOfWeek { get; } = new[] { DayOfWeek.Monday, DayOfWeek.Friday }; + public ReadOnlyObservableCollection Payouts { get; } = new(new ObservableCollection + { + new PayoutConfigSample { PayoutDate = DateTime.Now.AddMonths(1), Percent = 0.33m }, + new PayoutConfigSample { PayoutDate = DateTime.Now.AddMonths(2), Percent = 0.33m }, + new PayoutConfigSample { PayoutDate = DateTime.Now.AddMonths(3), Percent = 0.34m } + }); + public Zafiro.Avalonia.Misc.ReactiveSelection InstallmentSelection { get; } + + public PayoutsViewModelSample() + { + InstallmentSelection = new Zafiro.Avalonia.Misc.ReactiveSelection( + new Avalonia.Controls.Selection.SelectionModel { SingleSelect = false }, + x => x, + _ => true + ); + } + + public IObservable Title => Observable.Return("Payouts"); + public IObservable IsValid => Observable.Return(true); + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/ReviewAndDeployView.axaml b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/ReviewAndDeployView.axaml new file mode 100644 index 000000000..75bea4982 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/ReviewAndDeployView.axaml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Public Keys & IDs (Advanced) + + + + + + + + + + + + + + + + + + + + + + + + + + + Ready to deploy + Please review all information carefully. Once deployed only profile information can be changed. Your project will be live on bitcoin MainNet. + + + + + diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/ReviewAndDeployView.axaml.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/ReviewAndDeployView.axaml.cs new file mode 100644 index 000000000..3115f5e51 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/FundProject/ReviewAndDeployView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject +{ + public partial class ReviewAndDeployView : UserControl + { + public ReviewAndDeployView() + { + InitializeComponent(); + } + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/IProjectConfig.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/IProjectConfig.cs new file mode 100644 index 000000000..b6e400a32 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/IProjectConfig.cs @@ -0,0 +1,18 @@ +using ReactiveUI.Validation.Abstractions; + +namespace AngorApp.UI.Flows.CreateProject.Wizard +{ + + public interface IProjectConfig : IProjectProfile + { + + IAmountUI? TargetAmount { get; set; } + int? PenaltyDays { get; set; } + long? PenaltyThreshold { get; set; } + DateTime? ExpiryDate { get; set; } + + + Angor.Shared.Models.ProjectType ProjectType { get; set; } + } +} + diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/IProjectProfile.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/IProjectProfile.cs new file mode 100644 index 000000000..85e7fa612 --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/IProjectProfile.cs @@ -0,0 +1,20 @@ +using ReactiveUI.Validation.Abstractions; + +namespace AngorApp.UI.Flows.CreateProject.Wizard +{ + + public interface IProjectProfile : IValidatableViewModel, IValidatable, System.ComponentModel.INotifyDataErrorInfo + { + + string Name { get; set; } + string Description { get; set; } + string Website { get; set; } + + + string AvatarUri { get; set; } + string BannerUri { get; set; } + string Nip05 { get; set; } + string Lud16 { get; set; } + string Nip57 { get; set; } + } +} diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/FundingConfigurationView.axaml b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/FundingConfigurationView.axaml index 5fb179440..853f38e5c 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/FundingConfigurationView.axaml +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/FundingConfigurationView.axaml @@ -162,7 +162,7 @@ Configure your funding target, window, and project nature. - + @@ -183,22 +183,21 @@ Target Amount (BTC) * - - + BTC - - - - - + + The minimum amount your project needs to raise @@ -227,7 +226,7 @@ - + @@ -261,7 +260,7 @@ + ItemContainerTheme="{StaticResource ButtonizedAccent}" Background="Transparent"> @@ -288,7 +287,7 @@ - + diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/FundingConfigurationViewModel.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/FundingConfigurationViewModel.cs index cbc0dbb80..3999b722b 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/FundingConfigurationViewModel.cs +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/FundingConfigurationViewModel.cs @@ -25,14 +25,12 @@ public FundingConfigurationViewModel(IInvestmentProjectConfig newProject) var defaultPreset = AmountPresets.FirstOrDefault(); if (defaultPreset != null) { - NewProject.TargetAmount = new MutableAmountUI { Sats = defaultPreset.Sats }; + NewProject.TargetAmount = new AmountUI(defaultPreset.Sats); } } } - /// - /// Helper property to bridge between preset selection (long) and TargetAmount (IAmountUI) - /// + public long? SelectedPresetSats { get => NewProject.TargetAmount?.Sats; @@ -40,10 +38,7 @@ public long? SelectedPresetSats { if (value.HasValue) { - if (NewProject.TargetAmount == null) - NewProject.TargetAmount = new MutableAmountUI { Sats = value.Value }; - else if (NewProject.TargetAmount is MutableAmountUI mutable) - mutable.Sats = value.Value; + NewProject.TargetAmount = new AmountUI(value.Value); } } } diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/IReviewAndDeployViewModel.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/IReviewAndDeployViewModel.cs index f87625cf4..17282a8ee 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/IReviewAndDeployViewModel.cs +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/IReviewAndDeployViewModel.cs @@ -4,7 +4,7 @@ namespace AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject { public interface IReviewAndDeployViewModel { - IInvestmentProjectConfig NewProject { get; } + IProjectConfig NewProject { get; } IEnhancedCommand> DeployCommand { get; } } } \ No newline at end of file diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Mappers/CreateProjectDtoMapper.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Mappers/CreateProjectDtoMapper.cs index 78e8658de..62d78e665 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Mappers/CreateProjectDtoMapper.cs +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Mappers/CreateProjectDtoMapper.cs @@ -15,7 +15,7 @@ public static CreateProjectDto ToDto(this IInvestmentProjectConfig newProject) return new CreateProjectDto { - // Nostr profile + ProjectName = newProject.Name, Description = newProject.Description, WebsiteUri = newProject.Website, @@ -25,19 +25,21 @@ public static CreateProjectDto ToDto(this IInvestmentProjectConfig newProject) Lud16 = newProject.Lud16, Nip57 = newProject.Nip57, - // Project information + ProjectType = newProject.ProjectType, Sats = satsValue, - StartDate = newProject.StartDate ?? DateTime.Now, // Default or ensure verified + StartDate = newProject.StartDate ?? DateTime.Now, + ExpiryDate = newProject.ExpiryDate, EndDate = newProject.FundingEndDate, PenaltyDays = penaltyDaysValue, PenaltyThreshold = newProject.PenaltyThreshold, TargetAmount = new Amount(satsValue), Stages = newProject.Stages.Select(stage => - new CreateProjectStageDto(DateOnly.FromDateTime(stage.ReleaseDate!.Value.Date), stage.Percent ?? 0)), // Assuming valid before call + new CreateProjectStageDto(DateOnly.FromDateTime(stage.ReleaseDate!.Value.Date), stage.Percent ?? 0)), + + - // For Fund and Subscribe types - those might be null for Invest projects SelectedPatterns = null, PayoutDay = null }; diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Model/IFundingStageConfig.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Model/IFundingStageConfig.cs index d0356a303..ef3e6274f 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Model/IFundingStageConfig.cs +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Model/IFundingStageConfig.cs @@ -5,7 +5,8 @@ namespace AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject.Model { public interface IFundingStageConfig : IValidatableViewModel, IValidatable, INotifyPropertyChanged { - public decimal? Percent { get; set; } // Ratio: 0..1 + public decimal? Percent { get; set; } + public DateTime? ReleaseDate { get; set; } public TimeSpan? TimeFromPrevious { get; } void SetPreviousDateSource(IObservable source); diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Model/IInvestmentProjectConfig.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Model/IInvestmentProjectConfig.cs index c0fbaa19e..8e47e1e3e 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Model/IInvestmentProjectConfig.cs +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Model/IInvestmentProjectConfig.cs @@ -5,26 +5,12 @@ namespace AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject.Model { - public interface IInvestmentProjectConfig : IValidatableViewModel, IValidatable, System.ComponentModel.INotifyDataErrorInfo + public interface IInvestmentProjectConfig : IProjectConfig { - string Name { get; set; } - string Description { get; set; } - string Website { get; set; } - IAmountUI? TargetAmount { get; set; } - int? PenaltyDays { get; set; } - long? PenaltyThreshold { get; set; } + DateTime? FundingEndDate { get; set; } DateTime? StartDate { get; set; } - DateTime? ExpiryDate { get; set; } - - string AvatarUri { get; set; } - string BannerUri { get; set; } - string Nip05 { get; set; } - string Lud16 { get; set; } - string Nip57 { get; set; } - // Assuming ProjectType is needed here, need to add using or fully qualify - Angor.Shared.Models.ProjectType ProjectType { get; set; } ReadOnlyObservableCollection Stages { get; } diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Model/InvestmentProjectConfigBase.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Model/InvestmentProjectConfigBase.cs index 2437a3063..246d29c35 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Model/InvestmentProjectConfigBase.cs +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Model/InvestmentProjectConfigBase.cs @@ -23,7 +23,7 @@ protected enum ValidationEnvironment [Reactive] private string description = string.Empty; [Reactive] private string website = string.Empty; - // Investment Properties + [Reactive] private IAmountUI? targetAmount; [Reactive] private int? penaltyDays; [Reactive] private long? penaltyThreshold; @@ -59,7 +59,7 @@ protected InvestmentProjectConfigBase(ValidationEnvironment environment) .ToCollection() .Select(items => items.Sum(x => x.Percent ?? 0)); - // Basic Info Validations + this.ValidationRule(x => x.Name, x => !string.IsNullOrWhiteSpace(x), "Project name is required.").DisposeWith(Disposables); this.ValidationRule(x => x.Description, x => !string.IsNullOrWhiteSpace(x), "Project description is required.").DisposeWith(Disposables); this.ValidationRule(x => x.Website, x => string.IsNullOrWhiteSpace(x) || (Uri.TryCreate(x, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)), "Website must be a valid URL (http or https).").DisposeWith(Disposables); @@ -67,7 +67,7 @@ protected InvestmentProjectConfigBase(ValidationEnvironment environment) var isTotalPercentValid = totalPercent.Select(percent => Math.Abs(percent - 1.0m) < 0.0001m); this.ValidationRule(x => x.Stages, isTotalPercentValid, "Total percentage must be 100%"); - // Target Amount + this.ValidationRule( this.WhenAnyValue( x => x.TargetAmount, @@ -79,18 +79,18 @@ protected InvestmentProjectConfigBase(ValidationEnvironment environment) .DisposeWith(Disposables); this.ValidationRule(x => x.TargetAmount, x => x != null, _ => "Target amount is required.").DisposeWith(Disposables); - // Penalty Days + this.ValidationRule(x => x.PenaltyDays, x => x != null, "Penalty days is required.").DisposeWith(Disposables); this.ValidationRule(x => x.PenaltyDays, x => x is null || x >= 0, "Penalty days cannot be negative.").DisposeWith(Disposables); this.ValidationRule(x => x.PenaltyDays, x => x is null || x <= 365, "Penalty period cannot exceed 365 days.").DisposeWith(Disposables); - // Penalty Threshold + this.ValidationRule(x => x.PenaltyThreshold, x => x is null or >= 0, "Penalty threshold must be greater than or equal to 0.").DisposeWith(Disposables); - // Funding End Date + this.ValidationRule(x => x.FundingEndDate, x => x != null, "Funding end date is required.").DisposeWith(Disposables); - // Stages + var areStagesValid = StagesSource.Connect().FilterOnObservable(stage => stage.IsValid).IsEmpty().Select(b => !b); this.ValidationRule(x => x.Stages, areStagesValid, "Stages are not valid").DisposeWith(Disposables); @@ -102,7 +102,7 @@ protected InvestmentProjectConfigBase(ValidationEnvironment environment) _ => "Start date must be before or equal to funding end date." ).DisposeWith(Disposables); - // Environment-specific validations + AddEnvironmentSpecificValidations(environment); } @@ -120,7 +120,7 @@ private void AddEnvironmentSpecificValidations(ValidationEnvironment environment private void AddProductionValidations() { - // Target Amount Production Limits + this.ValidationRule( this.WhenAnyValue( x => x.TargetAmount, @@ -140,10 +140,10 @@ private void AddProductionValidations() _ => "Target amount cannot exceed 100 BTC.") .DisposeWith(Disposables); - // Penalty Days Production Limit + this.ValidationRule(x => x.PenaltyDays, x => x is null || x >= 10, "Penalty period must be at least 10 days.").DisposeWith(Disposables); - // Funding Date Production Check + this.ValidationRule(x => x.FundingEndDate, x => x == null || x.Value.Date > DateTime.Now.Date, "Funding end date must be after today.").DisposeWith(Disposables); this.ValidationRule(x => x.FundingEndDate, x => x == null || (x.Value - DateTime.Now) <= TimeSpan.FromDays(365), "Funding period cannot exceed one year.").DisposeWith(Disposables); } diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Model/InvestmentProjectConfigSample.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Model/InvestmentProjectConfigSample.cs index 5e0f4ab59..9a719d785 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Model/InvestmentProjectConfigSample.cs +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Model/InvestmentProjectConfigSample.cs @@ -62,11 +62,11 @@ public IFundingStageConfig CreateAndAddStage(decimal percent = 0, DateTime? rele return stage; } - // IValidatableViewModel implementation stub + public IObservable IsValid => Observable.Return(true); public IValidationContext ValidationContext { get; } = new ValidationContext(); - // INotifyDataErrorInfo implementation stub + public event EventHandler? ErrorsChanged; public System.Collections.IEnumerable GetErrors(string? propertyName) => Enumerable.Empty(); public bool HasErrors => false; diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ProjectDeploymentOrchestrator.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ProjectDeploymentOrchestrator.cs index 661526823..7b0c786de 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ProjectDeploymentOrchestrator.cs +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ProjectDeploymentOrchestrator.cs @@ -31,7 +31,7 @@ public async Task> Deploy(WalletId walletId, CreateProjectDto dto var transactionDraftPreviewerViewModel = new TransactionDraftPreviewerViewModel( async feerate => { - // Create transaction draft + var result = await CreateProjectTransactionDraft(walletId, feerate, dto, projectSeed); return result.Map(response => { @@ -42,7 +42,7 @@ public async Task> Deploy(WalletId walletId, CreateProjectDto dto }, model => { - // Submit transaction + return SubmitProjectTransaction(new PublishFounderTransaction.PublishFounderTransactionRequest(model.Model)) .Tap(txId => { diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ProjectImagesView.axaml b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ProjectImagesView.axaml index 9cc7d4c94..6f2a8e0c8 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ProjectImagesView.axaml +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ProjectImagesView.axaml @@ -196,7 +196,7 @@ - + @@ -217,7 +217,7 @@ - + - + diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ProjectImagesViewModel.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ProjectImagesViewModel.cs index 65b929add..d842fb56c 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ProjectImagesViewModel.cs +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ProjectImagesViewModel.cs @@ -1,10 +1,8 @@ -using AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject.Model; - namespace AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject { - public class ProjectImagesViewModel(IInvestmentProjectConfig newProject) : IHaveTitle + public class ProjectImagesViewModel(IProjectProfile newProject) : IHaveTitle { - public IInvestmentProjectConfig NewProject { get; } = newProject; + public IProjectProfile NewProject { get; } = newProject; public IObservable Title => Observable.Return("Project Images"); } diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ProjectProfileViewModel.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ProjectProfileViewModel.cs index 658af1553..a54a86cc8 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ProjectProfileViewModel.cs +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ProjectProfileViewModel.cs @@ -1,11 +1,10 @@ -using AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject.Model; using AngorApp.UI.Shared; namespace AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject { - public class ProjectProfileViewModel(IInvestmentProjectConfig newProject) : IHaveTitle, IValidatable + public class ProjectProfileViewModel(IProjectProfile newProject) : IHaveTitle, IValidatable { - public IInvestmentProjectConfig NewProject { get; } = newProject; + public IProjectProfile NewProject { get; } = newProject; public IObservable Title => Observable.Return("Project Profile"); public IObservable IsValid => NewProject.WhenValid( diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ReviewAndDeployView.axaml b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ReviewAndDeployView.axaml index 21bbd0f3e..f41e3b27f 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ReviewAndDeployView.axaml +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ReviewAndDeployView.axaml @@ -10,11 +10,7 @@ x:Class="AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject.ReviewAndDeployView" x:DataType="steps:IReviewAndDeployViewModel"> - - - - - + @@ -48,18 +44,18 @@ - - + + - - + + - + diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ReviewAndDeployViewModel.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ReviewAndDeployViewModel.cs index 671e88930..5c62d1800 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ReviewAndDeployViewModel.cs +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ReviewAndDeployViewModel.cs @@ -1,6 +1,8 @@ using System.Reactive.Disposables; using Angor.Sdk.Common; using Angor.Sdk.Funding.Founder.Dtos; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Mappers; +using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model; using AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject.Mappers; using AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject.Model; @@ -13,11 +15,11 @@ public class ReviewAndDeployViewModel : IHaveTitle, IReviewAndDeployViewModel, I private readonly ProjectSeedDto projectSeed; private readonly CompositeDisposable disposables = new(); - public IInvestmentProjectConfig NewProject { get; } + public IProjectConfig NewProject { get; } public IEnhancedCommand> DeployCommand { get; } public ReviewAndDeployViewModel( - IInvestmentProjectConfig newProject, + IProjectConfig newProject, IProjectDeploymentOrchestrator orchestrator, WalletId walletId, ProjectSeedDto projectSeed, @@ -34,7 +36,9 @@ public ReviewAndDeployViewModel( private async Task> Deploy() { - var dto = NewProject.ToDto(); + var dto = (NewProject as IInvestmentProjectConfig)?.ToDto() + ?? (NewProject as IFundProjectConfig)?.ToDto() + ?? throw new InvalidOperationException($"Unknown project type: {NewProject.GetType().Name}"); return await orchestrator.Deploy(walletId, dto, projectSeed); } diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ReviewAndDeployViewModelSample.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ReviewAndDeployViewModelSample.cs index 0675a22ea..15aa003c5 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ReviewAndDeployViewModelSample.cs +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/ReviewAndDeployViewModelSample.cs @@ -4,7 +4,7 @@ namespace AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject { public class ReviewAndDeployViewModelSample : IReviewAndDeployViewModel { - public IInvestmentProjectConfig NewProject { get; set; } = new InvestmentProjectConfigSample(); + public IProjectConfig NewProject { get; set; } = new InvestmentProjectConfigSample(); public IEnhancedCommand> DeployCommand { get; } = ReactiveCommand.Create(() => Result.Success("SampleTransactionId")).Enhance(); } } \ No newline at end of file diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Stages/StagesSimpleEditor.axaml b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Stages/StagesSimpleEditor.axaml index a36bae61f..41808256e 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Stages/StagesSimpleEditor.axaml +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/InvestmentProject/Stages/StagesSimpleEditor.axaml @@ -49,7 +49,7 @@ - @@ -88,7 +88,7 @@ How often do you need funding released? - diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/ProjectTypeView.axaml b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/ProjectTypeView.axaml index 4018387a4..b8921ef58 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/ProjectTypeView.axaml +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/ProjectTypeView.axaml @@ -14,7 +14,7 @@ - + @@ -109,7 +109,7 @@ Choose the type of project you want to launch. - + @@ -131,8 +131,8 @@ BorderThickness="2" Background="{Binding $parent[ListBoxItem].Foreground}"> - - + + diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/ProjectTypeViewModel.cs b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/ProjectTypeViewModel.cs index 2bbe1430b..e9e53717d 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/ProjectTypeViewModel.cs +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/ProjectTypeViewModel.cs @@ -1,4 +1,4 @@ -using System.Security.Principal; +using System.Linq; namespace AngorApp.UI.Flows.CreateProject.Wizard { @@ -6,15 +6,20 @@ public partial class ProjectTypeViewModel : ReactiveObject, IHaveTitle { [Reactive] private ProjectType projectType; + public ProjectTypeViewModel() + { + ProjectType = ProjectTypes.First(); + } + public IObservable Title => Observable.Return("Project Type"); public IEnumerable ProjectTypes { get; } = [ - new("Investment", "One-time funding with start and end dates.", new Icon("fa-arrow-trend-up")), - new("Fund", "Recurring funding with periodic contributions.", new Icon("fa-bitcoin")), - new("Subscription", "Ongoing subscription-based funding model.", new Icon("fa-arrows-rotate")) + new("Investment", "I am looking for Investors", "Investors can fund during a funding period and then once the goal is met funds start being released in stages.", new Icon("fa-arrow-trend-up")), + new("Fund", "I am looking for supporters", "Supporters can fund at anytime during the project timeline.", new Icon("fa-bitcoin")), + new("Subscription", "I am looking for paid subscribers", "We offer weekly and monthly subscriptions paid for up front 3/6/12 months and released on a chosen day each week of month.", new Icon("fa-arrows-rotate")) ]; } - public record ProjectType(string Name, string Description, object Icon); + public record ProjectType(string Name, string Title, string Description, object Icon); } \ No newline at end of file diff --git a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/WelcomeView.axaml b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/WelcomeView.axaml index 27abf0dc4..512393125 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/WelcomeView.axaml +++ b/src/Angor/Avalonia/AngorApp/UI/Flows/CreateProject/Wizard/WelcomeView.axaml @@ -26,7 +26,7 @@ A small fee is required to prevent spam and will be paid when you submit your project for deployment. - + diff --git a/src/Angor/Avalonia/AngorApp/UI/Sections/Home/HomeSectionView.axaml b/src/Angor/Avalonia/AngorApp/UI/Sections/Home/HomeSectionView.axaml index cff4254cf..088e81da1 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Sections/Home/HomeSectionView.axaml +++ b/src/Angor/Avalonia/AngorApp/UI/Sections/Home/HomeSectionView.axaml @@ -13,7 +13,7 @@ Fund Projects Discover and fund innovative Bitcoin projects on the Angor platform. - + @@ -29,10 +29,9 @@ Get Projects Funded - Create and launch your own projects to raise funding from the Bitcoin community. + Create and launch your own projects to raise funding. - diff --git a/src/Angor/Avalonia/AngorApp/UI/Shared/Controls/AngorConverters.cs b/src/Angor/Avalonia/AngorApp/UI/Shared/Controls/AngorConverters.cs index 8fe1f70ff..90e7197cc 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Shared/Controls/AngorConverters.cs +++ b/src/Angor/Avalonia/AngorApp/UI/Shared/Controls/AngorConverters.cs @@ -129,6 +129,8 @@ obs[0] is not int index || public static IValueConverter AddMonthsToToday { get; } = new AddTodayToMonthsConverter(); public static IValueConverter RatioToPercentage { get; } = new RatioToPercentageConverter(); + public static IValueConverter BtcToAmountUI { get; } = new BtcToAmountUIConverter(); + public static IValueConverter BtcToAmountUIWithDefault { get; } = new BtcToAmountUIWithDefaultConverter(); private sealed class FallbackIfNullOrEmptyConverter : IValueConverter { @@ -237,5 +239,71 @@ private class RatioToPercentageConverter : IValueConverter } } + /// + /// Converts between decimal? (BTC value for NumericUpDown) and IAmountUI?. + /// Convert: IAmountUI? → decimal? (extracts Btc) + /// ConvertBack: decimal? → IAmountUI? (creates new AmountUI from BTC) + /// + private class BtcToAmountUIConverter : IValueConverter + { + protected const long SatsPerBtc = 100_000_000; + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is IAmountUI amount) + { + return amount.Btc; + } + return null; + } + + public virtual object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is decimal btc) + { + long sats = (long)(btc * SatsPerBtc); + return new AmountUI(sats); + } + return null; + } + } + + /// + /// Same as BtcToAmountUIConverter but defaults to 0.001 BTC when value is null/cleared. + /// Used for Threshold inputs where a default is desired. + /// + private class BtcToAmountUIWithDefaultConverter : BtcToAmountUIConverter + { + private const long DefaultThresholdSats = 100_000; // 0.001 BTC + + public override object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is decimal btc) + { + long sats = (long)(btc * SatsPerBtc); + return new AmountUI(sats); + } + // When null/empty, return the default threshold + return new AmountUI(DefaultThresholdSats); + } + } + + public static IValueConverter IsEqualTo { get; } = new EqualsConverter(); + + private class EqualsConverter : IValueConverter + { + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return object.Equals(value, parameter); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return AvaloniaProperty.UnsetValue; + } + } + + public static IValueConverter JoinStrings { get; } = new FuncValueConverter?, string?>( + values => values == null ? null : string.Join(", ", values)); } } diff --git a/src/Angor/Avalonia/AngorApp/UI/Shared/Styles/Styles.axaml b/src/Angor/Avalonia/AngorApp/UI/Shared/Styles/Styles.axaml index f8b9893bc..12380cc19 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Shared/Styles/Styles.axaml +++ b/src/Angor/Avalonia/AngorApp/UI/Shared/Styles/Styles.axaml @@ -15,7 +15,7 @@ 5 10 - 20 + 24 40 Green diff --git a/src/Angor/Avalonia/AngorApp/UI/Themes/New/Containers.axaml b/src/Angor/Avalonia/AngorApp/UI/Themes/New/Containers.axaml new file mode 100644 index 000000000..ac0af58ea --- /dev/null +++ b/src/Angor/Avalonia/AngorApp/UI/Themes/New/Containers.axaml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Angor/Avalonia/AngorApp/UI/Themes/New/Custom/ListBox.axaml b/src/Angor/Avalonia/AngorApp/UI/Themes/New/Custom/ListBox.axaml index 80b34cae7..94c7422c5 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Themes/New/Custom/ListBox.axaml +++ b/src/Angor/Avalonia/AngorApp/UI/Themes/New/Custom/ListBox.axaml @@ -29,7 +29,7 @@ - + @@ -59,8 +59,8 @@ - - + + diff --git a/src/Angor/Avalonia/AngorApp/UI/Themes/New/Icons.axaml b/src/Angor/Avalonia/AngorApp/UI/Themes/New/Icons.axaml index ef84fc980..c047a6549 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Themes/New/Icons.axaml +++ b/src/Angor/Avalonia/AngorApp/UI/Themes/New/Icons.axaml @@ -5,4 +5,7 @@ m 59.886719,4.4863281 c -1.07495,0.5899597 -1.63314,1.8065382 -2.234996,2.8273089 -3.214053,6.681439 -6.271027,13.43767 -9.461081,20.131215 -6.345644,13.595261 -12.73279,27.170711 -19.124329,40.744944 -0.887942,1.935861 -1.872609,4.056333 -2.685454,5.88247 -0.643109,1.671055 -1.555067,3.277459 -1.847656,5.058593 -0.220023,1.768085 1.161632,3.160566 2.421875,4.181641 2.705637,1.946282 5.64285,3.540645 8.442648,5.344828 6.962457,4.279407 13.887935,8.620935 20.897564,12.822152 1.430271,0.79318 3.123174,1.30651 4.762109,1.06837 2.198587,-0.67634 4.040894,-2.12779 6.02925,-3.237557 7.817802,-4.852046 15.742335,-9.532452 23.509054,-14.466543 1.424729,-1.011022 3.039184,-1.876992 4.10827,-3.291464 1.11554,-1.67289 0.293473,-3.724997 -0.316253,-5.421215 C 90.835394,68.226406 87.075999,60.412197 83.417969,52.537109 77.515085,39.892865 71.528971,27.253248 65.683594,14.603516 64.886087,12.887707 63.986906,11.009213 63.201172,9.4199219 62.426775,7.9858816 61.756748,6.4706036 60.801096,5.1493769 60.538495,4.9024504 60.247043,4.5572525 59.886719,4.4863281 Z M 75.022242,71.785024 c 0.281395,0.69611 0.563287,1.392019 0.844946,2.088023 -4.825832,3.012072 -9.658587,6.018137 -14.527344,8.957031 C 60.086898,83.48834 58.603096,83.06479 57.534492,82.248701 52.968781,79.424599 48.366191,76.660696 43.835938,73.78125 48.99544,62.73373 55.050972,50.823685 60.23687,39.78852 65.209256,50.3146 70.244238,61.064005 75.022242,71.785024 Z M 16.769531,98.128906 c -1.734672,0.208861 -2.458123,2.039204 -3.238281,3.357424 -3.01348,5.77302 -5.6814402,11.71853 -8.4648438,17.60351 -0.5229742,1.32192 -1.0763019,2.87073 -0.5390624,4.26758 0.6196273,0.95591 1.9434053,0.73048 2.9201167,0.90896 2.0945798,0.1919 4.1944745,0.31533 6.2946745,0.1396 2.046675,-0.16417 4.39538,0.0723 6.02413,-1.43517 1.512184,-1.39517 2.24257,-3.39011 3.227756,-5.1518 1.584234,-3.34954 3.298058,-6.66045 4.502611,-10.17052 0.532374,-1.57427 -0.06009,-3.38648 -1.417218,-4.34484 -2.527613,-1.84781 -5.23303,-3.453387 -7.990774,-4.928295 -0.414334,-0.178574 -0.866489,-0.280917 -1.319109,-0.246449 z M 103.03711,98.125 c -2.35697,0.424382 -4.321459,1.93311 -6.326704,3.15037 -1.35792,0.98806 -3.018139,1.7188 -3.951517,3.17273 -0.798734,1.43253 -0.376512,3.18737 0.128493,4.64671 1.639807,3.59809 3.334574,7.20073 5.101467,10.66798 0.834174,1.81162 2.047101,3.77984 4.153781,4.21358 3.05079,0.50728 6.10647,0.42207 9.19092,0.37966 1.23407,-0.11407 2.54746,-0.0974 3.69184,-0.61775 0.99649,-0.71585 0.66696,-2.15584 0.39274,-3.15933 -2.72199,-6.21989 -5.56197,-12.39205 -8.61344,-18.45786 -0.79186,-1.32152 -1.37899,-2.917004 -2.72365,-3.782142 -0.31574,-0.179207 -0.68546,-0.231419 -1.04393,-0.213948 z M30,13.21A3.93,3.93,0,1,1,36.8,9.27L41.86,18A3.94,3.94,0,1,1,35.05,22L30,13.21Zm31.45,13A35.23,35.23,0,1,1,36.52,36.52,35.13,35.13,0,0,1,61.44,26.2ZM58.31,4A3.95,3.95,0,1,1,66.2,4V14.06a3.95,3.95,0,1,1-7.89,0V4ZM87.49,10.1A3.93,3.93,0,1,1,94.3,14l-5.06,8.76a3.93,3.93,0,1,1-6.81-3.92l5.06-8.75ZM109.67,30a3.93,3.93,0,1,1,3.94,6.81l-8.75,5.06a3.94,3.94,0,1,1-4-6.81L109.67,30Zm9.26,28.32a3.95,3.95,0,1,1,0,7.89H108.82a3.95,3.95,0,1,1,0-7.89Zm-6.15,29.18a3.93,3.93,0,1,1-3.91,6.81l-8.76-5.06A3.93,3.93,0,1,1,104,82.43l8.75,5.06ZM92.89,109.67a3.93,3.93,0,1,1-6.81,3.94L81,104.86a3.94,3.94,0,0,1,6.81-4l5.06,8.76Zm-28.32,9.26a3.95,3.95,0,1,1-7.89,0V108.82a3.95,3.95,0,1,1,7.89,0v10.11Zm-29.18-6.15a3.93,3.93,0,0,1-6.81-3.91l5.06-8.76A3.93,3.93,0,1,1,40.45,104l-5.06,8.75ZM13.21,92.89a3.93,3.93,0,1,1-3.94-6.81L18,81A3.94,3.94,0,1,1,22,87.83l-8.76,5.06ZM4,64.57a3.95,3.95,0,1,1,0-7.89H14.06a3.95,3.95,0,1,1,0,7.89ZM10.1,35.39A3.93,3.93,0,1,1,14,28.58l8.76,5.06a3.93,3.93,0,1,1-3.92,6.81L10.1,35.39Z M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12 + + M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z + \ No newline at end of file diff --git a/src/Angor/Avalonia/AngorApp/UI/Themes/New/NewTheme.axaml b/src/Angor/Avalonia/AngorApp/UI/Themes/New/NewTheme.axaml index a80fb0446..0f70ede97 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Themes/New/NewTheme.axaml +++ b/src/Angor/Avalonia/AngorApp/UI/Themes/New/NewTheme.axaml @@ -38,6 +38,7 @@ + @@ -61,7 +62,7 @@ #E5E7EB #FAF9F6 #606060 - + #ef4444 @@ -91,15 +92,15 @@ - + - - + + #0A0A0A @@ -116,14 +117,14 @@ #333333 #0A0A0A #FBBF24 - + #ef4444 - + @@ -161,6 +162,13 @@ + + + + + 2 + + @@ -593,6 +601,52 @@ + + diff --git a/src/Angor/Avalonia/AngorApp/UI/Themes/New/Templates/Showcase.axaml b/src/Angor/Avalonia/AngorApp/UI/Themes/New/Templates/Showcase.axaml index 73a0bcbeb..ebde2cc01 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Themes/New/Templates/Showcase.axaml +++ b/src/Angor/Avalonia/AngorApp/UI/Themes/New/Templates/Showcase.axaml @@ -6,6 +6,7 @@ xmlns:generic="clr-namespace:System.Collections.Generic;assembly=System.Collections" mc:Ignorable="d" d:DesignWidth="800" x:Class="AngorApp.UI.Themes.New.Templates.Showcase"> + @@ -38,6 +39,7 @@ +