From 860101d86e1e439e3a28e8575edab499ef13fdf2 Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 25 Sep 2025 15:53:55 +0300 Subject: [PATCH 1/4] create a simple form for program --- qrcodeprofile/.dockerignore | 25 ++++++++++ qrcodeprofile/.gitignore | 5 ++ qrcodeprofile/Apps/HelloApp.cs | 22 +++++++++ qrcodeprofile/Apps/ProfileApp.cs | 77 ++++++++++++++++++++++++++++++ qrcodeprofile/Dockerfile | 34 +++++++++++++ qrcodeprofile/GlobalUsings.cs | 28 +++++++++++ qrcodeprofile/Program.cs | 11 +++++ qrcodeprofile/QrCodeProfile.csproj | 23 +++++++++ qrcodeprofile/QrCodeProfile.sln | 24 ++++++++++ qrcodeprofile/README.md | 17 +++++++ 10 files changed, 266 insertions(+) create mode 100644 qrcodeprofile/.dockerignore create mode 100644 qrcodeprofile/.gitignore create mode 100644 qrcodeprofile/Apps/HelloApp.cs create mode 100644 qrcodeprofile/Apps/ProfileApp.cs create mode 100644 qrcodeprofile/Dockerfile create mode 100644 qrcodeprofile/GlobalUsings.cs create mode 100644 qrcodeprofile/Program.cs create mode 100644 qrcodeprofile/QrCodeProfile.csproj create mode 100644 qrcodeprofile/QrCodeProfile.sln create mode 100644 qrcodeprofile/README.md diff --git a/qrcodeprofile/.dockerignore b/qrcodeprofile/.dockerignore new file mode 100644 index 00000000..af50df1a --- /dev/null +++ b/qrcodeprofile/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/qrcodeprofile/.gitignore b/qrcodeprofile/.gitignore new file mode 100644 index 00000000..2cb90536 --- /dev/null +++ b/qrcodeprofile/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +*.sln.iml +.idea/**/* +**/.idea/* \ No newline at end of file diff --git a/qrcodeprofile/Apps/HelloApp.cs b/qrcodeprofile/Apps/HelloApp.cs new file mode 100644 index 00000000..f0a29454 --- /dev/null +++ b/qrcodeprofile/Apps/HelloApp.cs @@ -0,0 +1,22 @@ +namespace QrCodeProfile.Apps; + +[App(icon:Icons.PartyPopper, title:"Hello")] +public class HelloApp : ViewBase +{ + public override object? Build() + { + var nameState = this.UseState(); + + return Layout.Center() + | (new Card( + Layout.Vertical().Gap(6).Padding(2) + | new Confetti(new IvyLogo()) + | Text.H2("Hello " + (string.IsNullOrEmpty(nameState.Value) ? "there" : nameState.Value) + "!") + | Text.Block("Welcome to the fantastic world of Ivy. Let's build something amazing together!") + | nameState.ToInput(placeholder: "What is your name?") + | new Separator() + | Text.Markdown("You'd be a hero to us if you could ⭐ us on [Github](https://github.com/Ivy-Interactive/Ivy-Framework)") + ) + .Width(Size.Units(120).Max(500))); + } +} \ No newline at end of file diff --git a/qrcodeprofile/Apps/ProfileApp.cs b/qrcodeprofile/Apps/ProfileApp.cs new file mode 100644 index 00000000..25b61238 --- /dev/null +++ b/qrcodeprofile/Apps/ProfileApp.cs @@ -0,0 +1,77 @@ +namespace IvyQrCodeProfileSharing.Apps; + +[App(icon: Icons.User, title: "Profile Creator")] +public class ProfileApp : ViewBase +{ + // Profile model with basic fields for sharing + public record ProfileModel( + string FirstName, + string LastName, + string Email, + string? Phone, + string? LinkedIn, + string? GitHub + ); + + public override object? Build() + { + var profile = UseState(() => new ProfileModel("", "", "", null, null, null)); + var formBuilder = profile.ToForm() + .Required(m => m.FirstName, m => m.LastName, m => m.Email) + .Label(m => m.FirstName, "First Name") + .Label(m => m.LastName, "Last Name") + .Label(m => m.Email, "Email Address") + .Label(m => m.Phone, "Phone Number") + .Label(m => m.LinkedIn, "LinkedIn Profile") + .Label(m => m.GitHub, "GitHub Profile") + .Description(m => m.Email, "We'll use this to contact you") + .Description(m => m.Phone, "Optional - for direct contact") + .Description(m => m.LinkedIn, "Optional - your professional profile") + .Description(m => m.GitHub, "Optional - your code repositories") + .Validate(m => m.Email, email => + (email.Contains("@") && email.Contains("."), "Please enter a valid email address")) + .Validate(m => m.LinkedIn, linkedin => + (string.IsNullOrEmpty(linkedin) || linkedin.Contains("linkedin.com"), "Please enter a valid LinkedIn URL")) + .Validate(m => m.GitHub, github => + (string.IsNullOrEmpty(github) || github.Contains("github.com"), "Please enter a valid GitHub URL")); + + var (onSubmit, formView, validationView, loading) = formBuilder.UseForm(this.Context); + + async void HandleSubmit() + { + if (await onSubmit()) + { + // Form data is automatically copied to profile.Value + // You can access the client service here if needed + // For now, we'll just show a success message + } + } + + return Layout.Center() + | new Card( + Layout.Vertical().Gap(6).Padding(2) + | new IvyLogo() + | Text.H2("Create Your Profile") + | Text.Block("Fill in your information to create a shareable profile") + | new Separator() + | formView + | Layout.Horizontal() + | new Button("Create Profile").HandleClick(new Action(HandleSubmit)) + .Loading(loading).Disabled(loading) + | validationView + | (profile.Value.FirstName != "" && profile.Value.LastName != "" && profile.Value.Email != "" ? + new Card( + Layout.Vertical() + | Text.H3("Profile Preview") + | Text.Block($"Name: {profile.Value.FirstName} {profile.Value.LastName}") + | Text.Block($"Email: {profile.Value.Email}") + | (profile.Value.Phone != null ? Text.Block($"Phone: {profile.Value.Phone}") : null) + | (profile.Value.LinkedIn != null ? Text.Block($"LinkedIn: {profile.Value.LinkedIn}") : null) + | (profile.Value.GitHub != null ? Text.Block($"GitHub: {profile.Value.GitHub}") : null) + ).Title("Preview") + : null) + ) + .Width(Size.Units(120).Max(600)) + .Title("Profile Creator"); + } +} \ No newline at end of file diff --git a/qrcodeprofile/Dockerfile b/qrcodeprofile/Dockerfile new file mode 100644 index 00000000..6d459687 --- /dev/null +++ b/qrcodeprofile/Dockerfile @@ -0,0 +1,34 @@ +# Base runtime image +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +WORKDIR /app +EXPOSE 80 + +# Build stage +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src + +# Copy and restore +COPY ["QrCodeProfile.csproj", "./"] +RUN dotnet restore "QrCodeProfile.csproj" + +# Copy everything and build +COPY . . +RUN dotnet build "QrCodeProfile.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# Publish stage +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "QrCodeProfile.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=true + +# Final runtime image +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . + +# Set environment variables +ENV PORT=80 +ENV ASPNETCORE_URLS="http://+:80" + +# Run the executable +ENTRYPOINT ["dotnet","./QrCodeProfile.dll"] \ No newline at end of file diff --git a/qrcodeprofile/GlobalUsings.cs b/qrcodeprofile/GlobalUsings.cs new file mode 100644 index 00000000..9e8372aa --- /dev/null +++ b/qrcodeprofile/GlobalUsings.cs @@ -0,0 +1,28 @@ +global using Ivy; +global using Ivy.Apps; +global using Ivy.Auth; +global using Ivy.Chrome; +global using Ivy.Client; +global using Ivy.Core; +global using Ivy.Core.Hooks; +global using Ivy.Helpers; +global using Ivy.Hooks; +global using Ivy.Shared; +global using Ivy.Views; +global using Ivy.Views.Alerts; +global using Ivy.Views.Blades; +global using Ivy.Views.Builders; +global using Ivy.Views.Charts; +global using Ivy.Views.Dashboards; +global using Ivy.Views.Forms; +global using Ivy.Views.Tables; +global using Ivy.Widgets.Inputs; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; +global using System.Collections.Immutable; +global using System.ComponentModel.DataAnnotations; +global using System.Globalization; +global using System.Reactive.Linq; + +namespace QrCodeProfile; diff --git a/qrcodeprofile/Program.cs b/qrcodeprofile/Program.cs new file mode 100644 index 00000000..43f1d644 --- /dev/null +++ b/qrcodeprofile/Program.cs @@ -0,0 +1,11 @@ +using QrCodeProfile.Apps; +CultureInfo.DefaultThreadCurrentCulture = CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("en-US"); +var server = new Server(); +#if DEBUG +server.UseHotReload(); +#endif +server.AddAppsFromAssembly(); +server.AddConnectionsFromAssembly(); +var chromeSettings = new ChromeSettings().DefaultApp().UseTabs(preventDuplicates: true); +server.UseChrome(chromeSettings); +await server.RunAsync(); diff --git a/qrcodeprofile/QrCodeProfile.csproj b/qrcodeprofile/QrCodeProfile.csproj new file mode 100644 index 00000000..b73f7827 --- /dev/null +++ b/qrcodeprofile/QrCodeProfile.csproj @@ -0,0 +1,23 @@ + + + + Exe + net9.0 + enable + enable + CS8618;CS8603;CS8602;CS8604;CS9113 + QrCodeProfile + + + + + + + + + + + + + + diff --git a/qrcodeprofile/QrCodeProfile.sln b/qrcodeprofile/QrCodeProfile.sln new file mode 100644 index 00000000..1f26f9ec --- /dev/null +++ b/qrcodeprofile/QrCodeProfile.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hello", "QrCodeProfile.csproj", "{039638F3-A6D7-FB67-E733-27BD896EFDC4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {039638F3-A6D7-FB67-E733-27BD896EFDC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {039638F3-A6D7-FB67-E733-27BD896EFDC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {039638F3-A6D7-FB67-E733-27BD896EFDC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {039638F3-A6D7-FB67-E733-27BD896EFDC4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5F74B1AD-6C2E-4FDA-9DAF-9F7D8053687B} + EndGlobalSection +EndGlobal diff --git a/qrcodeprofile/README.md b/qrcodeprofile/README.md new file mode 100644 index 00000000..37a05000 --- /dev/null +++ b/qrcodeprofile/README.md @@ -0,0 +1,17 @@ +# Hello + +Web application created using [Ivy](https://github.com/Ivy-Interactive/Ivy). + +Ivy is a web framework for building interactive web applications using C# and .NET. + +## Run + +``` +dotnet watch +``` + +## Deploy + +``` +ivy deploy +``` \ No newline at end of file From 2d5000638f77ee71c0f149c23d33891faf17dc1d Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 25 Sep 2025 16:21:58 +0300 Subject: [PATCH 2/4] implement qrcode generator --- qrcodeprofile/Apps/HelloApp.cs | 22 ------------ qrcodeprofile/Apps/ProfileApp.cs | 45 ++++++++++++++++++++++-- qrcodeprofile/Program.cs | 7 ++-- qrcodeprofile/QrCodeProfile.csproj | 1 + qrcodeprofile/Services/IQrCodeService.cs | 6 ++++ qrcodeprofile/Services/QrCodeService.cs | 15 ++++++++ 6 files changed, 68 insertions(+), 28 deletions(-) delete mode 100644 qrcodeprofile/Apps/HelloApp.cs create mode 100644 qrcodeprofile/Services/IQrCodeService.cs create mode 100644 qrcodeprofile/Services/QrCodeService.cs diff --git a/qrcodeprofile/Apps/HelloApp.cs b/qrcodeprofile/Apps/HelloApp.cs deleted file mode 100644 index f0a29454..00000000 --- a/qrcodeprofile/Apps/HelloApp.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace QrCodeProfile.Apps; - -[App(icon:Icons.PartyPopper, title:"Hello")] -public class HelloApp : ViewBase -{ - public override object? Build() - { - var nameState = this.UseState(); - - return Layout.Center() - | (new Card( - Layout.Vertical().Gap(6).Padding(2) - | new Confetti(new IvyLogo()) - | Text.H2("Hello " + (string.IsNullOrEmpty(nameState.Value) ? "there" : nameState.Value) + "!") - | Text.Block("Welcome to the fantastic world of Ivy. Let's build something amazing together!") - | nameState.ToInput(placeholder: "What is your name?") - | new Separator() - | Text.Markdown("You'd be a hero to us if you could ⭐ us on [Github](https://github.com/Ivy-Interactive/Ivy-Framework)") - ) - .Width(Size.Units(120).Max(500))); - } -} \ No newline at end of file diff --git a/qrcodeprofile/Apps/ProfileApp.cs b/qrcodeprofile/Apps/ProfileApp.cs index 25b61238..2449d34a 100644 --- a/qrcodeprofile/Apps/ProfileApp.cs +++ b/qrcodeprofile/Apps/ProfileApp.cs @@ -1,3 +1,5 @@ +using IvyQrCodeProfileSharing.Services; + namespace IvyQrCodeProfileSharing.Apps; [App(icon: Icons.User, title: "Profile Creator")] @@ -16,6 +18,10 @@ public record ProfileModel( public override object? Build() { var profile = UseState(() => new ProfileModel("", "", "", null, null, null)); + var qrCodeService = new QrCodeService(); + var qrCodeBase64 = UseState(""); + var profileSubmitted = UseState(false); + var formBuilder = profile.ToForm() .Required(m => m.FirstName, m => m.LastName, m => m.Email) .Label(m => m.FirstName, "First Name") @@ -41,9 +47,30 @@ async void HandleSubmit() { if (await onSubmit()) { - // Form data is automatically copied to profile.Value - // You can access the client service here if needed - // For now, we'll just show a success message + // Generate profile data as JSON for QR code + var profileData = new + { + firstName = profile.Value.FirstName, + lastName = profile.Value.LastName, + email = profile.Value.Email, + phone = profile.Value.Phone, + linkedin = profile.Value.LinkedIn, + github = profile.Value.GitHub, + generatedAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") + }; + + var profileJson = System.Text.Json.JsonSerializer.Serialize(profileData, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }); + + try + { + qrCodeBase64.Value = qrCodeService.GenerateQrCodeAsBase64(profileJson, 8); + profileSubmitted.Value = true; + } + catch (Exception ex) + { + // Handle QR code generation error + Console.WriteLine($"Error generating QR code: {ex.Message}"); + } } } @@ -70,6 +97,18 @@ async void HandleSubmit() | (profile.Value.GitHub != null ? Text.Block($"GitHub: {profile.Value.GitHub}") : null) ).Title("Preview") : null) + | (profileSubmitted.Value && !string.IsNullOrEmpty(qrCodeBase64.Value) ? + new Card( + Layout.Vertical().Gap(6) + | Text.H3("Your QR Code") + | Text.Block("Scan this QR code to share your profile information:") + | Text.Html($"") + | new Button("Generate New QR Code").HandleClick(new Action(() => { + qrCodeBase64.Value = ""; + profileSubmitted.Value = false; + })) + ).Title("QR Code") + : null) ) .Width(Size.Units(120).Max(600)) .Title("Profile Creator"); diff --git a/qrcodeprofile/Program.cs b/qrcodeprofile/Program.cs index 43f1d644..41d042d0 100644 --- a/qrcodeprofile/Program.cs +++ b/qrcodeprofile/Program.cs @@ -1,4 +1,5 @@ -using QrCodeProfile.Apps; +using IvyQrCodeProfileSharing.Apps; + CultureInfo.DefaultThreadCurrentCulture = CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("en-US"); var server = new Server(); #if DEBUG @@ -6,6 +7,6 @@ #endif server.AddAppsFromAssembly(); server.AddConnectionsFromAssembly(); -var chromeSettings = new ChromeSettings().DefaultApp().UseTabs(preventDuplicates: true); +var chromeSettings = new ChromeSettings().DefaultApp().UseTabs(preventDuplicates: true); server.UseChrome(chromeSettings); -await server.RunAsync(); +await server.RunAsync(); \ No newline at end of file diff --git a/qrcodeprofile/QrCodeProfile.csproj b/qrcodeprofile/QrCodeProfile.csproj index b73f7827..674f2266 100644 --- a/qrcodeprofile/QrCodeProfile.csproj +++ b/qrcodeprofile/QrCodeProfile.csproj @@ -14,6 +14,7 @@ + diff --git a/qrcodeprofile/Services/IQrCodeService.cs b/qrcodeprofile/Services/IQrCodeService.cs new file mode 100644 index 00000000..22a5e67c --- /dev/null +++ b/qrcodeprofile/Services/IQrCodeService.cs @@ -0,0 +1,6 @@ +namespace IvyQrCodeProfileSharing.Services; + +public interface IQrCodeService +{ + string GenerateQrCodeAsBase64(string text, int pixelsPerModule = 8); +} \ No newline at end of file diff --git a/qrcodeprofile/Services/QrCodeService.cs b/qrcodeprofile/Services/QrCodeService.cs new file mode 100644 index 00000000..1da9bf0f --- /dev/null +++ b/qrcodeprofile/Services/QrCodeService.cs @@ -0,0 +1,15 @@ +using QRCoder; + +namespace IvyQrCodeProfileSharing.Services; + +public class QrCodeService : IQrCodeService +{ + public string GenerateQrCodeAsBase64(string text, int pixelsPerModule = 8) + { + using var qrGenerator = new QRCodeGenerator(); + using var qrCodeData = qrGenerator.CreateQrCode(text, QRCodeGenerator.ECCLevel.Q); + using var qrCode = new PngByteQRCode(qrCodeData); + var qrCodeBytes = qrCode.GetGraphic(pixelsPerModule); + return Convert.ToBase64String(qrCodeBytes); + } +} From 33cb26c61af9fe4fce2fb6808f15b5fdefeecd9e Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 25 Sep 2025 16:33:29 +0300 Subject: [PATCH 3/4] implement better UI for qrcode generator --- qrcodeprofile/Apps/ProfileApp.cs | 126 +++++++++++------------ qrcodeprofile/Services/IQrCodeService.cs | 1 + qrcodeprofile/Services/QrCodeService.cs | 64 ++++++++++++ 3 files changed, 126 insertions(+), 65 deletions(-) diff --git a/qrcodeprofile/Apps/ProfileApp.cs b/qrcodeprofile/Apps/ProfileApp.cs index 2449d34a..2cbd58c3 100644 --- a/qrcodeprofile/Apps/ProfileApp.cs +++ b/qrcodeprofile/Apps/ProfileApp.cs @@ -21,19 +21,21 @@ public record ProfileModel( var qrCodeService = new QrCodeService(); var qrCodeBase64 = UseState(""); var profileSubmitted = UseState(false); - + var formBuilder = profile.ToForm() .Required(m => m.FirstName, m => m.LastName, m => m.Email) + .Place(m => m.FirstName) + .Place(1, m => m.Email) + .Place(m => m.LastName) + .Place(1, m => m.LinkedIn) + .Place(m => m.Phone) + .Place(1, m => m.GitHub) .Label(m => m.FirstName, "First Name") .Label(m => m.LastName, "Last Name") .Label(m => m.Email, "Email Address") .Label(m => m.Phone, "Phone Number") .Label(m => m.LinkedIn, "LinkedIn Profile") .Label(m => m.GitHub, "GitHub Profile") - .Description(m => m.Email, "We'll use this to contact you") - .Description(m => m.Phone, "Optional - for direct contact") - .Description(m => m.LinkedIn, "Optional - your professional profile") - .Description(m => m.GitHub, "Optional - your code repositories") .Validate(m => m.Email, email => (email.Contains("@") && email.Contains("."), "Please enter a valid email address")) .Validate(m => m.LinkedIn, linkedin => @@ -47,70 +49,64 @@ async void HandleSubmit() { if (await onSubmit()) { - // Generate profile data as JSON for QR code - var profileData = new - { - firstName = profile.Value.FirstName, - lastName = profile.Value.LastName, - email = profile.Value.Email, - phone = profile.Value.Phone, - linkedin = profile.Value.LinkedIn, - github = profile.Value.GitHub, - generatedAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") - }; - - var profileJson = System.Text.Json.JsonSerializer.Serialize(profileData, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }); - - try - { - qrCodeBase64.Value = qrCodeService.GenerateQrCodeAsBase64(profileJson, 8); + // Generate vCard QR code for contact sharing + qrCodeBase64.Value = qrCodeService.GenerateVCardQrCodeAsBase64( + profile.Value.FirstName, + profile.Value.LastName, + profile.Value.Email, + profile.Value.Phone, + profile.Value.LinkedIn, + profile.Value.GitHub, + 8 + ); profileSubmitted.Value = true; - } - catch (Exception ex) - { - // Handle QR code generation error - Console.WriteLine($"Error generating QR code: {ex.Message}"); - } } } - return Layout.Center() - | new Card( + // Sidebar content - Profile form + var formContent = new Card( + Layout.Vertical().Gap(6).Padding(2) + | Text.H2("Create Your Profile") + | Text.Block("Fill in your information to create a shareable profile") + | new Separator() + | formView + | Layout.Horizontal() + | new Button("Create Profile").HandleClick(new Action(HandleSubmit)) + .Loading(loading).Disabled(loading) + | validationView + ).Height(Size.Full()); + + // Main content - QR Code display + var qrCodeContent = profileSubmitted.Value && !string.IsNullOrEmpty(qrCodeBase64.Value) ? + new Card( + Layout.Vertical().Gap(6).Padding(2) + | Text.H2("Your QR Code") + | Text.Block("Scan this QR code with your phone to automatically add this contact to your contacts:") + | (Layout.Horizontal().Align(Align.Center) + | new DemoBox( + Text.Html($"") + ).BorderStyle(BorderStyle.None).Width(Size.Units(70)).Height(Size.Units(70))) + | (Layout.Horizontal().Align(Align.Center) + | new Button("Generate New QR Code").HandleClick(new Action(() => + { + qrCodeBase64.Value = ""; + profileSubmitted.Value = false; + }))) + + ).Height(Size.Full()) + : new Card( Layout.Vertical().Gap(6).Padding(2) - | new IvyLogo() - | Text.H2("Create Your Profile") - | Text.Block("Fill in your information to create a shareable profile") - | new Separator() - | formView - | Layout.Horizontal() - | new Button("Create Profile").HandleClick(new Action(HandleSubmit)) - .Loading(loading).Disabled(loading) - | validationView - | (profile.Value.FirstName != "" && profile.Value.LastName != "" && profile.Value.Email != "" ? - new Card( - Layout.Vertical() - | Text.H3("Profile Preview") - | Text.Block($"Name: {profile.Value.FirstName} {profile.Value.LastName}") - | Text.Block($"Email: {profile.Value.Email}") - | (profile.Value.Phone != null ? Text.Block($"Phone: {profile.Value.Phone}") : null) - | (profile.Value.LinkedIn != null ? Text.Block($"LinkedIn: {profile.Value.LinkedIn}") : null) - | (profile.Value.GitHub != null ? Text.Block($"GitHub: {profile.Value.GitHub}") : null) - ).Title("Preview") - : null) - | (profileSubmitted.Value && !string.IsNullOrEmpty(qrCodeBase64.Value) ? - new Card( - Layout.Vertical().Gap(6) - | Text.H3("Your QR Code") - | Text.Block("Scan this QR code to share your profile information:") - | Text.Html($"") - | new Button("Generate New QR Code").HandleClick(new Action(() => { - qrCodeBase64.Value = ""; - profileSubmitted.Value = false; - })) - ).Title("QR Code") - : null) - ) - .Width(Size.Units(120).Max(600)) - .Title("Profile Creator"); + | (Layout.Center() + | Text.H2("Welcome to Profile Creator")) + | Text.Block("Fill out the form in the sidebar to create your shareable profile QR code.") + | Text.Block("Once you submit the form, your QR code will appear here in the main content area.") + ).Height(Size.Full()); + + return Layout.Vertical().Height(Size.Full()) + | new ResizeablePanelGroup( + new ResizeablePanel(70, formContent), + new ResizeablePanel(30, qrCodeContent) + ).Horizontal(); + } } \ No newline at end of file diff --git a/qrcodeprofile/Services/IQrCodeService.cs b/qrcodeprofile/Services/IQrCodeService.cs index 22a5e67c..05eac314 100644 --- a/qrcodeprofile/Services/IQrCodeService.cs +++ b/qrcodeprofile/Services/IQrCodeService.cs @@ -3,4 +3,5 @@ namespace IvyQrCodeProfileSharing.Services; public interface IQrCodeService { string GenerateQrCodeAsBase64(string text, int pixelsPerModule = 8); + string GenerateVCardQrCodeAsBase64(string firstName, string lastName, string email, string? phone = null, string? linkedin = null, string? github = null, int pixelsPerModule = 8); } \ No newline at end of file diff --git a/qrcodeprofile/Services/QrCodeService.cs b/qrcodeprofile/Services/QrCodeService.cs index 1da9bf0f..31260179 100644 --- a/qrcodeprofile/Services/QrCodeService.cs +++ b/qrcodeprofile/Services/QrCodeService.cs @@ -1,4 +1,5 @@ using QRCoder; +using System.Text; namespace IvyQrCodeProfileSharing.Services; @@ -12,4 +13,67 @@ public string GenerateQrCodeAsBase64(string text, int pixelsPerModule = 8) var qrCodeBytes = qrCode.GetGraphic(pixelsPerModule); return Convert.ToBase64String(qrCodeBytes); } + + public string GenerateVCardQrCodeAsBase64(string firstName, string lastName, string email, string? phone = null, string? linkedin = null, string? github = null, int pixelsPerModule = 8) + { + var vCard = GenerateVCard(firstName, lastName, email, phone, linkedin, github); + return GenerateQrCodeAsBase64(vCard, pixelsPerModule); + } + + private static string GenerateVCard(string firstName, string lastName, string email, string? phone, string? linkedin, string? github) + { + var vCard = new StringBuilder(); + + // vCard header + vCard.AppendLine("BEGIN:VCARD"); + vCard.AppendLine("VERSION:3.0"); + + // Full name + vCard.AppendLine($"FN:{firstName} {lastName}"); + + // Structured name (Last;First;;;) + vCard.AppendLine($"N:{lastName};{firstName};;;"); + + // Email + vCard.AppendLine($"EMAIL;TYPE=INTERNET:{email}"); + + // Phone (if provided) + if (!string.IsNullOrWhiteSpace(phone)) + { + vCard.AppendLine($"TEL;TYPE=CELL:{phone}"); + } + + // LinkedIn URL as a URL field + if (!string.IsNullOrWhiteSpace(linkedin)) + { + vCard.AppendLine($"URL;TYPE=LinkedIn:{linkedin}"); + } + + // GitHub URL as a URL field + if (!string.IsNullOrWhiteSpace(github)) + { + vCard.AppendLine($"URL;TYPE=GitHub:{github}"); + } + + // Add note with social profiles if they exist + var notes = new List(); + if (!string.IsNullOrWhiteSpace(linkedin)) + { + notes.Add($"LinkedIn: {linkedin}"); + } + if (!string.IsNullOrWhiteSpace(github)) + { + notes.Add($"GitHub: {github}"); + } + + if (notes.Count > 0) + { + vCard.AppendLine($"NOTE:{string.Join(" | ", notes)}"); + } + + // vCard footer + vCard.AppendLine("END:VCARD"); + + return vCard.ToString(); + } } From 0291d370f8b98109fb1e43b3d506ebb514b91ba1 Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 25 Sep 2025 17:31:33 +0300 Subject: [PATCH 4/4] remove button for new generator --- qrcodeprofile/Apps/ProfileApp.cs | 33 ++++++++++--------------- qrcodeprofile/Services/QrCodeService.cs | 16 ------------ 2 files changed, 13 insertions(+), 36 deletions(-) diff --git a/qrcodeprofile/Apps/ProfileApp.cs b/qrcodeprofile/Apps/ProfileApp.cs index 2cbd58c3..b7274a95 100644 --- a/qrcodeprofile/Apps/ProfileApp.cs +++ b/qrcodeprofile/Apps/ProfileApp.cs @@ -49,17 +49,17 @@ async void HandleSubmit() { if (await onSubmit()) { - // Generate vCard QR code for contact sharing - qrCodeBase64.Value = qrCodeService.GenerateVCardQrCodeAsBase64( - profile.Value.FirstName, - profile.Value.LastName, - profile.Value.Email, - profile.Value.Phone, - profile.Value.LinkedIn, - profile.Value.GitHub, - 8 - ); - profileSubmitted.Value = true; + // Generate vCard QR code for contact sharing + qrCodeBase64.Value = qrCodeService.GenerateVCardQrCodeAsBase64( + profile.Value.FirstName, + profile.Value.LastName, + profile.Value.Email, + profile.Value.Phone, + profile.Value.LinkedIn, + profile.Value.GitHub, + 8 + ); + profileSubmitted.Value = true; } } @@ -86,13 +86,6 @@ async void HandleSubmit() | new DemoBox( Text.Html($"") ).BorderStyle(BorderStyle.None).Width(Size.Units(70)).Height(Size.Units(70))) - | (Layout.Horizontal().Align(Align.Center) - | new Button("Generate New QR Code").HandleClick(new Action(() => - { - qrCodeBase64.Value = ""; - profileSubmitted.Value = false; - }))) - ).Height(Size.Full()) : new Card( Layout.Vertical().Gap(6).Padding(2) @@ -104,8 +97,8 @@ async void HandleSubmit() return Layout.Vertical().Height(Size.Full()) | new ResizeablePanelGroup( - new ResizeablePanel(70, formContent), - new ResizeablePanel(30, qrCodeContent) + new ResizeablePanel(70, formContent), + new ResizeablePanel(30, qrCodeContent) ).Horizontal(); } diff --git a/qrcodeprofile/Services/QrCodeService.cs b/qrcodeprofile/Services/QrCodeService.cs index 31260179..335ec04f 100644 --- a/qrcodeprofile/Services/QrCodeService.cs +++ b/qrcodeprofile/Services/QrCodeService.cs @@ -55,22 +55,6 @@ private static string GenerateVCard(string firstName, string lastName, string em vCard.AppendLine($"URL;TYPE=GitHub:{github}"); } - // Add note with social profiles if they exist - var notes = new List(); - if (!string.IsNullOrWhiteSpace(linkedin)) - { - notes.Add($"LinkedIn: {linkedin}"); - } - if (!string.IsNullOrWhiteSpace(github)) - { - notes.Add($"GitHub: {github}"); - } - - if (notes.Count > 0) - { - vCard.AppendLine($"NOTE:{string.Join(" | ", notes)}"); - } - // vCard footer vCard.AppendLine("END:VCARD");