Compare commits

14 Commits

Author SHA1 Message Date
OlgunR
e6b41f10c8 Merge branch 'feat/receiver-ui' of http://git.dd:3000/AppStd/EnvelopeGenerator into feat/receiver-ui 2025-12-10 11:16:27 +01:00
OlgunR
dc78ad4a24 Improve canvas mouse position accuracy; add settings.json
Refactored mouse event handling in pdfInterop.js to use a new getPos helper, ensuring accurate coordinate mapping on scaled or resized canvases. Updated start and move functions to use this helper. Added an empty settings.json file.
2025-12-10 11:16:13 +01:00
b282318298 Add launchSettings.json for ReceiverUIBlazor project
Configured development launch profile with browser launch, environment variable, and HTTP/HTTPS URLs for EnvelopeGenerator.ReceiverUIBlazor.
2025-12-10 09:55:32 +01:00
8d68ea8c57 Add IIS publish profile for EnvelopeGenerator.ReceiverUIBlazor
Introduced IISProfile.pubxml to enable packaging and deployment
of the EnvelopeGenerator.ReceiverUIBlazor project to IIS. The
profile configures publish method, build settings, output
package location, and IIS app path.
2025-12-10 09:55:24 +01:00
OlgunR
7cb8b02b1d Revamp UI colors and fonts for warmer, modern look
Switched to "Red Hat Text" and "Teko" fonts via Google Fonts. Updated color palette from cool blues to warm reds and yellows, including backgrounds, accents, and error states. Refreshed button, top bar, and card styles for improved visual hierarchy and softer appearance.
2025-12-09 10:58:53 +01:00
OlgunR
cc1d33462c Add drag-and-drop PDF support to Blazor app
Implement drag-and-drop PDF loading via JS interop and DotNetObjectReference. Refactor file loading logic and UI structure for clarity. Add IAsyncDisposable for resource cleanup. Update pdfInterop.js to handle drop events and send PDF data to Blazor.
2025-12-09 10:42:11 +01:00
OlgunR
cd85b4fffc Improve PDF reset logic and modernize UI styling
Refactor PDF reset to restore original document state using a new OriginalPdfBase64 variable and async logic. Redesign app.css with a lighter color palette, CSS variables, and updated styles for buttons, overlays, modals, and inputs for a cleaner, more accessible UI. Adjust signature and text overlay colors in pdfInterop.js for better contrast and consistency.
2025-12-09 10:26:01 +01:00
OlgunR
ab3e7fb4e9 Error while dragging fields fixed 2025-12-09 09:50:10 +01:00
OlgunR
4a7676765c Fixed error while loading file 2025-12-09 08:57:39 +01:00
OlgunR
562ceb9c3f First results converting receiver-ui-react into a Blazor Web App 2025-12-08 16:21:10 +01:00
OlgunR
751ea706df Created EnvelopeGenerator.ReceiverUIBlazor 2025-12-08 16:06:26 +01:00
OlgunR
490ad9f7cf Created EnvelopeGenerator.ReceiverUIBlazor 2025-12-08 16:04:51 +01:00
7aeaba7c12 Add EnvelopeGenerator.ReceiverUI project and UI components
Introduced a new Razor Components-based web application, `EnvelopeGenerator.ReceiverUI`, to the solution. This includes:

- Added `EnvelopeGenerator.ReceiverUI` project to `EnvelopeGenerator.sln` with Debug and Release configurations.
- Created `EnvelopeGenerator.ReceiverUI.csproj` targeting `.NET 8.0` with nullable and implicit usings enabled.
- Added Razor components (`App.razor`, `Routes.razor`, `MainLayout.razor`, `NavMenu.razor`, `Counter.razor`, `Error.razor`, `Home.razor`, `Weather.razor`) for layout, navigation, and pages.
- Included styles (`MainLayout.razor.css`, `NavMenu.razor.css`, `app.css`) and assets (`favicon.png`, `bootstrap.min.css`).
- Configured logging and allowed hosts in `appsettings.json` and `appsettings.Development.json`.
- Added `Program.cs` to configure and run the application.
- Added `launchSettings.json` for development and debugging profiles.

These changes enable a modern, interactive web interface with Bootstrap styling and development-friendly configurations.
2025-12-05 09:08:57 +01:00
2301a81a1c init receiver ui with react 2025-12-04 18:11:47 +01:00
63 changed files with 14386 additions and 0 deletions

1
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="EnvelopeGenerator.ReceiverUI.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -0,0 +1,23 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

View File

@@ -0,0 +1,96 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

View File

@@ -0,0 +1,30 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">EnvelopeGenerator.ReceiverUI</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
</nav>
</div>

View File

@@ -0,0 +1,105 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -0,0 +1,19 @@
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View File

@@ -0,0 +1,7 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.

View File

@@ -0,0 +1,64 @@
@page "/weather"
@attribute [StreamRendering]
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

View File

@@ -0,0 +1,10 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using EnvelopeGenerator.ReceiverUI
@using EnvelopeGenerator.ReceiverUI.Components

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,27 @@
using EnvelopeGenerator.ReceiverUI.Components;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();

View File

@@ -0,0 +1,38 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:26087",
"sslPort": 44331
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5134",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7124;http://localhost:5134",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,51 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,13 @@
@using Microsoft.AspNetCore.Components.Routing
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
<NotFound>
<LayoutView Layout="typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>EnvelopeGenerator.ReceiverUIBlazor</AssemblyName>
<RootNamespace>EnvelopeGenerator.ReceiverUIBlazor</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.0" PrivateAssets="all" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,428 @@
@page "/"
@using Microsoft.JSInterop
@inject IJSRuntime JS
@implements IAsyncDisposable
<div class="page-shell">
<h1>Sign PDF (Blazor)</h1>
<div class="controls">
<InputFile OnChange="HandleFileSelected" accept="application/pdf" />
<button class="btn" @onclick="ShowSignaturePad" disabled="@(!HasPdf)">Add signature</button>
<button class="btn" @onclick="() => ShowTextOverlay(false)" disabled="@(!HasPdf)">Add text</button>
<button class="btn" @onclick="() => ShowTextOverlay(true)" disabled="@(!HasPdf)">Add date</button>
<button class="btn" @onclick="Reset" disabled="@(!HasPdf)">Reset</button>
<button class="btn secondary" @onclick="Download" disabled="@(!HasPdf)">Download</button>
</div>
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
{
<div class="error-banner">@ErrorMessage</div>
}
@if (!HasPdf)
{
<div class="drop-hint">Drop or select a PDF to start.</div>
}
@if (HasPdf)
{
<div class="document-shell" @ref="PdfHostRef" style="@($"width:{ViewportWidthPx}px")">
<canvas id="pdf-canvas" @ref="PdfCanvasRef"></canvas>
@if (ShowSignature)
{
<div class="overlay signature" @ref="OverlayRef"
style="@($"left:{OverlayXpx}px; top:{OverlayYpx}px; width:{OverlayWidthPx}px; height:{OverlayHeightPx}px;")"
@onpointerdown="StartDrag" @onpointermove="OnDrag" @onpointerup="EndDrag" @onpointercancel="EndDrag">
<div class="overlay-controls">
<button class="overlay-btn" @onclick="ApplySignature" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✔</button>
<button class="overlay-btn" @onclick="CancelOverlay" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✖</button>
</div>
<img src="@SignatureDataUrl" draggable="false" />
</div>
}
@if (ShowText)
{
<div class="overlay text" @ref="OverlayRef"
style="@($"left:{OverlayXpx}px; top:{OverlayYpx}px;")"
@onpointerdown="StartDrag" @onpointermove="OnDrag" @onpointerup="EndDrag" @onpointercancel="EndDrag">
<div class="overlay-controls">
<button class="overlay-btn" @onclick="ApplyText" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✔</button>
<button class="overlay-btn" @onclick="CancelOverlay" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✖</button>
</div>
<input class="overlay-input" @bind="TextValue" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation />
</div>
}
</div>
<div class="paging">
<button class="btn" @onclick="PrevPage" disabled="@(!CanPrev)">&lt;</button>
<span>Page @DisplayPage / @PageCount</span>
<button class="btn" @onclick="NextPage" disabled="@(!CanNext)">&gt;</button>
</div>
}
</div>
@if (ShowSignaturePadModal)
{
<div class="modal-backdrop">
<div class="modal">
<h3>Add signature</h3>
<canvas id="@SignatureCanvasId" width="700" height="220"></canvas>
<div class="modal-row">
<label><input type="checkbox" @bind="AutoDate" /> Auto date/time</label>
<button class="btn" @onclick="ClearSignature">Clear</button>
<span class="spacer"></span>
<button class="btn" @onclick="ConfirmSignature">Use signature</button>
<button class="btn secondary" @onclick="CloseSignaturePad">Cancel</button>
</div>
</div>
</div>
}
@code {
private ElementReference PdfCanvasRef;
private ElementReference PdfHostRef;
private ElementReference OverlayRef;
private string? PdfBase64;
private string? OriginalPdfBase64;
private DotNetObjectReference<Index>? _dotNetRef;
private int PageIndex;
private int PageCount;
private double ViewportWidthPx = 800;
private double ViewportHeightPx;
private bool ShowSignaturePadModal;
private bool ShowSignature;
private bool ShowText;
private string SignatureCanvasId { get; } = $"sig-{Guid.NewGuid():N}";
private string? SignatureDataUrl;
private bool AutoDate = true;
private double OverlayXpx = 20;
private double OverlayYpx = 20;
private double OverlayWidthPx = 200;
private double OverlayHeightPx = 80;
private bool IsDragging;
private double DragStartX;
private double DragStartY;
private double StartLeft;
private double StartTop;
private string TextValue = "Text";
private string? ErrorMessage;
private DateTimeOffset _lastDragRender = DateTimeOffset.MinValue;
private bool HasPdf => !string.IsNullOrWhiteSpace(PdfBase64);
private int DisplayPage => PageIndex + 1;
private bool CanPrev => PageIndex > 0;
private bool CanNext => PageIndex + 1 < PageCount;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("pdfInterop.ensureReady");
_dotNetRef ??= DotNetObjectReference.Create(this);
await JS.InvokeVoidAsync("pdfInterop.registerDropHandler", _dotNetRef);
}
if (ShowSignaturePadModal)
{
await JS.InvokeVoidAsync("pdfInterop.initSignaturePad", SignatureCanvasId);
}
}
private async Task HandleFileSelected(InputFileChangeEventArgs e)
{
if (e.FileCount == 0)
{
return;
}
await LoadPdfFromBrowserFile(e.File);
}
private async Task LoadPdfFromBrowserFile(IBrowserFile file)
{
ErrorMessage = null;
try
{
await using var stream = file.OpenReadStream(maxAllowedSize: 20 * 1024 * 1024);
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
await LoadPdfFromBase64Internal(Convert.ToBase64String(ms.ToArray()));
}
catch (Exception ex)
{
ErrorMessage = $"Fehler beim Laden der PDF: {ex.Message}";
PdfBase64 = null;
PageCount = 0;
PageIndex = 0;
}
}
[JSInvokable]
public Task LoadPdfFromBase64(string base64)
{
return LoadPdfFromBase64Internal(base64);
}
private async Task LoadPdfFromBase64Internal(string base64)
{
ErrorMessage = null;
PdfBase64 = base64;
OriginalPdfBase64 = PdfBase64;
// Show the canvas before we start rendering
await InvokeAsync(StateHasChanged);
await Task.Yield();
// Make sure pdf.js is ready
await JS.InvokeVoidAsync("pdfInterop.ensureReady");
var result = await JS.InvokeAsync<RenderResult>("pdfInterop.loadPdf", PdfBase64);
PageCount = result.Pages;
PageIndex = 0;
await RenderPage();
}
private async Task RenderPage()
{
if (!HasPdf)
{
return;
}
try
{
var viewport = await JS.InvokeAsync<ViewportInfo>("pdfInterop.renderPage", PageIndex, "pdf-canvas", ViewportWidthPx);
ViewportWidthPx = viewport.Width;
ViewportHeightPx = viewport.Height;
StateHasChanged();
}
catch (Exception ex)
{
ErrorMessage = $"Fehler beim Rendern: {ex.Message}";
}
}
private async Task Reset()
{
ErrorMessage = null;
CloseOverlays();
ShowSignaturePadModal = false;
OverlayXpx = 20;
OverlayYpx = 20;
OverlayWidthPx = 200;
OverlayHeightPx = 80;
TextValue = "Text";
if (string.IsNullOrWhiteSpace(OriginalPdfBase64))
{
return;
}
PdfBase64 = OriginalPdfBase64;
PageIndex = 0;
var result = await JS.InvokeAsync<RenderResult>("pdfInterop.loadPdf", PdfBase64);
PageCount = result.Pages;
await RenderPage();
}
private void CloseOverlays()
{
ShowSignature = false;
ShowText = false;
SignatureDataUrl = null;
}
private void ShowSignaturePad()
{
ShowSignaturePadModal = true;
}
private async Task ConfirmSignature()
{
SignatureDataUrl = await JS.InvokeAsync<string>("pdfInterop.getSignatureDataUrl", SignatureCanvasId);
if (string.IsNullOrWhiteSpace(SignatureDataUrl))
{
return;
}
OverlayWidthPx = 200;
OverlayHeightPx = 80;
OverlayXpx = 20;
OverlayYpx = 20;
ShowSignature = true;
ShowText = false;
ShowSignaturePadModal = false;
}
private void CloseSignaturePad()
{
ShowSignaturePadModal = false;
}
private void ClearSignature()
{
JS.InvokeVoidAsync("pdfInterop.clearSignaturePad", SignatureCanvasId);
}
private void ShowTextOverlay(bool autoDate)
{
TextValue = autoDate ? DateTimeOffset.Now.ToString("M/d/yyyy HH:mm:ss zzz") : "Text";
OverlayWidthPx = 240;
OverlayHeightPx = 40;
OverlayXpx = 20;
OverlayYpx = 20;
ShowText = true;
ShowSignature = false;
}
private void StartDrag(PointerEventArgs args)
{
IsDragging = true;
DragStartX = args.ClientX;
DragStartY = args.ClientY;
StartLeft = OverlayXpx;
StartTop = OverlayYpx;
if (OverlayRef.Context != null)
{
JS.InvokeVoidAsync("pdfInterop.capturePointer", OverlayRef, args.PointerId);
}
}
private void OnDrag(PointerEventArgs args)
{
if (!IsDragging)
{
return;
}
var dx = args.ClientX - DragStartX;
var dy = args.ClientY - DragStartY;
OverlayXpx = StartLeft + dx;
OverlayYpx = StartTop + dy;
var now = DateTimeOffset.UtcNow;
if (now - _lastDragRender > TimeSpan.FromMilliseconds(16))
{
_lastDragRender = now;
InvokeAsync(StateHasChanged);
}
}
private void EndDrag(PointerEventArgs args)
{
IsDragging = false;
if (OverlayRef.Context != null)
{
JS.InvokeVoidAsync("pdfInterop.releasePointer", OverlayRef, args.PointerId);
}
}
private async Task ApplySignature()
{
if (SignatureDataUrl is null || !HasPdf)
{
return;
}
PdfBase64 = await JS.InvokeAsync<string>("pdfInterop.applySignature", new
{
base64 = PdfBase64,
pageIndex = PageIndex,
left = OverlayXpx,
top = OverlayYpx,
width = OverlayWidthPx,
height = OverlayHeightPx,
renderWidth = ViewportWidthPx,
renderHeight = ViewportHeightPx,
dataUrl = SignatureDataUrl,
autoDate = AutoDate,
});
CloseOverlays();
await RenderPage();
}
private async Task ApplyText()
{
if (string.IsNullOrWhiteSpace(TextValue) || !HasPdf)
{
return;
}
PdfBase64 = await JS.InvokeAsync<string>("pdfInterop.applyText", new
{
base64 = PdfBase64,
pageIndex = PageIndex,
left = OverlayXpx,
top = OverlayYpx,
width = OverlayWidthPx,
height = OverlayHeightPx,
renderWidth = ViewportWidthPx,
renderHeight = ViewportHeightPx,
text = TextValue,
fontSize = 20,
});
CloseOverlays();
await RenderPage();
}
private void CancelOverlay()
{
CloseOverlays();
}
private async Task PrevPage()
{
if (!CanPrev)
{
return;
}
PageIndex--;
await RenderPage();
}
private async Task NextPage()
{
if (!CanNext)
{
return;
}
PageIndex++;
await RenderPage();
}
private async Task Download()
{
if (!HasPdf)
{
return;
}
await JS.InvokeVoidAsync("pdfInterop.downloadPdf", PdfBase64, "document-signed.pdf");
}
private record RenderResult(int Pages);
private record ViewportInfo(double Width, double Height, double PageWidth, double PageHeight);
public async ValueTask DisposeAsync()
{
_dotNetRef?.Dispose();
await Task.CompletedTask;
}
}

View File

@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using EnvelopeGenerator.ReceiverUIBlazor;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<WebPublishMethod>Package</WebPublishMethod>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<SiteUrlToLaunchAfterPublish />
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<ExcludeApp_Data>false</ExcludeApp_Data>
<ProjectGuid>7f262ad4-53b1-42d3-9a5f-132cf50f150c</ProjectGuid>
<DesktopBuildPackageLocation>E:\TekH\Visual Studio\src\EnvelopeGenerator.ReceiverUIBlazor\EnvelopeGenerator.ReceiverUIBlazor.zip</DesktopBuildPackageLocation>
<PackageAsSingleFile>true</PackageAsSingleFile>
<DeployIisAppPath>ReceiverUIBlazor</DeployIisAppPath>
<_TargetId>IISWebDeployPackage</_TargetId>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,12 @@
{
"profiles": {
"EnvelopeGenerator.ReceiverUIBlazor": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:49582;http://localhost:49583"
}
}
}

View File

@@ -0,0 +1,10 @@
@inherits LayoutComponentBase
<div class="main-layout">
<header class="top-bar">
<div class="brand">Receiver UI (Blazor)</div>
</header>
<main class="content">
@Body
</main>
</div>

View File

@@ -0,0 +1,8 @@
@using System.Net.Http
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.WebAssembly.Hosting
@using Microsoft.JSInterop
@using EnvelopeGenerator.ReceiverUIBlazor
@using EnvelopeGenerator.ReceiverUIBlazor.Shared

View File

@@ -0,0 +1,241 @@
@import url('https://fonts.googleapis.com/css2?family=Red+Hat+Text:wght@400;500;600;700&family=Teko:wght@500;600&display=swap');
:root {
--bg: #f7f7f8;
--bg-strong: #fff6f6;
--text: #474747;
--muted: #777777;
--border: #e7e7e7;
--shadow: 0 18px 55px rgba(20, 20, 20, 0.08);
--card: #ffffff;
--accent: #a52431;
--accent-strong: #8d1e2a;
--accent-soft: #f8e5e8;
--highlight: #ffd62f;
--danger: #a52431;
}
body {
margin: 0;
font-family: "Red Hat Text", "Segoe UI", system-ui, -apple-system, sans-serif;
background: radial-gradient(120% 120% at 6% 12%, var(--bg-strong) 0%, #fffdf7 45%, var(--bg) 85%);
color: var(--text);
line-height: 1.5;
}
.main-layout {
min-height: 100vh;
}
.top-bar {
display: flex;
align-items: center;
padding: 14px 24px;
background: var(--accent);
border-bottom: 1px solid var(--accent-strong);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
color: #ffd62f;
}
.top-bar .brand {
font-family: "Teko", "Red Hat Text", sans-serif;
font-weight: 600;
letter-spacing: 0.6px;
color: #ffd62f;
}
.content {
padding: 28px 32px 40px;
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
}
h1 {
margin: 0 0 10px;
letter-spacing: 0.2px;
font-family: "Teko", "Red Hat Text", sans-serif;
font-weight: 600;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 14px;
justify-content: center;
}
.btn {
background: #4a4a4a;
color: #ffffff;
border: 1px solid #404040;
padding: 10px 15px;
border-radius: 12px;
font-weight: 700;
cursor: pointer;
transition: transform 120ms ease, box-shadow 120ms ease, background-color 120ms ease;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
}
.btn:hover:not(:disabled) {
transform: translateY(-1px);
background: #3f3f3f;
}
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
box-shadow: none;
}
.btn.secondary {
background: #f4f4f4;
color: #474747;
border: 1px solid #d3d3d3;
box-shadow: none;
}
.btn.secondary:hover:not(:disabled) {
background: #e9e9e9;
transform: translateY(-1px);
}
.drop-hint {
padding: 26px;
border: 1px dashed var(--border);
border-radius: 14px;
text-align: center;
color: var(--muted);
background: #ffffff;
width: min(1100px, 100%);
margin: 0 auto;
box-shadow: 0 8px 18px rgba(165, 36, 49, 0.06);
}
.error-banner {
margin-top: 10px;
padding: 12px 14px;
border-radius: 10px;
background: #fcebec;
border: 1px solid #f3c6cd;
color: var(--accent-strong);
}
.document-shell {
position: relative;
margin-top: 14px;
border-radius: 14px;
overflow: hidden;
box-shadow: var(--shadow);
background: var(--card);
border: 1px solid var(--border);
margin-left: auto;
margin-right: auto;
}
canvas {
display: block;
width: 100%;
height: auto;
background: #ffffff;
}
.overlay {
position: absolute;
border: 1px dashed var(--accent);
border-radius: 10px;
padding: 8px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 12px 30px rgba(165, 36, 49, 0.15);
user-select: none;
touch-action: none;
}
.overlay.signature img {
display: block;
max-width: 100%;
max-height: 100%;
}
.overlay-controls {
position: absolute;
top: -44px;
right: 0;
display: flex;
gap: 6px;
}
.overlay-btn {
background: #ffffff;
color: var(--accent-strong);
border: 1px solid var(--accent);
border-radius: 8px;
padding: 6px 10px;
cursor: pointer;
box-shadow: 0 6px 16px rgba(165, 36, 49, 0.16);
}
.overlay-input {
border: 1px solid var(--border);
background: #ffffff;
color: var(--text);
font-size: 18px;
padding: 6px 8px;
min-width: 180px;
border-radius: 8px;
outline: none;
}
.paging {
margin-top: 14px;
display: flex;
gap: 10px;
align-items: center;
color: var(--muted);
}
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(15, 23, 42, 0.35);
backdrop-filter: blur(3px);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal {
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 18px;
min-width: 760px;
box-shadow: var(--shadow);
}
.modal-row {
display: flex;
align-items: center;
gap: 12px;
margin-top: 12px;
color: var(--muted);
}
.modal canvas {
background: #ffffff;
border: 1px solid var(--border);
border-radius: 10px;
}
.spacer {
flex: 1;
}

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Receiver UI (Blazor)</title>
<base href="/" />
<link rel="stylesheet" href="css/app.css" />
<!-- pdf.js 3.11 UMD + classic worker for compatibility; SRI removed to avoid digest mismatches -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js"></script>
<script src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js"></script>
<script src="js/pdfInterop.js"></script>
</head>
<body>
<div id="app">Loading...</div>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

View File

@@ -0,0 +1,339 @@
(function () {
// Stick to pdf.js 3.11 UMD + classic worker for compatibility.
const PDF_JS_SRC = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js";
const WORKER_SRC = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
const state = {
pdfDoc: null,
pdfBytes: null,
lastViewport: null,
pdfJsReady: null,
};
function base64ToUint8(base64) {
const binStr = atob(base64);
const len = binStr.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binStr.charCodeAt(i);
}
return bytes;
}
async function reloadFromBase64(base64) {
state.pdfBytes = base64ToUint8(base64);
state.pdfDoc = await pdfjsLib.getDocument({ data: state.pdfBytes }).promise;
return { pages: state.pdfDoc.numPages };
}
function dataUrlDownload(dataUrl, filename) {
const a = document.createElement('a');
a.href = dataUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
const pointerPads = new Map();
function loadScriptOnce(url) {
return new Promise((resolve, reject) => {
// If already present, resolve immediately
const existing = Array.from(document.getElementsByTagName('script')).find(s => s.src === url);
if (existing && existing.dataset.loaded === "true") {
resolve();
return;
}
const script = existing || document.createElement('script');
script.src = url;
script.defer = true;
script.onload = () => {
script.dataset.loaded = "true";
resolve();
};
script.onerror = (e) => reject(new Error(`Script load failed: ${url}`));
if (!existing) {
document.head.appendChild(script);
}
});
}
async function ensurePdfJsLoaded() {
if (typeof pdfjsLib !== "undefined") {
return;
}
if (!state.pdfJsReady) {
state.pdfJsReady = loadScriptOnce(PDF_JS_SRC);
}
await state.pdfJsReady;
if (typeof pdfjsLib === "undefined") {
throw new Error("pdfjsLib could not be loaded");
}
}
window.pdfInterop = {
ensureReady: async () => {
// Ensure pdf.js is present and the worker path is set explicitly.
await ensurePdfJsLoaded();
if (pdfjsLib && pdfjsLib.GlobalWorkerOptions) {
if (pdfjsLib.GlobalWorkerOptions.workerSrc !== WORKER_SRC) {
pdfjsLib.GlobalWorkerOptions.workerSrc = WORKER_SRC;
}
} else {
throw new Error("pdf.js not available after load");
}
},
loadPdf: async (base64) => {
await ensurePdfJsLoaded();
try {
const result = await reloadFromBase64(base64);
if (!result || !result.pages) {
throw new Error("PDF has keine Seiten erkannt");
}
return result;
} catch (err) {
console.error("pdfInterop.loadPdf failed", err);
throw err;
}
},
renderPage: async (pageIndex, canvasId, targetWidth) => {
await ensurePdfJsLoaded();
if (!state.pdfDoc) {
throw new Error('PDF not loaded');
}
const page = await state.pdfDoc.getPage(pageIndex + 1);
const rawViewport = page.getViewport({ scale: 1 });
const scale = targetWidth / rawViewport.width;
const viewport = page.getViewport({ scale });
let canvas = document.getElementById(canvasId);
if (!canvas) {
// give the UI a tiny delay to render the canvas into the DOM
await new Promise(r => setTimeout(r, 40));
canvas = document.getElementById(canvasId);
}
if (!canvas) {
console.error("renderPage: canvas not found", canvasId);
throw new Error('Canvas not found');
}
const ctx = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: ctx, viewport }).promise;
state.lastViewport = {
width: viewport.width,
height: viewport.height,
pageWidth: rawViewport.width,
pageHeight: rawViewport.height,
};
return state.lastViewport;
},
applySignature: async (payload) => {
const {
base64,
pageIndex,
left,
top,
width,
height,
renderWidth,
renderHeight,
dataUrl,
autoDate,
} = payload;
const pdfDoc = await PDFLib.PDFDocument.load(base64ToUint8(base64));
const page = pdfDoc.getPage(pageIndex);
const scaleX = page.getWidth() / renderWidth;
const scaleY = page.getHeight() / renderHeight;
const pngImage = await pdfDoc.embedPng(dataUrl);
const drawWidth = width * scaleX;
const drawHeight = height * scaleY;
const x = left * scaleX;
const y = page.getHeight() - (top + height) * scaleY;
page.drawImage(pngImage, {
x,
y,
width: drawWidth,
height: drawHeight,
});
if (autoDate) {
const text = `Signed ${new Date().toLocaleString()}`;
page.drawText(text, {
x,
y: y - 14 * scaleY,
size: 14 * scaleX,
color: PDFLib.rgb(0.11, 0.25, 0.56),
});
}
const updatedBase64 = await pdfDoc.saveAsBase64({ dataUri: false });
await reloadFromBase64(updatedBase64);
return updatedBase64;
},
applyText: async (payload) => {
const {
base64,
pageIndex,
left,
top,
width,
height,
renderWidth,
renderHeight,
text,
fontSize,
} = payload;
const pdfDoc = await PDFLib.PDFDocument.load(base64ToUint8(base64));
const page = pdfDoc.getPage(pageIndex);
const scaleX = page.getWidth() / renderWidth;
const scaleY = page.getHeight() / renderHeight;
const x = left * scaleX;
const y = page.getHeight() - (top + height) * scaleY;
page.drawText(text, {
x,
y,
size: fontSize * scaleX,
color: PDFLib.rgb(0.2, 0.23, 0.28),
});
const updatedBase64 = await pdfDoc.saveAsBase64({ dataUri: false });
await reloadFromBase64(updatedBase64);
return updatedBase64;
},
downloadPdf: (base64, filename) => {
dataUrlDownload(`data:application/pdf;base64,${base64}`, filename);
},
initSignaturePad: (canvasId) => {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.strokeStyle = '#1c3d8f';
const padState = {
drawing: false,
lastX: 0,
lastY: 0,
};
function getPos(evt) {
const rect = canvas.getBoundingClientRect();
const scaleX = rect.width ? canvas.width / rect.width : 1;
const scaleY = rect.height ? canvas.height / rect.height : 1;
return {
x: (evt.clientX - rect.left) * scaleX,
y: (evt.clientY - rect.top) * scaleY,
};
}
function start(e) {
padState.drawing = true;
const pos = getPos(e);
padState.lastX = pos.x;
padState.lastY = pos.y;
}
function move(e) {
if (!padState.drawing) return;
const pos = getPos(e);
const x = pos.x;
const y = pos.y;
ctx.beginPath();
ctx.moveTo(padState.lastX, padState.lastY);
ctx.lineTo(x, y);
ctx.stroke();
padState.lastX = x;
padState.lastY = y;
}
function end() {
padState.drawing = false;
}
canvas.onpointerdown = start;
canvas.onpointermove = move;
canvas.onpointerup = end;
canvas.onpointerleave = end;
pointerPads.set(canvasId, { ctx, canvas });
},
registerDropHandler: (dotNetRef) => {
if (window.__pdfDropRegistered) return;
window.__pdfDropRegistered = true;
const prevent = (e) => {
e.preventDefault();
e.stopPropagation();
};
['dragenter', 'dragover', 'dragleave'].forEach(evt => {
document.addEventListener(evt, prevent, false);
});
document.addEventListener('drop', (e) => {
prevent(e);
const files = e.dataTransfer?.files;
if (!files || files.length === 0) {
return;
}
const file = files[0];
const reader = new FileReader();
reader.onload = () => {
const result = reader.result;
if (typeof result === 'string') {
const base64 = result.split(',')[1] || result;
dotNetRef?.invokeMethodAsync('LoadPdfFromBase64', base64);
}
};
reader.readAsDataURL(file);
}, false);
},
clearSignaturePad: (canvasId) => {
const pad = pointerPads.get(canvasId);
if (!pad) return;
pad.ctx.clearRect(0, 0, pad.canvas.width, pad.canvas.height);
},
getSignatureDataUrl: (canvasId) => {
const pad = pointerPads.get(canvasId);
if (!pad) return null;
return pad.canvas.toDataURL('image/png');
},
capturePointer: (element, pointerId) => {
if (element && element.setPointerCapture) {
try {
element.setPointerCapture(pointerId);
} catch (err) {
console.warn('capturePointer failed', err);
}
}
},
releasePointer: (element, pointerId) => {
if (element && element.releasePointerCapture) {
try {
element.releasePointerCapture(pointerId);
} catch (err) {
console.warn('releasePointer failed', err);
}
}
}
};
})();

View File

@@ -0,0 +1,52 @@
{
"homepage": "https://slavik0329.github.io/pdf-sign",
"name": "pdf-sign",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"annotpdf": "^1.0.12",
"dayjs": "^1.9.6",
"pdf-lib": "^1.12.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-draggable": "^4.4.3",
"react-dropzone": "^11.2.4",
"react-icons": "^4.1.0",
"react-pdf": "^5.0.0",
"react-scripts": "4.0.1",
"react-signature-canvas": "^1.0.3",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"gh-pages": "^3.1.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Finally, a simple way to put your signature on a PDF for free."
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>PDF-sign</title>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;800&display=swap" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,10 @@
body {
font-family: "Open Sans";
}
input:focus,
select:focus,
textarea:focus,
button:focus {
outline: none;
}

View File

@@ -0,0 +1,278 @@
import "./App.css";
import { useRef, useState } from "react";
import Drop from "./Drop";
import { Document, Page, pdfjs } from "react-pdf";
import { PDFDocument, rgb } from "pdf-lib";
import { blobToURL } from "./utils/Utils";
import PagingControl from "./components/PagingControl";
import { AddSigDialog } from "./components/AddSigDialog";
import { Header } from "./Header";
import { BigButton } from "./components/BigButton";
import DraggableSignature from "./components/DraggableSignature";
import DraggableText from "./components/DraggableText";
import dayjs from "dayjs";
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
function downloadURI(uri, name) {
var link = document.createElement("a");
link.download = name;
link.href = uri;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function App() {
const styles = {
container: {
maxWidth: 900,
margin: "0 auto",
},
sigBlock: {
display: "inline-block",
border: "1px solid #000",
},
documentBlock: {
maxWidth: 800,
margin: "20px auto",
marginTop: 8,
border: "1px solid #999",
},
controls: {
maxWidth: 800,
margin: "0 auto",
marginTop: 8,
},
};
const [pdf, setPdf] = useState(null);
const [autoDate, setAutoDate] = useState(true);
const [signatureURL, setSignatureURL] = useState(null);
const [position, setPosition] = useState(null);
const [signatureDialogVisible, setSignatureDialogVisible] = useState(false);
const [textInputVisible, setTextInputVisible] = useState(false);
const [pageNum, setPageNum] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [pageDetails, setPageDetails] = useState(null);
const documentRef = useRef(null);
return (
<div>
<Header />
<div style={styles.container}>
{signatureDialogVisible ? (
<AddSigDialog
autoDate={autoDate}
setAutoDate={setAutoDate}
onClose={() => setSignatureDialogVisible(false)}
onConfirm={(url) => {
setSignatureURL(url);
setSignatureDialogVisible(false);
}}
/>
) : null}
{!pdf ? (
<Drop
onLoaded={async (files) => {
const URL = await blobToURL(files[0]);
setPdf(URL);
}}
/>
) : null}
{pdf ? (
<div>
<div style={styles.controls}>
{!signatureURL ? (
<BigButton
marginRight={8}
title={"Add signature"}
onClick={() => setSignatureDialogVisible(true)}
/>
) : null}
<BigButton
marginRight={8}
title={"Add Date"}
onClick={() => setTextInputVisible("date")}
/>
<BigButton
marginRight={8}
title={"Add Text"}
onClick={() => setTextInputVisible(true)}
/>
<BigButton
marginRight={8}
title={"Reset"}
onClick={() => {
setTextInputVisible(false);
setSignatureDialogVisible(false);
setSignatureURL(null);
setPdf(null);
setTotalPages(0);
setPageNum(0);
setPageDetails(null);
}}
/>
{pdf ? (
<BigButton
marginRight={8}
inverted={true}
title={"Download"}
onClick={() => {
downloadURI(pdf, "file.pdf");
}}
/>
) : null}
</div>
<div ref={documentRef} style={styles.documentBlock}>
{textInputVisible ? (
<DraggableText
initialText={
textInputVisible === "date"
? dayjs().format("M/d/YYYY")
: null
}
onCancel={() => setTextInputVisible(false)}
onEnd={setPosition}
onSet={async (text) => {
const { originalHeight, originalWidth } = pageDetails;
const scale = originalWidth / documentRef.current.clientWidth;
const y =
documentRef.current.clientHeight -
(position.y +
(12 * scale) -
position.offsetY -
documentRef.current.offsetTop);
const x =
position.x -
166 -
position.offsetX -
documentRef.current.offsetLeft;
// new XY in relation to actual document size
const newY =
(y * originalHeight) / documentRef.current.clientHeight;
const newX =
(x * originalWidth) / documentRef.current.clientWidth;
const pdfDoc = await PDFDocument.load(pdf);
const pages = pdfDoc.getPages();
const firstPage = pages[pageNum];
firstPage.drawText(text, {
x: newX,
y: newY,
size: 20 * scale,
});
const pdfBytes = await pdfDoc.save();
const blob = new Blob([new Uint8Array(pdfBytes)]);
const URL = await blobToURL(blob);
setPdf(URL);
setPosition(null);
setTextInputVisible(false);
}}
/>
) : null}
{signatureURL ? (
<DraggableSignature
url={signatureURL}
onCancel={() => {
setSignatureURL(null);
}}
onSet={async () => {
const { originalHeight, originalWidth } = pageDetails;
const scale = originalWidth / documentRef.current.clientWidth;
const y =
documentRef.current.clientHeight -
(position.y -
position.offsetY +
64 -
documentRef.current.offsetTop);
const x =
position.x -
160 -
position.offsetX -
documentRef.current.offsetLeft;
// new XY in relation to actual document size
const newY =
(y * originalHeight) / documentRef.current.clientHeight;
const newX =
(x * originalWidth) / documentRef.current.clientWidth;
const pdfDoc = await PDFDocument.load(pdf);
const pages = pdfDoc.getPages();
const firstPage = pages[pageNum];
const pngImage = await pdfDoc.embedPng(signatureURL);
const pngDims = pngImage.scale( scale * .3);
firstPage.drawImage(pngImage, {
x: newX,
y: newY,
width: pngDims.width,
height: pngDims.height,
});
if (autoDate) {
firstPage.drawText(
`Signed ${dayjs().format(
"M/d/YYYY HH:mm:ss ZZ"
)}`,
{
x: newX,
y: newY - 10,
size: 14 * scale,
color: rgb(0.074, 0.545, 0.262),
}
);
}
const pdfBytes = await pdfDoc.save();
const blob = new Blob([new Uint8Array(pdfBytes)]);
const URL = await blobToURL(blob);
setPdf(URL);
setPosition(null);
setSignatureURL(null);
}}
onEnd={setPosition}
/>
) : null}
<Document
file={pdf}
onLoadSuccess={(data) => {
setTotalPages(data.numPages);
}}
>
<Page
pageNumber={pageNum + 1}
width={800}
height={1200}
onLoadSuccess={(data) => {
setPageDetails(data);
}}
/>
</Document>
</div>
<PagingControl
pageNum={pageNum}
setPageNum={setPageNum}
totalPages={totalPages}
/>
</div>
) : null}
</div>
</div>
);
}
export default App;

View File

@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -0,0 +1,37 @@
import React, { useCallback } from "react";
import { useDropzone } from "react-dropzone";
import { cleanBorder, primary45 } from "./utils/colors";
export default function Drop({ onLoaded }) {
const styles = {
container: {
textAlign: "center",
border: cleanBorder,
padding: 20,
marginTop: 12,
color: primary45,
fontSize: 18,
fontWeight: 600,
borderRadius: 4,
userSelect: "none",
outline: 0,
cursor: "pointer",
},
};
const onDrop = useCallback((acceptedFiles) => {
onLoaded(acceptedFiles);
// Do something with the files
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: "application/pdf",
});
return (
<div {...getRootProps()} style={styles.container}>
<input {...getInputProps()} />
{isDragActive ? <p>Drop a PDF here</p> : <p>Drag a PDF here</p>}
</div>
);
}

View File

@@ -0,0 +1,15 @@
import {primary45} from "./utils/colors";
export function Header() {
const styles = {
container: {
backgroundColor: primary45,
color: '#FFF',
padding: 12,
fontWeight: 600,
}
}
return <div style={styles.container}>
<div>Open PDF Sign</div>
</div>
}

View File

@@ -0,0 +1,77 @@
import { Dialog } from "./Dialog";
import SignatureCanvas from "react-signature-canvas";
import { ConfirmOrCancel } from "./ConfirmOrCancel";
import { primary45 } from "../utils/colors";
import { useRef } from "react";
export function AddSigDialog({ onConfirm, onClose, autoDate, setAutoDate }) {
const sigRef = useRef(null);
const styles = {
sigContainer: {
display: "flex",
justifyContent: "center",
},
sigBlock: {
display: "inline-block",
border: `1px solid ${primary45}`,
},
instructions: {
display: "flex",
justifyContent: "space-between",
textAlign: "center",
color: primary45,
marginTop: 8,
width: 600,
alignSelf: "center",
},
instructionsContainer: {
display: "flex",
justifyContent: "center",
},
};
return (
<Dialog
isVisible={true}
title={"Add signature"}
body={
<div style={styles.container}>
<div style={styles.sigContainer}>
<div style={styles.sigBlock}>
<SignatureCanvas
velocityFilterWeight={1}
ref={sigRef}
canvasProps={{
width: "600",
height: 200,
className: "sigCanvas",
}}
/>
</div>
</div>
<div style={styles.instructionsContainer}>
<div style={styles.instructions}>
<div>
Auto date/time{" "}
<input
type={"checkbox"}
checked={autoDate}
onChange={(e) => setAutoDate(e.target.checked)}
/>
</div>
<div>Draw your signature above</div>
</div>
</div>
<ConfirmOrCancel
onCancel={onClose}
onConfirm={() => {
const sigURL = sigRef.current.toDataURL();
onConfirm(sigURL);
}}
/>
</div>
}
/>
);
}

View File

@@ -0,0 +1,81 @@
import React from "react";
import { primary45 } from "../utils/colors";
import useHover from "../hooks/useHover";
export function BigButton({
title,
onClick,
inverted,
fullWidth,
customFillColor,
customWhiteColor,
style,
noHover,
id,
small,
disabled,
marginRight,
}) {
const [hoverRef, isHovered] = useHover();
let fillColor = customFillColor || primary45;
const whiteColor = customWhiteColor || "#FFF";
let initialBg = null;
let hoverBg = fillColor;
let initialColor = fillColor;
let hoverColor = whiteColor;
if (inverted) {
initialBg = fillColor;
hoverBg = null;
initialColor = whiteColor;
hoverColor = fillColor;
}
if (disabled) {
initialBg = "#ddd";
hoverBg = "#ddd";
fillColor = "#ddd";
}
const styles = {
container: {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: fullWidth ? "100%" : null,
backgroundColor: isHovered && !noHover ? hoverBg : initialBg,
color:
isHovered && !noHover && !disabled
? hoverColor
: disabled
? "#999"
: initialColor,
borderRadius: 4,
padding: small ? "2px 4px" : "6px 8px",
fontSize: small ? 14 : null,
border: `1px solid ${fillColor}`,
cursor: !disabled ? "pointer" : null,
userSelect: "none",
boxSizing: "border-box",
marginRight,
},
};
return (
<div
id={id}
ref={hoverRef}
style={{ ...styles.container, ...style }}
onClick={() => {
if (!disabled) {
onClick();
}
}}
>
{title}
</div>
);
}

View File

@@ -0,0 +1,37 @@
import { BigButton } from "./BigButton";
import React from "react";
export function ConfirmOrCancel({
onCancel,
onConfirm,
confirmTitle = "Confirm",
leftBlock,
hideCancel,
disabled
}) {
const styles = {
actions: {
display: "flex",
justifyContent: "space-between",
},
cancel: {
marginRight: 8,
},
};
return (
<div style={styles.actions}>
<div>{leftBlock}</div>
<div>
{!hideCancel ? (
<BigButton
title={"Cancel"}
style={styles.cancel}
onClick={onCancel}
/>
) : null}
<BigButton title={confirmTitle} inverted={true} onClick={onConfirm} disabled={disabled}/>
</div>
</div>
);
}

View File

@@ -0,0 +1,56 @@
import React from 'react';
import {primary45} from '../utils/colors';
import {FaTimes} from 'react-icons/fa';
import {Modal} from './Modal';
export function Dialog({
isVisible,
body,
onClose,
title,
noPadding,
backgroundColor,
positionTop,
style,
}) {
if (!isVisible) {
return null;
}
const styles = {
header: {
backgroundColor: primary45,
color: '#FFF',
padding: 8,
fontSize: 14,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
body: {
padding: noPadding ? 0 : 14,
backgroundColor: backgroundColor ? backgroundColor : '#FFF',
},
xIcon: {
cursor: 'pointer',
},
};
return (
<Modal onClose={onClose} isVisible={isVisible} positionTop={positionTop} style={style}>
<div style={styles.container}>
<div style={styles.header}>
<div>{title}</div>
<FaTimes
color={'#FFF'}
size={16}
style={styles.xIcon}
className={'dialogClose'}
onClick={onClose}
/>
</div>
<div style={styles.body}>{body}</div>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,37 @@
import Draggable from "react-draggable";
import {BigButton} from "./BigButton"; // The default
import {FaCheck, FaTimes} from 'react-icons/fa'
import {cleanBorder, errorColor, goodColor, primary45} from "../utils/colors";
export default function DraggableSignature({ url, onEnd, onSet, onCancel }) {
const styles = {
container: {
position: 'absolute',
zIndex: 100000,
border: `2px solid ${primary45}`,
},
controls: {
position: 'absolute',
right: 0,
display: 'inline-block',
backgroundColor: primary45,
// borderRadius: 4,
},
smallButton: {
display: 'inline-block',
cursor: 'pointer',
padding: 4,
}
}
return (
<Draggable onStop={onEnd}>
<div style={styles.container}>
<div style={styles.controls}>
<div style={styles.smallButton} onClick={onSet}><FaCheck color={goodColor}/></div>
<div style={styles.smallButton} onClick={onCancel}><FaTimes color={errorColor}/></div>
</div>
<img src={url} width={200} style={styles.img} draggable={false} />
</div>
</Draggable>
);
}

View File

@@ -0,0 +1,66 @@
import Draggable from "react-draggable";
import { FaCheck, FaTimes } from "react-icons/fa";
import { cleanBorder, errorColor, goodColor, primary45 } from "../utils/colors";
import { useState, useEffect, useRef } from "react";
export default function DraggableText({ onEnd, onSet, onCancel, initialText }) {
const [text, setText] = useState("Text");
const inputRef = useRef(null);
useEffect(() => {
if (initialText) {
setText(initialText)
} else {
inputRef.current.focus();
inputRef.current.select()
}
}, [])
const styles = {
container: {
position: "absolute",
zIndex: 100000,
border: `2px solid ${primary45}`,
},
controls: {
position: "absolute",
right: 0,
display: "inline-block",
backgroundColor: primary45,
// borderRadius: 4,
},
smallButton: {
display: "inline-block",
cursor: "pointer",
padding: 4,
},
input: {
border: 0,
fontSize: 20,
padding: 3,
backgroundColor: 'rgba(0,0,0,0)',
cursor: 'move'
}
};
return (
<Draggable onStop={onEnd}>
<div style={styles.container}>
<div style={styles.controls}>
<div style={styles.smallButton} onClick={()=>onSet(text)}>
<FaCheck color={goodColor} />
</div>
<div style={styles.smallButton} onClick={onCancel}>
<FaTimes color={errorColor} />
</div>
</div>
<input
ref={inputRef}
style={styles.input}
value={text}
placeholder={'Text'}
onChange={(e) => setText(e.target.value)}
/>
</div>
</Draggable>
);
}

View File

@@ -0,0 +1,43 @@
import React from 'react';
import {primary45} from '../utils/colors';
import {useIsSmallScreen} from '../hooks/useIsSmallScreen';
export function Modal({onClose, children, isVisible, style, positionTop}) {
const isSmallScreen = useIsSmallScreen();
const styles = {
container: {
position: isSmallScreen ? 'fixed' : 'absolute',
backgroundColor: '#FFF',
border: `1px solid ${primary45}`,
borderRadius: 4,
top: positionTop ? positionTop : isSmallScreen ? 60 : 150,
left: '50%',
transform: 'translateX(-50%)',
width: '94%',
fontFamily: 'Open Sans',
zIndex: 10000,
boxShadow: '0 0px 14px hsla(0, 0%, 0%, 0.2)',
},
background: {
position: 'fixed',
width: '100%',
height: '100%',
top: 0,
left: 0,
backgroundColor: '#00000033',
zIndex: 5000,
},
};
if (!isVisible) {
return null;
}
return (
<div style={styles.outer}>
<div style={styles.background} onClick={onClose} />
<div style={{...styles.container, ...style}}>{children}</div>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { BigButton } from "./BigButton";
import {primary45} from "../utils/colors";
export default function PagingControl({totalPages, pageNum, setPageNum}) {
const styles= {
container: {
marginTop: 8,
marginBottom: 8,
},
inlineFlex: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
},
pageInfo: {
padding: 8,
color: primary45,
fontSize: 14,
}
}
return (
<div style={styles.container}>
<div style={styles.inlineFlex}>
<BigButton
title={"<"}
onClick={() => setPageNum(pageNum - 1)}
disabled={pageNum-1===-1}
/>
<div style={styles.pageInfo}>
Page: {pageNum + 1}/{totalPages}
</div>
<BigButton
title={">"}
onClick={() => setPageNum(pageNum + 1)}
disabled={pageNum+1>totalPages-1}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,29 @@
import React, {useCallback, useRef, useState} from 'react';
export default function useHover() {
const [value, setValue] = useState(false);
const handleMouseOver = useCallback(() => setValue(true), []);
const handleMouseOut = useCallback(() => setValue(false), []);
const ref = useRef();
const callbackRef = useCallback(
(node) => {
if (ref.current) {
ref.current.removeEventListener('mouseenter', handleMouseOver);
ref.current.removeEventListener('mouseleave', handleMouseOut);
}
ref.current = node;
if (ref.current) {
ref.current.addEventListener('mouseenter', handleMouseOver);
ref.current.addEventListener('mouseleave', handleMouseOut);
}
},
[handleMouseOver, handleMouseOut],
);
return [callbackRef, value];
}

View File

@@ -0,0 +1,6 @@
import {useWindowSize} from './useWindowSize';
export function useIsSmallScreen() {
const windowSize = useWindowSize();
return windowSize.width < 600;
}

View File

@@ -0,0 +1,29 @@
import React, {useState, useEffect} from 'react';
export function useWindowSize() {
const isClient = typeof window === 'object';
function getSize() {
return {
width: isClient ? window.innerWidth : undefined,
height: isClient ? window.innerHeight : undefined,
};
}
const [windowSize, setWindowSize] = useState(getSize);
useEffect(() => {
if (!isClient) {
return false;
}
function handleResize() {
setWindowSize(getSize());
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []); // Empty array ensures that effect is only run on mount and unmount
return windowSize;
}

View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@@ -0,0 +1,27 @@
export function blobToURL(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function () {
const base64data = reader.result;
resolve(base64data);
};
});
}
export async function fileToBlob(file, handleUpdate) {
const { content, size } = file;
let chunks = [];
let i = 0;
const totalCount = Math.round(size / 250000);
for await (const chunk of content) {
if (handleUpdate) {
handleUpdate(i, totalCount);
}
chunks.push(chunk);
i++;
}
// eslint-disable-next-line no-undef
return new Blob(chunks);
}

View File

@@ -0,0 +1,28 @@
export const primary = '#2b6284';
export const primary2 = '#ecf4f9';
export const primary3 = '#9fc7e0';
export const primary35 = '#97bace';
export const primary4 = 'hsl(204,38%,55%)';
export const primary45 = 'hsl(218,49%,66%)';
export const primary46 = '#6778cb';
export const primary15 = 'rgb(241 249 255)';
export const primary5 = '#3881ad';
export const primary6 = '#132b3a';
// export const primary = '#666';
// export const primary2 = '#EEE';
// export const primary3 = '#CCC';
// export const primary4 = '#AAA';
// export const primary5 = '#888';
// export const primary6 = '#333';
export const primary16 = 'hsl(208 100% 96% / 1)';
export const errorColor = '#ef6565';
export const lightErrorColor = '#ef9c9c';
export const goodColor = '#53c171';
export const cleanBorder = '1px solid rgb(208, 227, 239)';
export const lightBorder = 'hsl(203 51% 80% / 1)';

File diff suppressed because it is too large Load Diff

View File

@@ -37,6 +37,10 @@ Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "EnvelopeGenerator.Form", "E
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.PdfEditor", "EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj", "{211619F5-AE25-4BA5-A552-BACAFE0632D3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ReceiverUI", "EnvelopeGenerator.ReceiverUI\EnvelopeGenerator.ReceiverUI.csproj", "{34AF8679-E8AF-4AB6-B861-53ED241B1228}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ReceiverUIBlazor", "EnvelopeGenerator.ReceiverUIBlazor\EnvelopeGenerator.ReceiverUIBlazor.csproj", "{7F262AD4-53B1-42D3-9A5F-132CF50F150C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -91,6 +95,14 @@ Global
{211619F5-AE25-4BA5-A552-BACAFE0632D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{211619F5-AE25-4BA5-A552-BACAFE0632D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{211619F5-AE25-4BA5-A552-BACAFE0632D3}.Release|Any CPU.Build.0 = Release|Any CPU
{34AF8679-E8AF-4AB6-B861-53ED241B1228}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{34AF8679-E8AF-4AB6-B861-53ED241B1228}.Debug|Any CPU.Build.0 = Debug|Any CPU
{34AF8679-E8AF-4AB6-B861-53ED241B1228}.Release|Any CPU.ActiveCfg = Release|Any CPU
{34AF8679-E8AF-4AB6-B861-53ED241B1228}.Release|Any CPU.Build.0 = Release|Any CPU
{7F262AD4-53B1-42D3-9A5F-132CF50F150C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7F262AD4-53B1-42D3-9A5F-132CF50F150C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F262AD4-53B1-42D3-9A5F-132CF50F150C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7F262AD4-53B1-42D3-9A5F-132CF50F150C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -111,6 +123,8 @@ Global
{A9F9B431-BB9B-49B8-9E2C-0703634A653A} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{211619F5-AE25-4BA5-A552-BACAFE0632D3} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{34AF8679-E8AF-4AB6-B861-53ED241B1228} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{7F262AD4-53B1-42D3-9A5F-132CF50F150C} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7}