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/ProfileApp.cs b/qrcodeprofile/Apps/ProfileApp.cs new file mode 100644 index 00000000..b7274a95 --- /dev/null +++ b/qrcodeprofile/Apps/ProfileApp.cs @@ -0,0 +1,105 @@ +using IvyQrCodeProfileSharing.Services; + +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 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") + .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()) + { + // 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; + } + } + + // 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))) + ).Height(Size.Full()) + : new Card( + Layout.Vertical().Gap(6).Padding(2) + | (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/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..41d042d0 --- /dev/null +++ b/qrcodeprofile/Program.cs @@ -0,0 +1,12 @@ +using IvyQrCodeProfileSharing.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(); \ No newline at end of file diff --git a/qrcodeprofile/QrCodeProfile.csproj b/qrcodeprofile/QrCodeProfile.csproj new file mode 100644 index 00000000..674f2266 --- /dev/null +++ b/qrcodeprofile/QrCodeProfile.csproj @@ -0,0 +1,24 @@ + + + + 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 diff --git a/qrcodeprofile/Services/IQrCodeService.cs b/qrcodeprofile/Services/IQrCodeService.cs new file mode 100644 index 00000000..05eac314 --- /dev/null +++ b/qrcodeprofile/Services/IQrCodeService.cs @@ -0,0 +1,7 @@ +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 new file mode 100644 index 00000000..335ec04f --- /dev/null +++ b/qrcodeprofile/Services/QrCodeService.cs @@ -0,0 +1,63 @@ +using QRCoder; +using System.Text; + +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); + } + + 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}"); + } + + // vCard footer + vCard.AppendLine("END:VCARD"); + + return vCard.ToString(); + } +}