first commit
This commit is contained in:
commit
b1d75213ab
63
.gitattributes
vendored
Normal file
63
.gitattributes
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
###############################################################################
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
###############################################################################
|
||||
* text=auto
|
||||
|
||||
###############################################################################
|
||||
# Set default behavior for command prompt diff.
|
||||
#
|
||||
# This is need for earlier builds of msysgit that does not have it on by
|
||||
# default for csharp files.
|
||||
# Note: This is only used by command line
|
||||
###############################################################################
|
||||
#*.cs diff=csharp
|
||||
|
||||
###############################################################################
|
||||
# Set the merge driver for project and solution files
|
||||
#
|
||||
# Merging from the command prompt will add diff markers to the files if there
|
||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||
# the diff markers are never inserted). Diff markers may cause the following
|
||||
# file extensions to fail to load in VS. An alternative would be to treat
|
||||
# these files as binary and thus will always conflict and require user
|
||||
# intervention with every merge. To do so, just uncomment the entries below
|
||||
###############################################################################
|
||||
#*.sln merge=binary
|
||||
#*.csproj merge=binary
|
||||
#*.vbproj merge=binary
|
||||
#*.vcxproj merge=binary
|
||||
#*.vcproj merge=binary
|
||||
#*.dbproj merge=binary
|
||||
#*.fsproj merge=binary
|
||||
#*.lsproj merge=binary
|
||||
#*.wixproj merge=binary
|
||||
#*.modelproj merge=binary
|
||||
#*.sqlproj merge=binary
|
||||
#*.wwaproj merge=binary
|
||||
|
||||
###############################################################################
|
||||
# behavior for image files
|
||||
#
|
||||
# image files are treated as binary by default.
|
||||
###############################################################################
|
||||
#*.jpg binary
|
||||
#*.png binary
|
||||
#*.gif binary
|
||||
|
||||
###############################################################################
|
||||
# diff behavior for common document formats
|
||||
#
|
||||
# Convert binary document formats to text before diffing them. This feature
|
||||
# is only available from the command line. Turn it on by uncommenting the
|
||||
# entries below.
|
||||
###############################################################################
|
||||
#*.doc diff=astextplain
|
||||
#*.DOC diff=astextplain
|
||||
#*.docx diff=astextplain
|
||||
#*.DOCX diff=astextplain
|
||||
#*.dot diff=astextplain
|
||||
#*.DOT diff=astextplain
|
||||
#*.pdf diff=astextplain
|
||||
#*.PDF diff=astextplain
|
||||
#*.rtf diff=astextplain
|
||||
#*.RTF diff=astextplain
|
||||
363
.gitignore
vendored
Normal file
363
.gitignore
vendored
Normal file
@ -0,0 +1,363 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Oo]ut/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
25
OnlyOneAccessTemplate.sln
Normal file
25
OnlyOneAccessTemplate.sln
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.10.35122.118
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OnlyOneAccessTemplate", "OnlyOneAccessTemplate\OnlyOneAccessTemplate.csproj", "{2A672B8D-D16E-452E-A975-A3E19625453B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{2A672B8D-D16E-452E-A975-A3E19625453B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2A672B8D-D16E-452E-A975-A3E19625453B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2A672B8D-D16E-452E-A975-A3E19625453B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2A672B8D-D16E-452E-A975-A3E19625453B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {ACF1B5E0-2CA8-4090-9FBF-17649A1ABE11}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
526
OnlyOneAccessTemplate/Controllers/BaseController.cs
Normal file
526
OnlyOneAccessTemplate/Controllers/BaseController.cs
Normal file
@ -0,0 +1,526 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using OnlyOneAccessTemplate.Services;
|
||||
using OnlyOneAccessTemplate.Models;
|
||||
|
||||
namespace OnlyOneAccessTemplate.Controllers
|
||||
{
|
||||
|
||||
public abstract class BaseController : Controller
|
||||
{
|
||||
protected readonly ISiteConfigurationService _siteConfig;
|
||||
protected readonly ILanguageService _languageService;
|
||||
protected readonly ISeoService _seoService;
|
||||
protected readonly IMemoryCache _cache;
|
||||
protected readonly IConfiguration _configuration;
|
||||
|
||||
public BaseController(
|
||||
ISiteConfigurationService siteConfig,
|
||||
ILanguageService languageService,
|
||||
ISeoService seoService,
|
||||
IMemoryCache cache,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
_siteConfig = siteConfig;
|
||||
_languageService = languageService;
|
||||
_seoService = seoService;
|
||||
_cache = cache;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var language = GetCurrentLanguage();
|
||||
var config = _siteConfig.GetConfiguration(language);
|
||||
var currentUrl = GetCurrentUrl();
|
||||
|
||||
SetupSeoViewBag(config, language, currentUrl);
|
||||
SetupContentViewBag(config, language);
|
||||
base.OnActionExecuting(context);
|
||||
}
|
||||
|
||||
protected virtual string GetCurrentLanguage()
|
||||
{
|
||||
var path = Request.Path.Value ?? "";
|
||||
|
||||
if (path.StartsWith("/en"))
|
||||
return "en";
|
||||
if (path.StartsWith("/es"))
|
||||
return "es";
|
||||
|
||||
return "pt"; // default
|
||||
}
|
||||
|
||||
protected string GetCurrentUrl()
|
||||
{
|
||||
return $"{Request.Scheme}://{Request.Host}{Request.Path}{Request.QueryString}";
|
||||
}
|
||||
|
||||
protected void SetupSeoViewBag(SiteConfiguration config, string language, string currentUrl)
|
||||
{
|
||||
// Basic SEO
|
||||
ViewBag.Language = language;
|
||||
ViewBag.Direction = _languageService.GetLanguageDirection(language);
|
||||
ViewBag.SiteName = config.Seo.SiteName;
|
||||
ViewBag.SiteDescription = config.Seo.DefaultDescription;
|
||||
ViewBag.CanonicalUrl = currentUrl;
|
||||
ViewBag.Author = config.Seo.Author;
|
||||
ViewBag.GTMId = config.Seo.GoogleTagManagerId;
|
||||
|
||||
// Default SEO values
|
||||
ViewBag.Title = ViewBag.Title ?? config.Seo.DefaultTitle;
|
||||
ViewBag.Description = ViewBag.Description ?? config.Seo.DefaultDescription;
|
||||
ViewBag.Keywords = ViewBag.Keywords ?? config.Seo.DefaultKeywords;
|
||||
|
||||
// Open Graph
|
||||
ViewBag.OgTitle = ViewBag.Title;
|
||||
ViewBag.OgDescription = ViewBag.Description;
|
||||
ViewBag.OgImage = config.Seo.OgImage;
|
||||
ViewBag.OgLocale = GetOgLocale(language);
|
||||
ViewBag.TwitterHandle = config.Seo.TwitterHandle;
|
||||
|
||||
// Language alternatives
|
||||
SetupHreflangUrls(currentUrl, language);
|
||||
|
||||
// Current language display
|
||||
ViewBag.CurrentLanguageDisplay = _languageService.GetLanguageDisplayName(language);
|
||||
}
|
||||
|
||||
protected void SetupContentViewBag(SiteConfiguration config, string language)
|
||||
{
|
||||
// Navigation
|
||||
ViewBag.HomeUrl = language == "pt" ? "/" : $"/{language}";
|
||||
ViewBag.AboutUrl = language == "pt" ? "/about" : $"/{language}/about";
|
||||
ViewBag.ContactUrl = language == "pt" ? "/contact" : $"/{language}/contact";
|
||||
ViewBag.PrivacyUrl = language == "pt" ? "/privacy" : $"/{language}/privacy";
|
||||
ViewBag.TermsUrl = language == "pt" ? "/terms" : $"/{language}/terms";
|
||||
|
||||
// Menu texts based on language
|
||||
switch (language)
|
||||
{
|
||||
case "en":
|
||||
SetupEnglishContent();
|
||||
break;
|
||||
case "es":
|
||||
SetupSpanishContent();
|
||||
break;
|
||||
default:
|
||||
SetupPortugueseContent();
|
||||
break;
|
||||
}
|
||||
|
||||
// Logo and branding
|
||||
ViewBag.LogoUrl = "/img/logo.png";
|
||||
ViewBag.BodyClass = $"lang-{language}";
|
||||
|
||||
// Conversion config
|
||||
ViewBag.ConversionConfig = config.Conversion;
|
||||
}
|
||||
|
||||
protected void SetupPortugueseContent()
|
||||
{
|
||||
// Menu
|
||||
ViewBag.MenuHome = "Início";
|
||||
ViewBag.MenuAbout = "Sobre";
|
||||
ViewBag.MenuContact = "Contato";
|
||||
ViewBag.MenuPrivacy = "Privacidade";
|
||||
ViewBag.MenuTerms = "Termos";
|
||||
|
||||
// Hero Section
|
||||
ViewBag.DefaultHeroTitle = "Transforme Seu Negócio com Nossa Solução";
|
||||
ViewBag.DefaultHeroSubtitle = "Aumente suas vendas em até 300% com nossa metodologia comprovada e suporte especializado.";
|
||||
ViewBag.DefaultCtaText = "Comece Agora";
|
||||
ViewBag.WatchVideoText = "Assistir Vídeo";
|
||||
|
||||
// Features
|
||||
ViewBag.DefaultFeaturesTitle = "Por Que Escolher Nossa Solução?";
|
||||
ViewBag.DefaultFeaturesSubtitle = "Descubra os benefícios que já transformaram mais de 1000 empresas";
|
||||
ViewBag.Feature1Title = "Resultados Rápidos";
|
||||
ViewBag.Feature1Description = "Veja os primeiros resultados em até 30 dias";
|
||||
ViewBag.Feature2Title = "Suporte 24/7";
|
||||
ViewBag.Feature2Description = "Equipe especializada sempre disponível";
|
||||
ViewBag.Feature3Title = "Metodologia Comprovada";
|
||||
ViewBag.Feature3Description = "Mais de 1000 casos de sucesso documentados";
|
||||
|
||||
// Form
|
||||
ViewBag.FormTitle = "Solicite uma Demonstração Gratuita";
|
||||
ViewBag.FormSubtitle = "Preencha o formulário e nossa equipe entrará em contato";
|
||||
ViewBag.NameLabel = "Nome Completo";
|
||||
ViewBag.NamePlaceholder = "Digite seu nome completo";
|
||||
ViewBag.EmailLabel = "E-mail";
|
||||
ViewBag.EmailPlaceholder = "Digite seu e-mail";
|
||||
ViewBag.PhoneLabel = "Telefone";
|
||||
ViewBag.PhonePlaceholder = "Digite seu telefone";
|
||||
ViewBag.SubmitButtonText = "Solicitar Demonstração";
|
||||
ViewBag.LoadingText = "Enviando...";
|
||||
|
||||
// Quick Form
|
||||
ViewBag.QuickFormTitle = "Acesso Rápido";
|
||||
ViewBag.QuickSubmitText = "Começar Agora";
|
||||
ViewBag.QuickConsentText = "Concordo em receber informações por e-mail";
|
||||
|
||||
// Validation
|
||||
ViewBag.RequiredFieldMessage = "Este campo é obrigatório";
|
||||
ViewBag.EmailValidationMessage = "Digite um e-mail válido";
|
||||
ViewBag.PhoneValidationMessage = "Digite um telefone válido";
|
||||
ViewBag.ConsentValidationMessage = "Você deve concordar para continuar";
|
||||
|
||||
// Consent
|
||||
ViewBag.ConsentText = "Concordo em receber comunicações e com a <a href='/privacy'>Política de Privacidade</a>";
|
||||
|
||||
// Benefits
|
||||
ViewBag.BenefitsTitle = "O que você vai receber:";
|
||||
ViewBag.DefaultBenefit1 = "Consultoria personalizada";
|
||||
ViewBag.DefaultBenefit2 = "Suporte técnico especializado";
|
||||
ViewBag.DefaultBenefit3 = "Garantia de resultados";
|
||||
|
||||
// Testimonials
|
||||
ViewBag.DefaultTestimonialsTitle = "O Que Nossos Clientes Dizem";
|
||||
ViewBag.DefaultTestimonialsSubtitle = "Mais de 1000 empresas já transformaram seus resultados";
|
||||
SetupPortugueseTestimonials();
|
||||
|
||||
// CTA
|
||||
ViewBag.DefaultCtaTitle = "Pronto para Transformar Seu Negócio?";
|
||||
ViewBag.DefaultCtaSubtitle = "Junte-se a mais de 1000 empresas que já alcançaram resultados extraordinários";
|
||||
ViewBag.DefaultCtaButtonText = "Começar Transformação";
|
||||
|
||||
// Footer
|
||||
ViewBag.FooterDescription = "Transformando negócios através de soluções inovadoras";
|
||||
ViewBag.FooterMenuTitle = "Links";
|
||||
ViewBag.FooterLegalTitle = "Legal";
|
||||
ViewBag.FooterContactTitle = "Contato";
|
||||
ViewBag.FooterCopyright = "Todos os direitos reservados.";
|
||||
ViewBag.FooterMadeWith = "Feito com ❤️ no Brasil";
|
||||
|
||||
// Success
|
||||
ViewBag.SuccessTitle = "Obrigado!";
|
||||
ViewBag.SuccessMessage = "Recebemos sua solicitação. Nossa equipe entrará em contato em breve.";
|
||||
ViewBag.CloseButtonText = "Fechar";
|
||||
|
||||
// Thank You Page
|
||||
ViewBag.NextStepsTitle = "O que acontece agora?";
|
||||
ViewBag.Step1Title = "Análise";
|
||||
ViewBag.Step1Description = "Nossa equipe analisará sua solicitação";
|
||||
ViewBag.Step2Title = "Contato";
|
||||
ViewBag.Step2Description = "Entraremos em contato em até 24h";
|
||||
ViewBag.Step3Title = "Implementação";
|
||||
ViewBag.Step3Description = "Iniciaremos sua transformação";
|
||||
ViewBag.ContactInfoTitle = "Precisa falar conosco?";
|
||||
ViewBag.BackToHomeText = "Voltar ao Início";
|
||||
ViewBag.WhatsAppText = "Falar no WhatsApp";
|
||||
ViewBag.RedirectConfirmText = "Deseja voltar à página inicial?";
|
||||
|
||||
// Security
|
||||
ViewBag.SecurityText = "Seus dados estão seguros conosco";
|
||||
|
||||
// Time
|
||||
ViewBag.DaysText = "Dias";
|
||||
ViewBag.HoursText = "Horas";
|
||||
ViewBag.MinutesText = "Min";
|
||||
ViewBag.SecondsText = "Seg";
|
||||
|
||||
// Contact info
|
||||
ViewBag.ContactEmail = "contato@seusite.com";
|
||||
ViewBag.ContactPhone = "(11) 99999-9999";
|
||||
ViewBag.ContactAddress = "São Paulo, SP";
|
||||
ViewBag.TestimonialsCount = "Mais de 500 avaliações 5 estrelas";
|
||||
}
|
||||
|
||||
protected void SetupEnglishContent()
|
||||
{
|
||||
// Menu
|
||||
ViewBag.MenuHome = "Home";
|
||||
ViewBag.MenuAbout = "About";
|
||||
ViewBag.MenuContact = "Contact";
|
||||
ViewBag.MenuPrivacy = "Privacy";
|
||||
ViewBag.MenuTerms = "Terms";
|
||||
|
||||
// Hero Section
|
||||
ViewBag.DefaultHeroTitle = "Transform Your Business with Our Solution";
|
||||
ViewBag.DefaultHeroSubtitle = "Increase your sales by up to 300% with our proven methodology and expert support.";
|
||||
ViewBag.DefaultCtaText = "Get Started";
|
||||
ViewBag.WatchVideoText = "Watch Video";
|
||||
|
||||
// Features
|
||||
ViewBag.DefaultFeaturesTitle = "Why Choose Our Solution?";
|
||||
ViewBag.DefaultFeaturesSubtitle = "Discover the benefits that have already transformed over 1000 companies";
|
||||
ViewBag.Feature1Title = "Fast Results";
|
||||
ViewBag.Feature1Description = "See first results within 30 days";
|
||||
ViewBag.Feature2Title = "24/7 Support";
|
||||
ViewBag.Feature2Description = "Specialized team always available";
|
||||
ViewBag.Feature3Title = "Proven Method";
|
||||
ViewBag.Feature3Description = "Over 1000 documented success cases";
|
||||
|
||||
// Form
|
||||
ViewBag.FormTitle = "Request a Free Demo";
|
||||
ViewBag.FormSubtitle = "Fill out the form and our team will contact you";
|
||||
ViewBag.NameLabel = "Full Name";
|
||||
ViewBag.NamePlaceholder = "Enter your full name";
|
||||
ViewBag.EmailLabel = "Email";
|
||||
ViewBag.EmailPlaceholder = "Enter your email";
|
||||
ViewBag.PhoneLabel = "Phone";
|
||||
ViewBag.PhonePlaceholder = "Enter your phone";
|
||||
ViewBag.SubmitButtonText = "Request Demo";
|
||||
ViewBag.LoadingText = "Sending...";
|
||||
|
||||
// Quick Form
|
||||
ViewBag.QuickFormTitle = "Quick Access";
|
||||
ViewBag.QuickSubmitText = "Start Now";
|
||||
ViewBag.QuickConsentText = "I agree to receive information by email";
|
||||
|
||||
// Validation
|
||||
ViewBag.RequiredFieldMessage = "This field is required";
|
||||
ViewBag.EmailValidationMessage = "Enter a valid email";
|
||||
ViewBag.PhoneValidationMessage = "Enter a valid phone";
|
||||
ViewBag.ConsentValidationMessage = "You must agree to continue";
|
||||
|
||||
// Consent
|
||||
ViewBag.ConsentText = "I agree to receive communications and with the <a href='/en/privacy'>Privacy Policy</a>";
|
||||
|
||||
// Benefits
|
||||
ViewBag.BenefitsTitle = "What you'll receive:";
|
||||
ViewBag.DefaultBenefit1 = "Personalized consultation";
|
||||
ViewBag.DefaultBenefit2 = "Specialized technical support";
|
||||
ViewBag.DefaultBenefit3 = "Results guarantee";
|
||||
|
||||
// Testimonials
|
||||
ViewBag.DefaultTestimonialsTitle = "What Our Clients Say";
|
||||
ViewBag.DefaultTestimonialsSubtitle = "Over 1000 companies have already transformed their results";
|
||||
SetupEnglishTestimonials();
|
||||
|
||||
// CTA
|
||||
ViewBag.DefaultCtaTitle = "Ready to Transform Your Business?";
|
||||
ViewBag.DefaultCtaSubtitle = "Join over 1000 companies that have already achieved extraordinary results";
|
||||
ViewBag.DefaultCtaButtonText = "Start Transformation";
|
||||
|
||||
// Footer
|
||||
ViewBag.FooterDescription = "Transforming businesses through innovative solutions";
|
||||
ViewBag.FooterMenuTitle = "Links";
|
||||
ViewBag.FooterLegalTitle = "Legal";
|
||||
ViewBag.FooterContactTitle = "Contact";
|
||||
ViewBag.FooterCopyright = "All rights reserved.";
|
||||
ViewBag.FooterMadeWith = "Made with ❤️ in Brazil";
|
||||
|
||||
// Success
|
||||
ViewBag.SuccessTitle = "Thank You!";
|
||||
ViewBag.SuccessMessage = "We received your request. Our team will contact you soon.";
|
||||
ViewBag.CloseButtonText = "Close";
|
||||
|
||||
// Thank You Page
|
||||
ViewBag.NextStepsTitle = "What happens next?";
|
||||
ViewBag.Step1Title = "Analysis";
|
||||
ViewBag.Step1Description = "Our team will analyze your request";
|
||||
ViewBag.Step2Title = "Contact";
|
||||
ViewBag.Step2Description = "We'll contact you within 24h";
|
||||
ViewBag.Step3Title = "Implementation";
|
||||
ViewBag.Step3Description = "We'll start your transformation";
|
||||
ViewBag.ContactInfoTitle = "Need to talk to us?";
|
||||
ViewBag.BackToHomeText = "Back to Home";
|
||||
ViewBag.WhatsAppText = "Chat on WhatsApp";
|
||||
ViewBag.RedirectConfirmText = "Would you like to return to the home page?";
|
||||
|
||||
// Security
|
||||
ViewBag.SecurityText = "Your data is safe with us";
|
||||
|
||||
// Time
|
||||
ViewBag.DaysText = "Days";
|
||||
ViewBag.HoursText = "Hours";
|
||||
ViewBag.MinutesText = "Min";
|
||||
ViewBag.SecondsText = "Sec";
|
||||
|
||||
// Contact info
|
||||
ViewBag.ContactEmail = "contact@yoursite.com";
|
||||
ViewBag.ContactPhone = "+1 (555) 123-4567";
|
||||
ViewBag.ContactAddress = "New York, NY";
|
||||
ViewBag.TestimonialsCount = "Over 500 five-star reviews";
|
||||
}
|
||||
|
||||
protected void SetupSpanishContent()
|
||||
{
|
||||
// Menu
|
||||
ViewBag.MenuHome = "Inicio";
|
||||
ViewBag.MenuAbout = "Acerca";
|
||||
ViewBag.MenuContact = "Contacto";
|
||||
ViewBag.MenuPrivacy = "Privacidad";
|
||||
ViewBag.MenuTerms = "Términos";
|
||||
|
||||
// Hero Section
|
||||
ViewBag.DefaultHeroTitle = "Transforma Tu Negocio con Nuestra Solución";
|
||||
ViewBag.DefaultHeroSubtitle = "Aumenta tus ventas hasta un 300% con nuestra metodología probada y soporte especializado.";
|
||||
ViewBag.DefaultCtaText = "Comenzar Ahora";
|
||||
ViewBag.WatchVideoText = "Ver Video";
|
||||
|
||||
// Features
|
||||
ViewBag.DefaultFeaturesTitle = "¿Por Qué Elegir Nuestra Solución?";
|
||||
ViewBag.DefaultFeaturesSubtitle = "Descubre los beneficios que ya han transformado más de 1000 empresas";
|
||||
ViewBag.Feature1Title = "Resultados Rápidos";
|
||||
ViewBag.Feature1Description = "Ve los primeros resultados en 30 días";
|
||||
ViewBag.Feature2Title = "Soporte 24/7";
|
||||
ViewBag.Feature2Description = "Equipo especializado siempre disponible";
|
||||
ViewBag.Feature3Title = "Metodología Probada";
|
||||
ViewBag.Feature3Description = "Más de 1000 casos de éxito documentados";
|
||||
|
||||
// Form
|
||||
ViewBag.FormTitle = "Solicita una Demo Gratuita";
|
||||
ViewBag.FormSubtitle = "Completa el formulario y nuestro equipo te contactará";
|
||||
ViewBag.NameLabel = "Nombre Completo";
|
||||
ViewBag.NamePlaceholder = "Ingresa tu nombre completo";
|
||||
ViewBag.EmailLabel = "Correo";
|
||||
ViewBag.EmailPlaceholder = "Ingresa tu correo";
|
||||
ViewBag.PhoneLabel = "Teléfono";
|
||||
ViewBag.PhonePlaceholder = "Ingresa tu teléfono";
|
||||
ViewBag.SubmitButtonText = "Solicitar Demo";
|
||||
ViewBag.LoadingText = "Enviando...";
|
||||
|
||||
// Quick Form
|
||||
ViewBag.QuickFormTitle = "Acceso Rápido";
|
||||
ViewBag.QuickSubmitText = "Comenzar Ahora";
|
||||
ViewBag.QuickConsentText = "Acepto recibir información por correo";
|
||||
|
||||
// Validation
|
||||
ViewBag.RequiredFieldMessage = "Este campo es obligatorio";
|
||||
ViewBag.EmailValidationMessage = "Ingresa un correo válido";
|
||||
ViewBag.PhoneValidationMessage = "Ingresa un teléfono válido";
|
||||
ViewBag.ConsentValidationMessage = "Debes aceptar para continuar";
|
||||
|
||||
// Consent
|
||||
ViewBag.ConsentText = "Acepto recibir comunicaciones y la <a href='/es/privacy'>Política de Privacidad</a>";
|
||||
|
||||
// Benefits
|
||||
ViewBag.BenefitsTitle = "Lo que recibirás:";
|
||||
ViewBag.DefaultBenefit1 = "Consultoría personalizada";
|
||||
ViewBag.DefaultBenefit2 = "Soporte técnico especializado";
|
||||
ViewBag.DefaultBenefit3 = "Garantía de resultados";
|
||||
|
||||
// Testimonials
|
||||
ViewBag.DefaultTestimonialsTitle = "Lo Que Dicen Nuestros Clientes";
|
||||
ViewBag.DefaultTestimonialsSubtitle = "Más de 1000 empresas ya han transformado sus resultados";
|
||||
SetupSpanishTestimonials();
|
||||
|
||||
// CTA
|
||||
ViewBag.DefaultCtaTitle = "¿Listo para Transformar Tu Negocio?";
|
||||
ViewBag.DefaultCtaSubtitle = "Únete a más de 1000 empresas que ya han logrado resultados extraordinarios";
|
||||
ViewBag.DefaultCtaButtonText = "Comenzar Transformación";
|
||||
|
||||
// Footer
|
||||
ViewBag.FooterDescription = "Transformando negocios a través de soluciones innovadoras";
|
||||
ViewBag.FooterMenuTitle = "Enlaces";
|
||||
ViewBag.FooterLegalTitle = "Legal";
|
||||
ViewBag.FooterContactTitle = "Contacto";
|
||||
ViewBag.FooterCopyright = "Todos los derechos reservados.";
|
||||
ViewBag.FooterMadeWith = "Hecho con ❤️ en Brasil";
|
||||
|
||||
// Success
|
||||
ViewBag.SuccessTitle = "¡Gracias!";
|
||||
ViewBag.SuccessMessage = "Recibimos tu solicitud. Nuestro equipo te contactará pronto.";
|
||||
ViewBag.CloseButtonText = "Cerrar";
|
||||
|
||||
// Thank You Page
|
||||
ViewBag.NextStepsTitle = "¿Qué pasa ahora?";
|
||||
ViewBag.Step1Title = "Análisis";
|
||||
ViewBag.Step1Description = "Nuestro equipo analizará tu solicitud";
|
||||
ViewBag.Step2Title = "Contacto";
|
||||
ViewBag.Step2Description = "Te contactaremos en 24h";
|
||||
ViewBag.Step3Title = "Implementación";
|
||||
ViewBag.Step3Description = "Comenzaremos tu transformación";
|
||||
ViewBag.ContactInfoTitle = "¿Necesitas hablar con nosotros?";
|
||||
ViewBag.BackToHomeText = "Volver al Inicio";
|
||||
ViewBag.WhatsAppText = "Hablar por WhatsApp";
|
||||
ViewBag.RedirectConfirmText = "¿Te gustaría volver a la página principal?";
|
||||
|
||||
// Security
|
||||
ViewBag.SecurityText = "Tus datos están seguros con nosotros";
|
||||
|
||||
// Time
|
||||
ViewBag.DaysText = "Días";
|
||||
ViewBag.HoursText = "Horas";
|
||||
ViewBag.MinutesText = "Min";
|
||||
ViewBag.SecondsText = "Seg";
|
||||
|
||||
// Contact info
|
||||
ViewBag.ContactEmail = "contacto@tusitioweb.com";
|
||||
ViewBag.ContactPhone = "+34 123 456 789";
|
||||
ViewBag.ContactAddress = "Madrid, España";
|
||||
ViewBag.TestimonialsCount = "Más de 500 reseñas de 5 estrellas";
|
||||
}
|
||||
|
||||
protected void SetupPortugueseTestimonials()
|
||||
{
|
||||
ViewBag.Testimonial1Quote = "Aumentamos nossas vendas em 250% em apenas 3 meses. Incrível!";
|
||||
ViewBag.Testimonial1Name = "Maria Silva";
|
||||
ViewBag.Testimonial1Position = "CEO, TechStart";
|
||||
|
||||
ViewBag.Testimonial2Quote = "O suporte é excepcional. Sempre prontos a ajudar quando precisamos.";
|
||||
ViewBag.Testimonial2Name = "João Santos";
|
||||
ViewBag.Testimonial2Position = "Diretor, InovaCorp";
|
||||
|
||||
ViewBag.Testimonial3Quote = "A metodologia realmente funciona. Resultados visíveis desde o primeiro mês.";
|
||||
ViewBag.Testimonial3Name = "Ana Costa";
|
||||
ViewBag.Testimonial3Position = "Gerente, GrowBiz";
|
||||
}
|
||||
|
||||
protected void SetupEnglishTestimonials()
|
||||
{
|
||||
ViewBag.Testimonial1Quote = "We increased our sales by 250% in just 3 months. Amazing!";
|
||||
ViewBag.Testimonial1Name = "Mary Johnson";
|
||||
ViewBag.Testimonial1Position = "CEO, TechStart";
|
||||
|
||||
ViewBag.Testimonial2Quote = "The support is exceptional. Always ready to help when we need it.";
|
||||
ViewBag.Testimonial2Name = "John Smith";
|
||||
ViewBag.Testimonial2Position = "Director, InovaCorp";
|
||||
|
||||
ViewBag.Testimonial3Quote = "The methodology really works. Visible results from the first month.";
|
||||
ViewBag.Testimonial3Name = "Sarah Davis";
|
||||
ViewBag.Testimonial3Position = "Manager, GrowBiz";
|
||||
}
|
||||
|
||||
protected void SetupSpanishTestimonials()
|
||||
{
|
||||
ViewBag.Testimonial1Quote = "Aumentamos nuestras ventas un 250% en solo 3 meses. ¡Increíble!";
|
||||
ViewBag.Testimonial1Name = "María García";
|
||||
ViewBag.Testimonial1Position = "CEO, TechStart";
|
||||
|
||||
ViewBag.Testimonial2Quote = "El soporte es excepcional. Siempre listos para ayudar cuando lo necesitamos.";
|
||||
ViewBag.Testimonial2Name = "Carlos López";
|
||||
ViewBag.Testimonial2Position = "Director, InovaCorp";
|
||||
|
||||
ViewBag.Testimonial3Quote = "La metodología realmente funciona. Resultados visibles desde el primer mes.";
|
||||
ViewBag.Testimonial3Name = "Ana Rodríguez";
|
||||
ViewBag.Testimonial3Position = "Gerente, GrowBiz";
|
||||
}
|
||||
|
||||
protected void SetupHreflangUrls(string currentUrl, string currentLanguage)
|
||||
{
|
||||
var hreflangUrls = _languageService.GetHreflangUrls(currentUrl, currentLanguage);
|
||||
|
||||
ViewBag.PtUrl = hreflangUrls.GetValueOrDefault("pt", "/");
|
||||
ViewBag.EnUrl = hreflangUrls.GetValueOrDefault("en", "/en");
|
||||
ViewBag.EsUrl = hreflangUrls.GetValueOrDefault("es", "/es");
|
||||
ViewBag.DefaultUrl = ViewBag.PtUrl;
|
||||
}
|
||||
|
||||
protected string GetOgLocale(string language)
|
||||
{
|
||||
return language switch
|
||||
{
|
||||
"en" => "en_US",
|
||||
"es" => "es_ES",
|
||||
"pt" => "pt_BR",
|
||||
_ => "pt_BR"
|
||||
};
|
||||
}
|
||||
|
||||
protected void SetPageSeo(string title, string description, string keywords = null, string ogImage = null)
|
||||
{
|
||||
ViewBag.Title = title;
|
||||
ViewBag.Description = description;
|
||||
ViewBag.Keywords = keywords;
|
||||
|
||||
if (!string.IsNullOrEmpty(ogImage))
|
||||
{
|
||||
ViewBag.OgImage = ogImage;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
272
OnlyOneAccessTemplate/Controllers/ConversionController.cs
Normal file
272
OnlyOneAccessTemplate/Controllers/ConversionController.cs
Normal file
@ -0,0 +1,272 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using OnlyOneAccessTemplate.Services;
|
||||
using OnlyOneAccessTemplate.Models;
|
||||
using System.Text.Json;
|
||||
using global::OnlyOneAccessTemplate.Services;
|
||||
|
||||
namespace OnlyOneAccessTemplate.Controllers
|
||||
{
|
||||
|
||||
[Route("conversion")]
|
||||
public class ConversionController : BaseController
|
||||
{
|
||||
private readonly IConversionService _conversionService;
|
||||
|
||||
public ConversionController(
|
||||
ISiteConfigurationService siteConfig,
|
||||
ILanguageService languageService,
|
||||
ISeoService seoService,
|
||||
IMemoryCache cache,
|
||||
IConfiguration configuration,
|
||||
IConversionService conversionService)
|
||||
: base(siteConfig, languageService, seoService, cache, configuration)
|
||||
{
|
||||
_conversionService = conversionService;
|
||||
}
|
||||
|
||||
[HttpPost("submit")]
|
||||
public async Task<IActionResult> Submit([FromBody] Dictionary<string, string> formData)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Extract basic data
|
||||
var language = formData.GetValueOrDefault("language", "pt");
|
||||
var pageName = formData.GetValueOrDefault("pageName", "home");
|
||||
var formType = formData.GetValueOrDefault("formType", "main");
|
||||
|
||||
// Remove system fields from form data
|
||||
var cleanFormData = formData
|
||||
.Where(kvp => !new[] { "language", "pageName", "formType", "source", "medium", "campaign" }.Contains(kvp.Key))
|
||||
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
|
||||
// Get user info
|
||||
var userAgent = Request.Headers.UserAgent.ToString();
|
||||
var ipAddress = GetClientIpAddress();
|
||||
var referrer = Request.Headers.Referer.ToString();
|
||||
|
||||
// Create conversion data
|
||||
var conversionData = new ConversionData(
|
||||
Language: language,
|
||||
PageName: pageName,
|
||||
FormData: cleanFormData,
|
||||
UserAgent: userAgent,
|
||||
IpAddress: ipAddress,
|
||||
Referrer: referrer,
|
||||
SessionId: GetOrCreateSessionId(),
|
||||
UtmParameters: ExtractUtmParameters()
|
||||
);
|
||||
|
||||
// Process conversion
|
||||
var success = await _conversionService.ProcessConversionAsync(conversionData);
|
||||
|
||||
if (success)
|
||||
{
|
||||
var response = language switch
|
||||
{
|
||||
"en" => new
|
||||
{
|
||||
success = true,
|
||||
message = "Thank you! We received your request and will contact you soon.",
|
||||
redirectUrl = $"/en/thank-you"
|
||||
},
|
||||
"es" => new
|
||||
{
|
||||
success = true,
|
||||
message = "¡Gracias! Recibimos tu solicitud y te contactaremos pronto.",
|
||||
redirectUrl = $"/es/gracias"
|
||||
},
|
||||
_ => new
|
||||
{
|
||||
success = true,
|
||||
message = "Obrigado! Recebemos sua solicitação e entraremos em contato em breve.",
|
||||
redirectUrl = $"/obrigado"
|
||||
}
|
||||
};
|
||||
|
||||
return Json(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Json(new { success = false, message = GetErrorMessage(language) });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log error
|
||||
Console.WriteLine($"Conversion error: {ex.Message}");
|
||||
|
||||
var language = formData.GetValueOrDefault("language", "pt");
|
||||
return Json(new { success = false, message = GetErrorMessage(language) });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("thank-you")]
|
||||
[HttpGet("obrigado")]
|
||||
public IActionResult ThankYou()
|
||||
{
|
||||
ViewBag.CurrentPage = "thank-you";
|
||||
SetPageSeo(
|
||||
"Obrigado!",
|
||||
"Obrigado por entrar em contato. Nossa equipe retornará em breve.",
|
||||
"obrigado, contato, sucesso"
|
||||
);
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet("en/thank-you")]
|
||||
public IActionResult ThankYouEn()
|
||||
{
|
||||
ViewBag.CurrentPage = "thank-you";
|
||||
SetPageSeo(
|
||||
"Thank You!",
|
||||
"Thank you for contacting us. Our team will get back to you soon.",
|
||||
"thank you, contact, success"
|
||||
);
|
||||
return View("ThankYou");
|
||||
}
|
||||
|
||||
[HttpGet("es/gracias")]
|
||||
public IActionResult ThankYouEs()
|
||||
{
|
||||
ViewBag.CurrentPage = "thank-you";
|
||||
SetPageSeo(
|
||||
"¡Gracias!",
|
||||
"Gracias por contactarnos. Nuestro equipo se pondrá en contacto contigo pronto.",
|
||||
"gracias, contacto, éxito"
|
||||
);
|
||||
return View("ThankYou");
|
||||
}
|
||||
|
||||
private string GetClientIpAddress()
|
||||
{
|
||||
// Try to get real IP address
|
||||
var ipAddress = Request.Headers["X-Forwarded-For"].FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(ipAddress))
|
||||
{
|
||||
ipAddress = Request.Headers["X-Real-IP"].FirstOrDefault();
|
||||
}
|
||||
if (string.IsNullOrEmpty(ipAddress))
|
||||
{
|
||||
ipAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||
}
|
||||
|
||||
return ipAddress ?? "unknown";
|
||||
}
|
||||
|
||||
private string GetOrCreateSessionId()
|
||||
{
|
||||
var sessionId = HttpContext.Session.GetString("SessionId");
|
||||
if (string.IsNullOrEmpty(sessionId))
|
||||
{
|
||||
sessionId = Guid.NewGuid().ToString("N")[..16];
|
||||
HttpContext.Session.SetString("SessionId", sessionId);
|
||||
}
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ExtractUtmParameters()
|
||||
{
|
||||
var utmParams = new Dictionary<string, string>();
|
||||
|
||||
foreach (var param in new[] { "utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content" })
|
||||
{
|
||||
if (Request.Query.ContainsKey(param))
|
||||
{
|
||||
utmParams[param] = Request.Query[param].ToString();
|
||||
}
|
||||
}
|
||||
|
||||
return utmParams;
|
||||
}
|
||||
|
||||
private string GetErrorMessage(string language)
|
||||
{
|
||||
return language switch
|
||||
{
|
||||
"en" => "An error occurred while processing your request. Please try again.",
|
||||
"es" => "Ocurrió un error al procesar tu solicitud. Por favor, inténtalo de nuevo.",
|
||||
_ => "Ocorreu um erro ao processar sua solicitação. Tente novamente."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Analytics Controller
|
||||
[Route("api/analytics")]
|
||||
public class AnalyticsController : ControllerBase
|
||||
{
|
||||
private readonly IConversionService _conversionService;
|
||||
|
||||
public AnalyticsController(IConversionService conversionService)
|
||||
{
|
||||
_conversionService = conversionService;
|
||||
}
|
||||
|
||||
[HttpPost("track")]
|
||||
public async Task<IActionResult> Track([FromBody] JsonElement data)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Extract event data
|
||||
var eventName = data.GetProperty("event").GetString() ?? "unknown";
|
||||
var properties = new Dictionary<string, object>();
|
||||
|
||||
if (data.TryGetProperty("data", out var dataProperty))
|
||||
{
|
||||
foreach (var prop in dataProperty.EnumerateObject())
|
||||
{
|
||||
properties[prop.Name] = prop.Value.ToString() ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
// Log the event
|
||||
await _conversionService.LogConversionEventAsync(eventName, properties);
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Analytics tracking error: {ex.Message}");
|
||||
return Ok(new { success = false }); // Don't break user experience
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("heatmap")]
|
||||
public async Task<IActionResult> Heatmap([FromBody] JsonElement data)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Process heatmap data
|
||||
await _conversionService.LogConversionEventAsync("heatmap_data", new Dictionary<string, object>
|
||||
{
|
||||
{ "data", data.ToString() }
|
||||
});
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Ok(new { success = false });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("time")]
|
||||
public async Task<IActionResult> TimeTracking([FromBody] JsonElement data)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Process time tracking data
|
||||
await _conversionService.LogConversionEventAsync("time_tracking", new Dictionary<string, object>
|
||||
{
|
||||
{ "data", data.ToString() }
|
||||
});
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Ok(new { success = false });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
OnlyOneAccessTemplate/Controllers/EnController.cs
Normal file
43
OnlyOneAccessTemplate/Controllers/EnController.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using global::OnlyOneAccessTemplate.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using OnlyOneAccessTemplate.Services;
|
||||
|
||||
namespace OnlyOneAccessTemplate.Controllers
|
||||
{
|
||||
|
||||
[Route("en")]
|
||||
public class EnController : HomeController
|
||||
{
|
||||
public EnController(
|
||||
ISiteConfigurationService siteConfig,
|
||||
ILanguageService languageService,
|
||||
ISeoService seoService,
|
||||
IMemoryCache cache,
|
||||
IConfiguration configuration)
|
||||
: base(siteConfig, languageService, seoService, cache, configuration)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string GetCurrentLanguage() => "en";
|
||||
|
||||
[Route("")]
|
||||
[Route("home")]
|
||||
public new async Task<IActionResult> Index()
|
||||
{
|
||||
return await base.Index();
|
||||
}
|
||||
|
||||
[Route("about")]
|
||||
public new async Task<IActionResult> About()
|
||||
{
|
||||
return await base.About();
|
||||
}
|
||||
|
||||
[Route("contact")]
|
||||
public new async Task<IActionResult> Contact()
|
||||
{
|
||||
return await base.Contact();
|
||||
}
|
||||
}
|
||||
}
|
||||
43
OnlyOneAccessTemplate/Controllers/EsController.cs
Normal file
43
OnlyOneAccessTemplate/Controllers/EsController.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using global::OnlyOneAccessTemplate.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using OnlyOneAccessTemplate.Services;
|
||||
|
||||
namespace OnlyOneAccessTemplate.Controllers
|
||||
{
|
||||
|
||||
[Route("es")]
|
||||
public class EsController : HomeController
|
||||
{
|
||||
public EsController(
|
||||
ISiteConfigurationService siteConfig,
|
||||
ILanguageService languageService,
|
||||
ISeoService seoService,
|
||||
IMemoryCache cache,
|
||||
IConfiguration configuration)
|
||||
: base(siteConfig, languageService, seoService, cache, configuration)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string GetCurrentLanguage() => "es";
|
||||
|
||||
[Route("")]
|
||||
[Route("inicio")]
|
||||
public new async Task<IActionResult> Index()
|
||||
{
|
||||
return await base.Index();
|
||||
}
|
||||
|
||||
[Route("acerca")]
|
||||
public new async Task<IActionResult> About()
|
||||
{
|
||||
return await base.About();
|
||||
}
|
||||
|
||||
[Route("contacto")]
|
||||
public new async Task<IActionResult> Contact()
|
||||
{
|
||||
return await base.Contact();
|
||||
}
|
||||
}
|
||||
}
|
||||
550
OnlyOneAccessTemplate/Controllers/HomeController.cs
Normal file
550
OnlyOneAccessTemplate/Controllers/HomeController.cs
Normal file
@ -0,0 +1,550 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using OnlyOneAccessTemplate.Services;
|
||||
using OnlyOneAccessTemplate.Models;
|
||||
|
||||
|
||||
namespace OnlyOneAccessTemplate.Controllers
|
||||
{
|
||||
|
||||
public class HomeController : BaseController
|
||||
{
|
||||
public HomeController(
|
||||
ISiteConfigurationService siteConfig,
|
||||
ILanguageService languageService,
|
||||
ISeoService seoService,
|
||||
IMemoryCache cache,
|
||||
IConfiguration configuration)
|
||||
: base(siteConfig, languageService, seoService, cache, configuration)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var language = GetCurrentLanguage();
|
||||
|
||||
// Get page content from database
|
||||
var pageContent = await _siteConfig.GetPageContentAsync(language, "index");
|
||||
|
||||
// If no content exists, create default content
|
||||
if (pageContent == null)
|
||||
{
|
||||
await CreateDefaultContentAsync(language);
|
||||
pageContent = await _siteConfig.GetPageContentAsync(language, "index");
|
||||
}
|
||||
|
||||
// Set current page for navigation
|
||||
ViewBag.CurrentPage = "home";
|
||||
|
||||
return View(pageContent);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> About()
|
||||
{
|
||||
var language = GetCurrentLanguage();
|
||||
var pageContent = await _siteConfig.GetPageContentAsync(language, "about");
|
||||
|
||||
if (pageContent == null)
|
||||
{
|
||||
await CreateDefaultContentAsync(language);
|
||||
pageContent = await _siteConfig.GetPageContentAsync(language, "about");
|
||||
}
|
||||
|
||||
ViewBag.CurrentPage = "about";
|
||||
return View(pageContent);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Contact()
|
||||
{
|
||||
var language = GetCurrentLanguage();
|
||||
var pageContent = await _siteConfig.GetPageContentAsync(language, "contact");
|
||||
|
||||
ViewBag.CurrentPage = "contact";
|
||||
return View(pageContent);
|
||||
}
|
||||
|
||||
[HttpGet("setup-data")]
|
||||
public async Task<IActionResult> SetupDefaultData()
|
||||
{
|
||||
try
|
||||
{
|
||||
var languages = new[] { "pt", "en", "es" };
|
||||
|
||||
foreach (var language in languages)
|
||||
{
|
||||
await CreateDefaultContentAsync(language);
|
||||
}
|
||||
|
||||
return Json(new { success = true, message = "Dados padrão criados com sucesso!" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Json(new { success = false, message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("privacy")]
|
||||
public IActionResult Privacy()
|
||||
{
|
||||
ViewBag.CurrentPage = "privacy";
|
||||
SetPageSeo(
|
||||
"Política de Privacidade",
|
||||
"Nossa política de privacidade e proteção de dados",
|
||||
"privacidade, proteção de dados, lgpd"
|
||||
);
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet("terms")]
|
||||
public IActionResult Terms()
|
||||
{
|
||||
ViewBag.CurrentPage = "terms";
|
||||
SetPageSeo(
|
||||
"Termos de Uso",
|
||||
"Termos e condições de uso do nosso serviço",
|
||||
"termos, condições, uso"
|
||||
);
|
||||
return View();
|
||||
}
|
||||
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public IActionResult Error()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
private async Task CreateDefaultContentAsync(string language)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check if configuration already exists
|
||||
var configExists = await _siteConfig.ConfigurationExistsAsync(language);
|
||||
if (configExists) return;
|
||||
|
||||
// Create default configuration with sample content
|
||||
var siteConfig = new SiteConfiguration
|
||||
{
|
||||
Language = language,
|
||||
Seo = CreateDefaultSeoConfig(language),
|
||||
Pages = CreateDefaultPages(language),
|
||||
Conversion = CreateDefaultConversionConfig(language),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _siteConfig.UpdateConfigurationAsync(siteConfig);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log error but don't break the page
|
||||
Console.WriteLine($"Error creating default content: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private SeoConfig CreateDefaultSeoConfig(string language)
|
||||
{
|
||||
return language switch
|
||||
{
|
||||
"en" => new SeoConfig
|
||||
{
|
||||
SiteName = "OnlyOne Access Template",
|
||||
DefaultTitle = "Transform Your Business - Conversion Template",
|
||||
DefaultDescription = "Professional conversion solutions to boost your business results with proven methodology",
|
||||
DefaultKeywords = "conversion, business, marketing, results, template",
|
||||
TwitterCard = "summary_large_image",
|
||||
GoogleTagManagerId = "GTM-XXXXXXX",
|
||||
GoogleAnalyticsId = "GA-XXXXXXXX",
|
||||
Author = "OnlyOne Access Team",
|
||||
OgImage = "/img/og-image-en.jpg"
|
||||
},
|
||||
"es" => new SeoConfig
|
||||
{
|
||||
SiteName = "OnlyOne Access Template",
|
||||
DefaultTitle = "Transforma Tu Negocio - Template de Conversión",
|
||||
DefaultDescription = "Soluciones profesionales de conversión para potenciar los resultados de tu negocio",
|
||||
DefaultKeywords = "conversión, negocio, marketing, resultados, plantilla",
|
||||
TwitterCard = "summary_large_image",
|
||||
GoogleTagManagerId = "GTM-XXXXXXX",
|
||||
GoogleAnalyticsId = "GA-XXXXXXXX",
|
||||
Author = "Equipo OnlyOne Access",
|
||||
OgImage = "/img/og-image-es.jpg"
|
||||
},
|
||||
_ => new SeoConfig
|
||||
{
|
||||
SiteName = "OnlyOne Access Template",
|
||||
DefaultTitle = "Transforme Seu Negócio - Template de Conversão",
|
||||
DefaultDescription = "Soluções profissionais de conversão para alavancar os resultados do seu negócio",
|
||||
DefaultKeywords = "conversão, negócio, marketing, resultados, template",
|
||||
TwitterCard = "summary_large_image",
|
||||
GoogleTagManagerId = "GTM-XXXXXXX",
|
||||
GoogleAnalyticsId = "GA-XXXXXXXX",
|
||||
Author = "Equipe OnlyOne Access",
|
||||
OgImage = "/img/og-image-pt.jpg"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Dictionary<string, PageContent> CreateDefaultPages(string language)
|
||||
{
|
||||
return language switch
|
||||
{
|
||||
"en" => new Dictionary<string, PageContent>
|
||||
{
|
||||
["index"] = new PageContent
|
||||
{
|
||||
Title = "Transform Your Business Today",
|
||||
Description = "Increase your sales by up to 300% with our proven methodology and expert support",
|
||||
Keywords = "business transformation, conversion, sales growth, methodology",
|
||||
H1 = "Transform Your Business Today",
|
||||
MetaTitle = "Business Transformation | Conversion Solutions",
|
||||
Blocks = CreateEnglishBlocks(),
|
||||
IsActive = true,
|
||||
PublishDate = DateTime.UtcNow
|
||||
},
|
||||
["about"] = new PageContent
|
||||
{
|
||||
Title = "About Us - Our Mission",
|
||||
Description = "Learn about our mission to help businesses achieve better conversion rates",
|
||||
H1 = "About Our Company",
|
||||
MetaTitle = "About Us | Conversion Experts"
|
||||
}
|
||||
},
|
||||
"es" => new Dictionary<string, PageContent>
|
||||
{
|
||||
["index"] = new PageContent
|
||||
{
|
||||
Title = "Transforma Tu Negocio Hoy",
|
||||
Description = "Aumenta tus ventas hasta un 300% con nuestra metodología probada y soporte especializado",
|
||||
Keywords = "transformación empresarial, conversión, crecimiento de ventas, metodología",
|
||||
H1 = "Transforma Tu Negocio Hoy",
|
||||
MetaTitle = "Transformación Empresarial | Soluciones de Conversión",
|
||||
Blocks = CreateSpanishBlocks(),
|
||||
IsActive = true,
|
||||
PublishDate = DateTime.UtcNow
|
||||
},
|
||||
["about"] = new PageContent
|
||||
{
|
||||
Title = "Acerca de Nosotros - Nuestra Misión",
|
||||
Description = "Conoce nuestra misión de ayudar a las empresas a lograr mejores tasas de conversión",
|
||||
H1 = "Acerca de Nuestra Empresa",
|
||||
MetaTitle = "Acerca de Nosotros | Expertos en Conversión"
|
||||
}
|
||||
},
|
||||
_ => new Dictionary<string, PageContent>
|
||||
{
|
||||
["index"] = new PageContent
|
||||
{
|
||||
Title = "Transforme Seu Negócio Hoje",
|
||||
Description = "Aumente suas vendas em até 300% com nossa metodologia comprovada e suporte especializado",
|
||||
Keywords = "transformação empresarial, conversão, crescimento de vendas, metodologia",
|
||||
H1 = "Transforme Seu Negócio Hoje",
|
||||
MetaTitle = "Transformação Empresarial | Soluções de Conversão",
|
||||
Blocks = CreatePortugueseBlocks(),
|
||||
IsActive = true,
|
||||
PublishDate = DateTime.UtcNow
|
||||
},
|
||||
["about"] = new PageContent
|
||||
{
|
||||
Title = "Sobre Nós - Nossa Missão",
|
||||
Description = "Conheça nossa missão de ajudar empresas a alcançar melhores taxas de conversão",
|
||||
H1 = "Sobre Nossa Empresa",
|
||||
MetaTitle = "Sobre Nós | Especialistas em Conversão"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private List<ContentBlock> CreatePortugueseBlocks()
|
||||
{
|
||||
return new List<ContentBlock>
|
||||
{
|
||||
new ContentBlock
|
||||
{
|
||||
Type = "hero",
|
||||
Title = "Transforme Seu Negócio com Nossa <span class='text-gradient'>Solução Inovadora</span>",
|
||||
Content = "Aumente suas vendas em até 300% com nossa metodologia comprovada, suporte especializado e resultados garantidos em 30 dias.",
|
||||
ImageUrl = "/img/hero-pt.jpg",
|
||||
ButtonText = "Começar Transformação",
|
||||
ButtonUrl = "#conversion-form",
|
||||
Order = 1,
|
||||
Properties = new Dictionary<string, object>
|
||||
{
|
||||
{ "badge", "🚀 Mais de 1000 empresas transformadas" },
|
||||
{ "features", new[] { "✅ Metodologia comprovada", "✅ Suporte 24/7", "✅ Resultados em 30 dias" } },
|
||||
{ "social_proof", "Mais de 500 avaliações 5 estrelas" }
|
||||
}
|
||||
},
|
||||
new ContentBlock
|
||||
{
|
||||
Type = "features",
|
||||
Title = "Por Que Escolher Nossa Solução?",
|
||||
Content = "Descubra os benefícios que já transformaram mais de 1000 empresas em todo o Brasil",
|
||||
Order = 2,
|
||||
Properties = new Dictionary<string, object>
|
||||
{
|
||||
{ "feature_list", new[]
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "icon", "fas fa-rocket" },
|
||||
{ "title", "Resultados Rápidos" },
|
||||
{ "description", "Veja os primeiros resultados em até 30 dias com nossa metodologia testada" }
|
||||
},
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "icon", "fas fa-shield-alt" },
|
||||
{ "title", "Garantia Total" },
|
||||
{ "description", "100% de garantia ou seu dinheiro de volta. Assumimos o risco por você" }
|
||||
},
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "icon", "fas fa-users" },
|
||||
{ "title", "Suporte Especializado" },
|
||||
{ "description", "Equipe dedicada 24/7 para garantir seu sucesso em cada etapa" }
|
||||
},
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "icon", "fas fa-chart-line" },
|
||||
{ "title", "Crescimento Sustentável" },
|
||||
{ "description", "Estratégias de longo prazo para crescimento consistente e duradouro" }
|
||||
},
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "icon", "fas fa-cog" },
|
||||
{ "title", "Automatização" },
|
||||
{ "description", "Processos automatizados que trabalham para você 24 horas por dia" }
|
||||
},
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "icon", "fas fa-trophy" },
|
||||
{ "title", "Casos de Sucesso" },
|
||||
{ "description", "Mais de 1000 empresas já alcançaram resultados extraordinários" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
new ContentBlock
|
||||
{
|
||||
Type = "testimonials",
|
||||
Title = "O Que Nossos Clientes Dizem",
|
||||
Content = "Mais de 1000 empresas já transformaram seus resultados. Veja alguns depoimentos:",
|
||||
Order = 3,
|
||||
Properties = new Dictionary<string, object>
|
||||
{
|
||||
{ "testimonials", new[]
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "quote", "Aumentamos nossas vendas em 250% em apenas 3 meses. A metodologia realmente funciona e o suporte é excepcional!" },
|
||||
{ "name", "Maria Silva" },
|
||||
{ "position", "CEO, TechStart Brasil" },
|
||||
{ "avatar", "/img/testimonial-1.jpg" }
|
||||
},
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "quote", "O ROI foi incrível. Recuperamos o investimento em 30 dias e continuamos crescendo desde então." },
|
||||
{ "name", "João Santos" },
|
||||
{ "position", "Diretor Comercial, InovaCorp" },
|
||||
{ "avatar", "/img/testimonial-2.jpg" }
|
||||
},
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "quote", "Finalmente encontramos uma solução que entrega o que promete. Resultados desde o primeiro mês!" },
|
||||
{ "name", "Ana Costa" },
|
||||
{ "position", "Fundadora, GrowBiz" },
|
||||
{ "avatar", "/img/testimonial-3.jpg" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
new ContentBlock
|
||||
{
|
||||
Type = "cta",
|
||||
Title = "Pronto para <span class='text-warning'>Transformar</span> Seu Negócio?",
|
||||
Content = "Junte-se a mais de 1000 empresas que já alcançaram resultados extraordinários com nossa metodologia comprovada.",
|
||||
ButtonText = "Começar Minha Transformação",
|
||||
ButtonUrl = "#conversion-form",
|
||||
Order = 4,
|
||||
Properties = new Dictionary<string, object>
|
||||
{
|
||||
{ "urgency_badge", "⚡ Últimas vagas da semana" },
|
||||
{ "guarantee", "🛡️ Garantia de 30 dias ou seu dinheiro de volta" },
|
||||
{ "secondary_button_text", "Assistir Demonstração" },
|
||||
{ "secondary_button_url", "#demo-video" }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private List<ContentBlock> CreateEnglishBlocks()
|
||||
{
|
||||
return new List<ContentBlock>
|
||||
{
|
||||
new ContentBlock
|
||||
{
|
||||
Type = "hero",
|
||||
Title = "Transform Your Business with Our <span class='text-gradient'>Innovative Solution</span>",
|
||||
Content = "Increase your sales by up to 300% with our proven methodology, expert support and guaranteed results in 30 days.",
|
||||
ImageUrl = "/img/hero-en.jpg",
|
||||
ButtonText = "Start Transformation",
|
||||
ButtonUrl = "#conversion-form",
|
||||
Order = 1,
|
||||
Properties = new Dictionary<string, object>
|
||||
{
|
||||
{ "badge", "🚀 Over 1000 companies transformed" },
|
||||
{ "features", new[] { "✅ Proven methodology", "✅ 24/7 Support", "✅ Results in 30 days" } },
|
||||
{ "social_proof", "Over 500 five-star reviews" }
|
||||
}
|
||||
},
|
||||
new ContentBlock
|
||||
{
|
||||
Type = "features",
|
||||
Title = "Why Choose Our Solution?",
|
||||
Content = "Discover the benefits that have already transformed over 1000 companies worldwide",
|
||||
Order = 2,
|
||||
Properties = new Dictionary<string, object>
|
||||
{
|
||||
{ "feature_list", new[]
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "icon", "fas fa-rocket" },
|
||||
{ "title", "Fast Results" },
|
||||
{ "description", "See first results within 30 days with our tested methodology" }
|
||||
},
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "icon", "fas fa-shield-alt" },
|
||||
{ "title", "Full Guarantee" },
|
||||
{ "description", "100% guarantee or your money back. We take the risk for you" }
|
||||
},
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "icon", "fas fa-users" },
|
||||
{ "title", "Expert Support" },
|
||||
{ "description", "24/7 dedicated team to ensure your success at every step" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
new ContentBlock
|
||||
{
|
||||
Type = "testimonials",
|
||||
Title = "What Our Clients Say",
|
||||
Content = "Over 1000 companies have already transformed their results. See some testimonials:",
|
||||
Order = 3,
|
||||
Properties = new Dictionary<string, object>
|
||||
{
|
||||
{ "testimonials", new[]
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "quote", "We increased our sales by 250% in just 3 months. The methodology really works!" },
|
||||
{ "name", "Mary Johnson" },
|
||||
{ "position", "CEO, TechStart USA" },
|
||||
{ "avatar", "/img/testimonial-en-1.jpg" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
new ContentBlock
|
||||
{
|
||||
Type = "cta",
|
||||
Title = "Ready to <span class='text-warning'>Transform</span> Your Business?",
|
||||
Content = "Join over 1000 companies that have already achieved extraordinary results.",
|
||||
ButtonText = "Start My Transformation",
|
||||
ButtonUrl = "#conversion-form",
|
||||
Order = 4
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private List<ContentBlock> CreateSpanishBlocks()
|
||||
{
|
||||
return new List<ContentBlock>
|
||||
{
|
||||
new ContentBlock
|
||||
{
|
||||
Type = "hero",
|
||||
Title = "Transforma Tu Negocio con Nuestra <span class='text-gradient'>Solución Innovadora</span>",
|
||||
Content = "Aumenta tus ventas hasta un 300% con nuestra metodología probada, soporte especializado y resultados garantizados en 30 días.",
|
||||
ImageUrl = "/img/hero-es.jpg",
|
||||
ButtonText = "Comenzar Transformación",
|
||||
ButtonUrl = "#conversion-form",
|
||||
Order = 1,
|
||||
Properties = new Dictionary<string, object>
|
||||
{
|
||||
{ "badge", "🚀 Más de 1000 empresas transformadas" },
|
||||
{ "features", new[] { "✅ Metodología probada", "✅ Soporte 24/7", "✅ Resultados en 30 días" } },
|
||||
{ "social_proof", "Más de 500 reseñas de 5 estrellas" }
|
||||
}
|
||||
},
|
||||
new ContentBlock
|
||||
{
|
||||
Type = "features",
|
||||
Title = "¿Por Qué Elegir Nuestra Solución?",
|
||||
Content = "Descubre los beneficios que ya han transformado más de 1000 empresas en todo el mundo",
|
||||
Order = 2
|
||||
},
|
||||
new ContentBlock
|
||||
{
|
||||
Type = "testimonials",
|
||||
Title = "Lo Que Dicen Nuestros Clientes",
|
||||
Content = "Más de 1000 empresas ya han transformado sus resultados. Ve algunos testimonios:",
|
||||
Order = 3
|
||||
},
|
||||
new ContentBlock
|
||||
{
|
||||
Type = "cta",
|
||||
Title = "¿Listo para <span class='text-warning'>Transformar</span> Tu Negocio?",
|
||||
Content = "Únete a más de 1000 empresas que ya han logrado resultados extraordinarios.",
|
||||
ButtonText = "Comenzar Mi Transformación",
|
||||
ButtonUrl = "#conversion-form",
|
||||
Order = 4
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private ConversionConfig CreateDefaultConversionConfig(string language)
|
||||
{
|
||||
return new ConversionConfig
|
||||
{
|
||||
FormAction = "/conversion/submit",
|
||||
ThankYouPage = language switch
|
||||
{
|
||||
"en" => "/en/thank-you",
|
||||
"es" => "/es/gracias",
|
||||
_ => "/obrigado"
|
||||
},
|
||||
FormFields = language switch
|
||||
{
|
||||
"en" => new List<FormField>
|
||||
{
|
||||
new() { Name = "name", Type = "text", Label = "Full Name", Placeholder = "Enter your full name", Required = true, Order = 1 },
|
||||
new() { Name = "email", Type = "email", Label = "Email Address", Placeholder = "Enter your email", Required = true, Order = 2 },
|
||||
new() { Name = "phone", Type = "tel", Label = "Phone Number", Placeholder = "Enter your phone number", Required = false, Order = 3 },
|
||||
new() { Name = "company", Type = "text", Label = "Company Name", Placeholder = "Enter your company name", Required = false, Order = 4 }
|
||||
},
|
||||
"es" => new List<FormField>
|
||||
{
|
||||
new() { Name = "name", Type = "text", Label = "Nombre Completo", Placeholder = "Ingresa tu nombre completo", Required = true, Order = 1 },
|
||||
new() { Name = "email", Type = "email", Label = "Correo Electrónico", Placeholder = "Ingresa tu correo", Required = true, Order = 2 },
|
||||
new() { Name = "phone", Type = "tel", Label = "Número de Teléfono", Placeholder = "Ingresa tu teléfono", Required = false, Order = 3 },
|
||||
new() { Name = "company", Type = "text", Label = "Nombre de la Empresa", Placeholder = "Ingresa el nombre de tu empresa", Required = false, Order = 4 }
|
||||
},
|
||||
_ => new List<FormField>
|
||||
{
|
||||
new() { Name = "name", Type = "text", Label = "Nome Completo", Placeholder = "Digite seu nome completo", Required = true, Order = 1 },
|
||||
new() { Name = "email", Type = "email", Label = "E-mail", Placeholder = "Digite seu e-mail", Required = true, Order = 2 },
|
||||
new() { Name = "phone", Type = "tel", Label = "Telefone", Placeholder = "Digite seu telefone", Required = false, Order = 3 },
|
||||
new() { Name = "company", Type = "text", Label = "Nome da Empresa", Placeholder = "Digite o nome da sua empresa", Required = false, Order = 4 }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
14
OnlyOneAccessTemplate/Models/ContentBlock.cs
Normal file
14
OnlyOneAccessTemplate/Models/ContentBlock.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace OnlyOneAccessTemplate.Models
|
||||
{
|
||||
public record ContentBlock
|
||||
{
|
||||
public string Type { get; init; } = string.Empty; // "hero", "features", "testimonials", "cta", "form"
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public string Content { get; init; } = string.Empty;
|
||||
public string ImageUrl { get; init; } = string.Empty;
|
||||
public string ButtonText { get; init; } = string.Empty;
|
||||
public string ButtonUrl { get; init; } = string.Empty;
|
||||
public Dictionary<string, object> Properties { get; init; } = new();
|
||||
public int Order { get; init; } = 0;
|
||||
}
|
||||
}
|
||||
14
OnlyOneAccessTemplate/Models/ConversionConfig.cs
Normal file
14
OnlyOneAccessTemplate/Models/ConversionConfig.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace OnlyOneAccessTemplate.Models
|
||||
{
|
||||
public record ConversionConfig
|
||||
{
|
||||
public string FormAction { get; init; } = string.Empty;
|
||||
public string ThankYouPage { get; init; } = "/obrigado";
|
||||
public List<FormField> FormFields { get; init; } = new();
|
||||
public string ConversionPixel { get; init; } = string.Empty;
|
||||
public string WebhookUrl { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
}
|
||||
9
OnlyOneAccessTemplate/Models/ErrorViewModel.cs
Normal file
9
OnlyOneAccessTemplate/Models/ErrorViewModel.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace OnlyOneAccessTemplate.Models
|
||||
{
|
||||
public class ErrorViewModel
|
||||
{
|
||||
public string? RequestId { get; set; }
|
||||
|
||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
}
|
||||
}
|
||||
15
OnlyOneAccessTemplate/Models/FormField.cs
Normal file
15
OnlyOneAccessTemplate/Models/FormField.cs
Normal file
@ -0,0 +1,15 @@
|
||||
namespace OnlyOneAccessTemplate.Models
|
||||
{
|
||||
public record FormField
|
||||
{
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Type { get; init; } = "text";
|
||||
public string Label { get; init; } = string.Empty;
|
||||
public string Placeholder { get; init; } = string.Empty;
|
||||
public bool Required { get; init; } = false;
|
||||
public string ValidationRegex { get; init; } = string.Empty;
|
||||
public int Order { get; init; } = 0;
|
||||
|
||||
public Dictionary<string, List<string>> Properties { get; set; }
|
||||
}
|
||||
}
|
||||
15
OnlyOneAccessTemplate/Models/PageContent.cs
Normal file
15
OnlyOneAccessTemplate/Models/PageContent.cs
Normal file
@ -0,0 +1,15 @@
|
||||
namespace OnlyOneAccessTemplate.Models
|
||||
{
|
||||
public record PageContent
|
||||
{
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public string Keywords { get; init; } = string.Empty;
|
||||
public string H1 { get; init; } = string.Empty;
|
||||
public string MetaTitle { get; init; } = string.Empty;
|
||||
public string OgImage { get; init; } = string.Empty;
|
||||
public List<ContentBlock> Blocks { get; init; } = new();
|
||||
public bool IsActive { get; init; } = true;
|
||||
public DateTime? PublishDate { get; init; }
|
||||
}
|
||||
}
|
||||
18
OnlyOneAccessTemplate/Models/SeoConfig.cs
Normal file
18
OnlyOneAccessTemplate/Models/SeoConfig.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace OnlyOneAccessTemplate.Models
|
||||
{
|
||||
public record SeoConfig
|
||||
{
|
||||
public string SiteName { get; init; } = string.Empty;
|
||||
public string DefaultTitle { get; init; } = string.Empty;
|
||||
public string DefaultDescription { get; init; } = string.Empty;
|
||||
public string DefaultKeywords { get; init; } = string.Empty;
|
||||
public string OgImage { get; init; } = string.Empty;
|
||||
public string TwitterCard { get; init; } = "summary_large_image";
|
||||
public string TwitterHandle { get; init; } = string.Empty;
|
||||
public string GoogleTagManagerId { get; init; } = string.Empty;
|
||||
public string GoogleAnalyticsId { get; init; } = string.Empty;
|
||||
public string FacebookPixelId { get; init; } = string.Empty;
|
||||
public string Author { get; init; } = string.Empty;
|
||||
public string Favicon { get; init; } = "/favicon.ico";
|
||||
}
|
||||
}
|
||||
22
OnlyOneAccessTemplate/Models/SiteConfiguration.cs
Normal file
22
OnlyOneAccessTemplate/Models/SiteConfiguration.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace OnlyOneAccessTemplate.Models
|
||||
{
|
||||
public class SiteConfiguration
|
||||
{
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Language { get; set; } = "pt";
|
||||
|
||||
public SeoConfig Seo { get; set; } = new();
|
||||
public Dictionary<string, PageContent> Pages { get; set; } = new();
|
||||
public ConversionConfig Conversion { get; set; } = new();
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
18
OnlyOneAccessTemplate/OnlyOneAccessTemplate.csproj
Normal file
18
OnlyOneAccessTemplate/OnlyOneAccessTemplate.csproj
Normal file
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Views\En\" />
|
||||
<Folder Include="Views\Es\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
104
OnlyOneAccessTemplate/Program.cs
Normal file
104
OnlyOneAccessTemplate/Program.cs
Normal file
@ -0,0 +1,104 @@
|
||||
using MongoDB.Driver;
|
||||
using OnlyOneAccessTemplate.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container
|
||||
builder.Services.AddControllersWithViews();
|
||||
|
||||
// Session support
|
||||
builder.Services.AddDistributedMemoryCache();
|
||||
builder.Services.AddSession(options =>
|
||||
{
|
||||
options.IdleTimeout = TimeSpan.FromMinutes(30);
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.Cookie.IsEssential = true;
|
||||
});
|
||||
|
||||
// MongoDB Configuration
|
||||
builder.Services.AddSingleton<IMongoClient>(sp =>
|
||||
{
|
||||
var connectionString = builder.Configuration.GetConnectionString("MongoDB");
|
||||
return new MongoClient(connectionString);
|
||||
});
|
||||
|
||||
builder.Services.AddScoped(sp =>
|
||||
{
|
||||
var client = sp.GetRequiredService<IMongoClient>();
|
||||
var databaseName = builder.Configuration.GetValue<string>("MongoDB:DatabaseName");
|
||||
return client.GetDatabase(databaseName);
|
||||
});
|
||||
|
||||
// Custom Services
|
||||
builder.Services.AddScoped<ISiteConfigurationService, SiteConfigurationService>();
|
||||
builder.Services.AddScoped<ILanguageService, LanguageService>();
|
||||
builder.Services.AddScoped<ISeoService, SeoService>();
|
||||
builder.Services.AddScoped<IConversionService, ConversionService>();
|
||||
|
||||
// HttpClient for external calls
|
||||
builder.Services.AddHttpClient<ConversionService>();
|
||||
|
||||
// Response Compression
|
||||
builder.Services.AddResponseCompression(options =>
|
||||
{
|
||||
options.EnableForHttps = true;
|
||||
});
|
||||
|
||||
// Response Caching
|
||||
builder.Services.AddResponseCaching();
|
||||
|
||||
// Memory Cache
|
||||
builder.Services.AddMemoryCache();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Home/Error");
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
|
||||
// Session
|
||||
app.UseSession();
|
||||
|
||||
// Response Compression & Caching
|
||||
app.UseResponseCompression();
|
||||
app.UseResponseCaching();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
// Custom routing for multilingual support
|
||||
app.MapControllerRoute(
|
||||
name: "default-pt",
|
||||
pattern: "{controller=Home}/{action=Index}/{id?}");
|
||||
|
||||
app.MapControllerRoute(
|
||||
name: "pt-explicit",
|
||||
pattern: "pt/{controller=Home}/{action=Index}/{id?}");
|
||||
|
||||
app.MapControllerRoute(
|
||||
name: "english",
|
||||
pattern: "en/{controller=En}/{action=Index}/{id?}");
|
||||
|
||||
app.MapControllerRoute(
|
||||
name: "spanish",
|
||||
pattern: "es/{controller=Es}/{action=Index}/{id?}");
|
||||
|
||||
// SEO Routes
|
||||
app.MapGet("/sitemap.xml", async (ISiteConfigurationService siteConfig) =>
|
||||
{
|
||||
var sitemap = await siteConfig.GenerateSitemapAsync();
|
||||
return Results.Content(sitemap, "application/xml");
|
||||
});
|
||||
|
||||
app.MapGet("/robots.txt", async (ISiteConfigurationService siteConfig) =>
|
||||
{
|
||||
var robots = await siteConfig.GenerateRobotsAsync();
|
||||
return Results.Content(robots, "text/plain");
|
||||
});
|
||||
|
||||
app.Run();
|
||||
38
OnlyOneAccessTemplate/Properties/launchSettings.json
Normal file
38
OnlyOneAccessTemplate/Properties/launchSettings.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:65221",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5201",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7299;http://localhost:5201",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
536
OnlyOneAccessTemplate/Services/ConversionService.cs
Normal file
536
OnlyOneAccessTemplate/Services/ConversionService.cs
Normal file
@ -0,0 +1,536 @@
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Bson;
|
||||
using System.Text.Json;
|
||||
using System.Net.Http;
|
||||
using OnlyOneAccessTemplate.Models;
|
||||
|
||||
namespace OnlyOneAccessTemplate.Services
|
||||
{
|
||||
public class ConversionService : IConversionService
|
||||
{
|
||||
private readonly IMongoDatabase _database;
|
||||
private readonly IMongoCollection<ConversionRecord> _conversions;
|
||||
private readonly IMongoCollection<ConversionEvent> _events;
|
||||
private readonly ISiteConfigurationService _siteConfig;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<ConversionService> _logger;
|
||||
|
||||
public ConversionService(
|
||||
IMongoDatabase database,
|
||||
ISiteConfigurationService siteConfig,
|
||||
HttpClient httpClient,
|
||||
ILogger<ConversionService> logger)
|
||||
{
|
||||
_database = database;
|
||||
_siteConfig = siteConfig;
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_conversions = _database.GetCollection<ConversionRecord>("conversions");
|
||||
_events = _database.GetCollection<ConversionEvent>("conversion_events");
|
||||
}
|
||||
|
||||
public async Task<bool> ProcessConversionAsync(ConversionData data)
|
||||
{
|
||||
try
|
||||
{
|
||||
var configuration = await _siteConfig.GetConfigurationAsync(data.Language);
|
||||
|
||||
// Criar registro de conversão
|
||||
var conversionRecord = new ConversionRecord
|
||||
{
|
||||
Id = ObjectId.GenerateNewId().ToString(),
|
||||
Language = data.Language,
|
||||
PageName = data.PageName,
|
||||
FormData = data.FormData,
|
||||
UserAgent = data.UserAgent,
|
||||
IpAddress = data.IpAddress,
|
||||
Referrer = data.Referrer,
|
||||
ConvertedAt = DateTime.UtcNow,
|
||||
SessionId = GenerateSessionId(),
|
||||
Source = ExtractSource(data.Referrer),
|
||||
Medium = ExtractMedium(data.Referrer),
|
||||
Campaign = ExtractCampaign(data.Referrer)
|
||||
};
|
||||
|
||||
await _conversions.InsertOneAsync(conversionRecord);
|
||||
|
||||
// Log evento de conversão
|
||||
await LogConversionEventAsync("conversion_completed", new Dictionary<string, object>
|
||||
{
|
||||
{ "conversion_id", conversionRecord.Id },
|
||||
{ "page_name", data.PageName },
|
||||
{ "language", data.Language },
|
||||
{ "source", conversionRecord.Source },
|
||||
{ "medium", conversionRecord.Medium }
|
||||
});
|
||||
|
||||
// Enviar para webhook se configurado
|
||||
if (!string.IsNullOrEmpty(configuration.Conversion.WebhookUrl))
|
||||
{
|
||||
_ = Task.Run(() => SendWebhookAsync(configuration.Conversion.WebhookUrl, conversionRecord));
|
||||
}
|
||||
|
||||
// Disparar pixel de conversão se configurado
|
||||
if (!string.IsNullOrEmpty(configuration.Conversion.ConversionPixel))
|
||||
{
|
||||
_ = Task.Run(() => FireConversionPixelAsync(configuration.Conversion.ConversionPixel, conversionRecord));
|
||||
}
|
||||
|
||||
_logger.LogInformation("Conversão processada com sucesso: {ConversionId}", conversionRecord.Id);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao processar conversão para {Language}/{PageName}", data.Language, data.PageName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ConversionStats> GetConversionStatsAsync(string language, DateTime from, DateTime to)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filter = Builders<ConversionRecord>.Filter.And(
|
||||
Builders<ConversionRecord>.Filter.Eq(x => x.Language, language),
|
||||
Builders<ConversionRecord>.Filter.Gte(x => x.ConvertedAt, from),
|
||||
Builders<ConversionRecord>.Filter.Lte(x => x.ConvertedAt, to)
|
||||
);
|
||||
|
||||
// Total de conversões
|
||||
var totalConversions = await _conversions.CountDocumentsAsync(filter);
|
||||
|
||||
// Conversões por fonte
|
||||
var conversionsBySource = await _conversions
|
||||
.Aggregate()
|
||||
.Match(filter)
|
||||
.Group(x => x.Source, g => new { Source = g.Key, Count = g.Count() })
|
||||
.ToListAsync();
|
||||
|
||||
var sourceDict = conversionsBySource.ToDictionary(x => x.Source ?? "direct", x => x.Count);
|
||||
|
||||
// Para calcular taxa de conversão, precisaríamos de dados de visitantes
|
||||
// Por enquanto, vamos usar uma estimativa baseada em eventos
|
||||
var visitorFilter = Builders<ConversionEvent>.Filter.And(
|
||||
Builders<ConversionEvent>.Filter.Eq("Properties.language", language),
|
||||
Builders<ConversionEvent>.Filter.Eq(x => x.EventName, "page_view"),
|
||||
Builders<ConversionEvent>.Filter.Gte(x => x.Timestamp, from),
|
||||
Builders<ConversionEvent>.Filter.Lte(x => x.Timestamp, to)
|
||||
);
|
||||
|
||||
var totalVisits = await _events.CountDocumentsAsync(visitorFilter);
|
||||
var conversionRate = totalVisits > 0 ? (double)totalConversions / totalVisits * 100 : 0;
|
||||
|
||||
return new ConversionStats(
|
||||
TotalVisits: (int)totalVisits,
|
||||
TotalConversions: (int)totalConversions,
|
||||
ConversionRate: Math.Round(conversionRate, 2),
|
||||
ConversionsBySource: sourceDict
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao buscar estatísticas de conversão para {Language}", language);
|
||||
return new ConversionStats(0, 0, 0, new Dictionary<string, int>());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LogConversionEventAsync(string eventName, Dictionary<string, object> properties)
|
||||
{
|
||||
try
|
||||
{
|
||||
var conversionEvent = new ConversionEvent
|
||||
{
|
||||
Id = ObjectId.GenerateNewId().ToString(),
|
||||
EventName = eventName,
|
||||
Properties = properties,
|
||||
Timestamp = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _events.InsertOneAsync(conversionEvent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao registrar evento {EventName}", eventName);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ConversionRecord?> GetConversionByIdAsync(string conversionId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filter = Builders<ConversionRecord>.Filter.Eq(x => x.Id, conversionId);
|
||||
return await _conversions.Find(filter).FirstOrDefaultAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao buscar conversão {ConversionId}", conversionId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<ConversionRecord>> GetConversionsAsync(string language, int skip = 0, int take = 50)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filter = Builders<ConversionRecord>.Filter.Eq(x => x.Language, language);
|
||||
return await _conversions
|
||||
.Find(filter)
|
||||
.SortByDescending(x => x.ConvertedAt)
|
||||
.Skip(skip)
|
||||
.Limit(take)
|
||||
.ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao buscar conversões para idioma {Language}", language);
|
||||
return new List<ConversionRecord>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateFormDataAsync(Dictionary<string, string> formData, List<FormField> formFields)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var field in formFields.Where(f => f.Required))
|
||||
{
|
||||
if (!formData.ContainsKey(field.Name) || string.IsNullOrWhiteSpace(formData[field.Name]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validação de email
|
||||
if (field.Type == "email" && !IsValidEmail(formData[field.Name]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validação de regex personalizada
|
||||
if (!string.IsNullOrEmpty(field.ValidationRegex) &&
|
||||
!System.Text.RegularExpressions.Regex.IsMatch(formData[field.Name], field.ValidationRegex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao validar dados do formulário");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ConversionStats> GetRealTimeStatsAsync(string language)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var last24Hours = now.AddHours(-24);
|
||||
return await GetConversionStatsAsync(language, last24Hours, now);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, object>> GetConversionMetricsAsync(string language, string period)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (from, to) = GetDateRangeFromPeriod(period);
|
||||
var stats = await GetConversionStatsAsync(language, from, to);
|
||||
|
||||
var metrics = new Dictionary<string, object>
|
||||
{
|
||||
{ "total_conversions", stats.TotalConversions },
|
||||
{ "total_visits", stats.TotalVisits },
|
||||
{ "conversion_rate", stats.ConversionRate },
|
||||
{ "period", period },
|
||||
{ "from_date", from },
|
||||
{ "to_date", to },
|
||||
{ "conversions_by_source", stats.ConversionsBySource }
|
||||
};
|
||||
|
||||
return metrics;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao buscar métricas para {Language}/{Period}", language, period);
|
||||
return new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SendConversionNotificationAsync(ConversionRecord conversion)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Este método pode ser expandido para enviar notificações por email, Slack, etc.
|
||||
await LogConversionEventAsync("conversion_notification_sent", new Dictionary<string, object>
|
||||
{
|
||||
{ "conversion_id", conversion.Id },
|
||||
{ "language", conversion.Language },
|
||||
{ "page_name", conversion.PageName }
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao enviar notificação de conversão {ConversionId}", conversion.Id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<ConversionTrend>> GetConversionTrendsAsync(string language, DateTime from, DateTime to, string groupBy = "day")
|
||||
{
|
||||
try
|
||||
{
|
||||
var filter = Builders<ConversionRecord>.Filter.And(
|
||||
Builders<ConversionRecord>.Filter.Eq(x => x.Language, language),
|
||||
Builders<ConversionRecord>.Filter.Gte(x => x.ConvertedAt, from),
|
||||
Builders<ConversionRecord>.Filter.Lte(x => x.ConvertedAt, to)
|
||||
);
|
||||
|
||||
var conversions = await _conversions.Find(filter).ToListAsync();
|
||||
var trends = new List<ConversionTrend>();
|
||||
|
||||
var currentDate = from.Date;
|
||||
while (currentDate <= to.Date)
|
||||
{
|
||||
var dayConversions = conversions.Where(c => c.ConvertedAt.Date == currentDate).Count();
|
||||
|
||||
// Para visitas, você pode implementar tracking separado ou usar uma estimativa
|
||||
var dayVisits = dayConversions > 0 ? dayConversions * 10 : 0; // Estimativa
|
||||
var conversionRate = dayVisits > 0 ? (double)dayConversions / dayVisits * 100 : 0;
|
||||
|
||||
trends.Add(new ConversionTrend(
|
||||
Date: currentDate,
|
||||
Conversions: dayConversions,
|
||||
Visits: dayVisits,
|
||||
ConversionRate: Math.Round(conversionRate, 2)
|
||||
));
|
||||
|
||||
currentDate = groupBy switch
|
||||
{
|
||||
"hour" => currentDate.AddHours(1),
|
||||
"week" => currentDate.AddDays(7),
|
||||
"month" => currentDate.AddMonths(1),
|
||||
_ => currentDate.AddDays(1)
|
||||
};
|
||||
}
|
||||
|
||||
return trends;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao buscar tendências de conversão para {Language}", language);
|
||||
return new List<ConversionTrend>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendWebhookAsync(string webhookUrl, ConversionRecord conversion)
|
||||
{
|
||||
try
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
conversion_id = conversion.Id,
|
||||
language = conversion.Language,
|
||||
page_name = conversion.PageName,
|
||||
form_data = conversion.FormData,
|
||||
converted_at = conversion.ConvertedAt,
|
||||
source = conversion.Source,
|
||||
medium = conversion.Medium,
|
||||
campaign = conversion.Campaign
|
||||
};
|
||||
|
||||
var jsonContent = JsonSerializer.Serialize(payload);
|
||||
var content = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await _httpClient.PostAsync(webhookUrl, content);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogInformation("Webhook enviado com sucesso para {WebhookUrl}", webhookUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Falha ao enviar webhook para {WebhookUrl}. Status: {StatusCode}",
|
||||
webhookUrl, response.StatusCode);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao enviar webhook para {WebhookUrl}", webhookUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FireConversionPixelAsync(string pixelUrl, ConversionRecord conversion)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Substituir placeholders no pixel URL
|
||||
var processedUrl = pixelUrl
|
||||
.Replace("{conversion_id}", conversion.Id)
|
||||
.Replace("{value}", "1")
|
||||
.Replace("{currency}", "BRL");
|
||||
|
||||
var response = await _httpClient.GetAsync(processedUrl);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogInformation("Pixel de conversão disparado com sucesso");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Falha ao disparar pixel de conversão. Status: {StatusCode}", response.StatusCode);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao disparar pixel de conversão");
|
||||
}
|
||||
}
|
||||
|
||||
private string GenerateSessionId()
|
||||
{
|
||||
return Guid.NewGuid().ToString("N")[..16];
|
||||
}
|
||||
|
||||
private string ExtractSource(string? referrer)
|
||||
{
|
||||
if (string.IsNullOrEmpty(referrer))
|
||||
return "direct";
|
||||
|
||||
try
|
||||
{
|
||||
var uri = new Uri(referrer);
|
||||
var host = uri.Host.ToLowerInvariant();
|
||||
|
||||
return host switch
|
||||
{
|
||||
var h when h.Contains("google") => "google",
|
||||
var h when h.Contains("facebook") => "facebook",
|
||||
var h when h.Contains("instagram") => "instagram",
|
||||
var h when h.Contains("youtube") => "youtube",
|
||||
var h when h.Contains("linkedin") => "linkedin",
|
||||
var h when h.Contains("twitter") => "twitter",
|
||||
var h when h.Contains("tiktok") => "tiktok",
|
||||
_ => host
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
private string ExtractMedium(string? referrer)
|
||||
{
|
||||
if (string.IsNullOrEmpty(referrer))
|
||||
return "direct";
|
||||
|
||||
try
|
||||
{
|
||||
var uri = new Uri(referrer);
|
||||
var query = uri.Query;
|
||||
|
||||
if (query.Contains("utm_medium="))
|
||||
{
|
||||
var match = System.Text.RegularExpressions.Regex.Match(query, @"utm_medium=([^&]+)");
|
||||
if (match.Success)
|
||||
return Uri.UnescapeDataString(match.Groups[1].Value);
|
||||
}
|
||||
|
||||
var host = uri.Host.ToLowerInvariant();
|
||||
return host switch
|
||||
{
|
||||
var h when h.Contains("google") => "search",
|
||||
var h when h.Contains("facebook") || h.Contains("instagram") => "social",
|
||||
var h when h.Contains("youtube") || h.Contains("tiktok") => "video",
|
||||
var h when h.Contains("linkedin") || h.Contains("twitter") => "social",
|
||||
_ => "referral"
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
private string ExtractCampaign(string? referrer)
|
||||
{
|
||||
if (string.IsNullOrEmpty(referrer))
|
||||
return "direct";
|
||||
|
||||
try
|
||||
{
|
||||
var uri = new Uri(referrer);
|
||||
var query = uri.Query;
|
||||
|
||||
if (query.Contains("utm_campaign="))
|
||||
{
|
||||
var match = System.Text.RegularExpressions.Regex.Match(query, @"utm_campaign=([^&]+)");
|
||||
if (match.Success)
|
||||
return Uri.UnescapeDataString(match.Groups[1].Value);
|
||||
}
|
||||
|
||||
return "organic";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsValidEmail(string email)
|
||||
{
|
||||
try
|
||||
{
|
||||
var addr = new System.Net.Mail.MailAddress(email);
|
||||
return addr.Address == email;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private (DateTime from, DateTime to) GetDateRangeFromPeriod(string period)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
return period.ToLowerInvariant() switch
|
||||
{
|
||||
"today" => (now.Date, now),
|
||||
"yesterday" => (now.Date.AddDays(-1), now.Date.AddSeconds(-1)),
|
||||
"last7days" => (now.AddDays(-7), now),
|
||||
"last30days" => (now.AddDays(-30), now),
|
||||
"thismonth" => (new DateTime(now.Year, now.Month, 1), now),
|
||||
"lastmonth" => (new DateTime(now.Year, now.Month, 1).AddMonths(-1),
|
||||
new DateTime(now.Year, now.Month, 1).AddSeconds(-1)),
|
||||
_ => (now.AddDays(-7), now) // default to last 7 days
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Modelos para MongoDB
|
||||
public class ConversionRecord
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Language { get; set; } = string.Empty;
|
||||
public string PageName { get; set; } = string.Empty;
|
||||
public Dictionary<string, string> FormData { get; set; } = new();
|
||||
public string UserAgent { get; set; } = string.Empty;
|
||||
public string IpAddress { get; set; } = string.Empty;
|
||||
public string? Referrer { get; set; }
|
||||
public DateTime ConvertedAt { get; set; }
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
public string Source { get; set; } = "direct";
|
||||
public string Medium { get; set; } = "direct";
|
||||
public string Campaign { get; set; } = "direct";
|
||||
}
|
||||
|
||||
public class ConversionEvent
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string EventName { get; set; } = string.Empty;
|
||||
public Dictionary<string, object> Properties { get; set; } = new();
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
}
|
||||
218
OnlyOneAccessTemplate/Services/Interfaces.cs
Normal file
218
OnlyOneAccessTemplate/Services/Interfaces.cs
Normal file
@ -0,0 +1,218 @@
|
||||
using OnlyOneAccessTemplate.Models;
|
||||
|
||||
namespace OnlyOneAccessTemplate.Services
|
||||
{
|
||||
public interface ISiteConfigurationService
|
||||
{
|
||||
Task<SiteConfiguration> GetConfigurationAsync(string language);
|
||||
SiteConfiguration GetConfiguration(string language); // Versão síncrona para o BaseController
|
||||
Task<PageContent?> GetPageContentAsync(string language, string pageName);
|
||||
Task<string> GenerateSitemapAsync();
|
||||
Task<string> GenerateRobotsAsync();
|
||||
Task UpdateConfigurationAsync(SiteConfiguration config);
|
||||
Task<List<string>> GetAvailableLanguagesAsync();
|
||||
Task<bool> ConfigurationExistsAsync(string language);
|
||||
Task DeleteConfigurationAsync(string language);
|
||||
}
|
||||
|
||||
public interface ILanguageService
|
||||
{
|
||||
string GetCurrentLanguage(HttpContext context);
|
||||
string GetLanguageFromPath(string path);
|
||||
List<string> GetSupportedLanguages();
|
||||
string GetDefaultLanguage();
|
||||
string GetLanguageDisplayName(string languageCode);
|
||||
Dictionary<string, string> GetHreflangUrls(string currentUrl, string currentLanguage);
|
||||
string GetCultureInfo(string languageCode);
|
||||
void SetLanguageCookie(HttpContext context, string languageCode);
|
||||
string GenerateLanguageSwitcher(HttpContext context, string currentLanguage);
|
||||
bool IsRtlLanguage(string languageCode);
|
||||
string GetLanguageDirection(string languageCode);
|
||||
bool IsValidLanguage(string languageCode);
|
||||
string NormalizeLanguageCode(string languageCode);
|
||||
}
|
||||
|
||||
public interface IConversionService
|
||||
{
|
||||
Task<bool> ProcessConversionAsync(ConversionData data);
|
||||
Task<ConversionStats> GetConversionStatsAsync(string language, DateTime from, DateTime to);
|
||||
Task LogConversionEventAsync(string eventName, Dictionary<string, object> properties);
|
||||
Task<ConversionRecord?> GetConversionByIdAsync(string conversionId);
|
||||
Task<List<ConversionRecord>> GetConversionsAsync(string language, int skip = 0, int take = 50);
|
||||
Task<bool> ValidateFormDataAsync(Dictionary<string, string> formData, List<FormField> formFields);
|
||||
Task<ConversionStats> GetRealTimeStatsAsync(string language);
|
||||
Task<Dictionary<string, object>> GetConversionMetricsAsync(string language, string period);
|
||||
Task<bool> SendConversionNotificationAsync(ConversionRecord conversion);
|
||||
Task<List<ConversionTrend>> GetConversionTrendsAsync(string language, DateTime from, DateTime to, string groupBy = "day");
|
||||
}
|
||||
public interface ISeoService
|
||||
{
|
||||
Task<SeoMetadata> GenerateSeoMetadataAsync(string language, string pageName, PageContent? content = null);
|
||||
Task<string> GenerateStructuredDataAsync(string language, string pageType, object data);
|
||||
Task<List<SeoIssue>> ValidateSeoAsync(string url);
|
||||
Task<SeoMetadata> GenerateMetadataForPageAsync(HttpContext context, string language, string pageName, PageContent? customContent = null);
|
||||
Task<string> GenerateCanonicalUrlAsync(HttpContext context, string language, string pageName);
|
||||
Task<Dictionary<string, string>> GenerateMetaTagsAsync(SeoMetadata metadata);
|
||||
Task<string> GenerateOpenGraphTagsAsync(SeoMetadata metadata);
|
||||
Task<string> GenerateTwitterCardTagsAsync(SeoMetadata metadata);
|
||||
Task<string> GenerateJsonLdAsync(string language, string pageType, object data);
|
||||
bool ValidateMetadata(SeoMetadata metadata, out List<SeoIssue> issues);
|
||||
}
|
||||
|
||||
// Interfaces adicionais
|
||||
public interface ICacheService
|
||||
{
|
||||
Task<T?> GetAsync<T>(string key) where T : class;
|
||||
Task SetAsync<T>(string key, T value, TimeSpan? expiration = null) where T : class;
|
||||
Task RemoveAsync(string key);
|
||||
Task RemoveByPatternAsync(string pattern);
|
||||
Task<bool> ExistsAsync(string key);
|
||||
}
|
||||
|
||||
public interface IAnalyticsService
|
||||
{
|
||||
Task TrackPageViewAsync(string language, string pageName, string userAgent, string ipAddress, string? referrer = null);
|
||||
Task TrackEventAsync(string eventName, string language, Dictionary<string, object> properties);
|
||||
Task<AnalyticsData> GetAnalyticsAsync(string language, DateTime from, DateTime to);
|
||||
Task<List<PopularPage>> GetPopularPagesAsync(string language, int limit = 10);
|
||||
Task<Dictionary<string, int>> GetTrafficSourcesAsync(string language, DateTime from, DateTime to);
|
||||
}
|
||||
|
||||
public interface IEmailService
|
||||
{
|
||||
Task<bool> SendConversionNotificationAsync(ConversionRecord conversion, string toEmail);
|
||||
Task<bool> SendWelcomeEmailAsync(string toEmail, string name, string language);
|
||||
Task<bool> SendBulkEmailAsync(List<string> emails, string subject, string content, string language);
|
||||
Task<EmailTemplate?> GetEmailTemplateAsync(string templateName, string language);
|
||||
}
|
||||
|
||||
public interface IValidationService
|
||||
{
|
||||
ValidationResult ValidateFormData(Dictionary<string, string> formData, List<FormField> formFields);
|
||||
bool ValidateEmail(string email);
|
||||
bool ValidatePhone(string phone, string? countryCode = null);
|
||||
bool ValidateRequired(string value);
|
||||
bool ValidateRegex(string value, string pattern);
|
||||
ValidationResult ValidateConversionData(ConversionData data);
|
||||
}
|
||||
|
||||
public interface IFileService
|
||||
{
|
||||
Task<string> UploadImageAsync(IFormFile file, string folder = "uploads");
|
||||
Task<bool> DeleteFileAsync(string filePath);
|
||||
Task<string> ResizeImageAsync(string filePath, int width, int height);
|
||||
Task<List<string>> GetImagesAsync(string folder = "uploads");
|
||||
bool IsValidImageFile(IFormFile file);
|
||||
}
|
||||
// DTOs e Records
|
||||
public record ConversionData(
|
||||
string Language,
|
||||
string PageName,
|
||||
Dictionary<string, string> FormData,
|
||||
string UserAgent,
|
||||
string IpAddress,
|
||||
string? Referrer = null,
|
||||
string? SessionId = null,
|
||||
Dictionary<string, string>? UtmParameters = null
|
||||
);
|
||||
|
||||
public record ConversionStats(
|
||||
int TotalVisits,
|
||||
int TotalConversions,
|
||||
double ConversionRate,
|
||||
Dictionary<string, int> ConversionsBySource,
|
||||
Dictionary<string, int>? ConversionsByMedium = null,
|
||||
Dictionary<string, int>? ConversionsByPage = null,
|
||||
DateTime? LastUpdated = null
|
||||
);
|
||||
|
||||
public record ConversionTrend(
|
||||
DateTime Date,
|
||||
int Conversions,
|
||||
int Visits,
|
||||
double ConversionRate
|
||||
);
|
||||
|
||||
public record SeoMetadata(
|
||||
string Title,
|
||||
string Description,
|
||||
string Keywords,
|
||||
string OgTitle,
|
||||
string OgDescription,
|
||||
string OgImage,
|
||||
string CanonicalUrl,
|
||||
Dictionary<string, string> HreflangUrls,
|
||||
string StructuredData,
|
||||
string? OgType = "website",
|
||||
string? TwitterCard = "summary_large_image",
|
||||
string? Author = null,
|
||||
DateTime? LastModified = null
|
||||
);
|
||||
|
||||
public record SeoIssue(
|
||||
string Type,
|
||||
string Message,
|
||||
string Severity,
|
||||
string Element,
|
||||
string? Recommendation = null,
|
||||
string? HelpUrl = null
|
||||
);
|
||||
|
||||
public record AnalyticsData(
|
||||
int TotalPageViews,
|
||||
int UniqueVisitors,
|
||||
Dictionary<string, int> PageViews,
|
||||
Dictionary<string, int> TrafficSources,
|
||||
double AverageSessionDuration,
|
||||
double BounceRate
|
||||
);
|
||||
|
||||
public record PopularPage(
|
||||
string PageName,
|
||||
string Title,
|
||||
int Views,
|
||||
double ConversionRate
|
||||
);
|
||||
|
||||
public record EmailTemplate(
|
||||
string Name,
|
||||
string Subject,
|
||||
string HtmlContent,
|
||||
string TextContent,
|
||||
string Language
|
||||
);
|
||||
|
||||
public record ValidationResult(
|
||||
bool IsValid,
|
||||
List<string> Errors,
|
||||
Dictionary<string, string>? FieldErrors = null
|
||||
);
|
||||
|
||||
// Enums
|
||||
public enum SeoIssueSeverity
|
||||
{
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Critical
|
||||
}
|
||||
|
||||
public enum ConversionEventType
|
||||
{
|
||||
PageView,
|
||||
FormStart,
|
||||
FormSubmit,
|
||||
ConversionComplete,
|
||||
DownloadStart,
|
||||
VideoPlay,
|
||||
EmailClick,
|
||||
PhoneClick
|
||||
}
|
||||
|
||||
public enum CacheType
|
||||
{
|
||||
Memory,
|
||||
Distributed,
|
||||
Database
|
||||
}
|
||||
}
|
||||
261
OnlyOneAccessTemplate/Services/LanguageService.cs
Normal file
261
OnlyOneAccessTemplate/Services/LanguageService.cs
Normal file
@ -0,0 +1,261 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using System.Globalization;
|
||||
|
||||
namespace OnlyOneAccessTemplate.Services
|
||||
{
|
||||
public class LanguageService : ILanguageService
|
||||
{
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly ILogger<LanguageService> _logger;
|
||||
|
||||
private static readonly Dictionary<string, string> SupportedLanguages = new()
|
||||
{
|
||||
{ "pt", "Português" },
|
||||
{ "en", "English" },
|
||||
{ "es", "Español" }
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, string> LanguageCultures = new()
|
||||
{
|
||||
{ "pt", "pt-BR" },
|
||||
{ "en", "en-US" },
|
||||
{ "es", "es-ES" }
|
||||
};
|
||||
|
||||
public LanguageService(IMemoryCache cache, ILogger<LanguageService> logger)
|
||||
{
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string GetCurrentLanguage(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. Verificar na URL
|
||||
var pathLanguage = GetLanguageFromPath(context.Request.Path);
|
||||
if (!string.IsNullOrEmpty(pathLanguage))
|
||||
{
|
||||
return pathLanguage;
|
||||
}
|
||||
|
||||
// 2. Verificar cookie de preferência
|
||||
if (context.Request.Cookies.TryGetValue("preferred_language", out var cookieLanguage) &&
|
||||
SupportedLanguages.ContainsKey(cookieLanguage))
|
||||
{
|
||||
return cookieLanguage;
|
||||
}
|
||||
|
||||
// 3. Verificar Accept-Language header
|
||||
var browserLanguage = GetLanguageFromBrowser(context);
|
||||
if (!string.IsNullOrEmpty(browserLanguage))
|
||||
{
|
||||
return browserLanguage;
|
||||
}
|
||||
|
||||
// 4. Retornar idioma padrão
|
||||
return GetDefaultLanguage();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao determinar idioma atual");
|
||||
return GetDefaultLanguage();
|
||||
}
|
||||
}
|
||||
|
||||
public string GetLanguageFromPath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path) || path == "/")
|
||||
return string.Empty;
|
||||
|
||||
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (segments.Length == 0)
|
||||
return string.Empty;
|
||||
|
||||
var firstSegment = segments[0].ToLowerInvariant();
|
||||
|
||||
return SupportedLanguages.ContainsKey(firstSegment) ? firstSegment : string.Empty;
|
||||
}
|
||||
|
||||
public List<string> GetSupportedLanguages()
|
||||
{
|
||||
return SupportedLanguages.Keys.ToList();
|
||||
}
|
||||
|
||||
public string GetDefaultLanguage()
|
||||
{
|
||||
return "pt";
|
||||
}
|
||||
|
||||
public string GetLanguageDisplayName(string languageCode)
|
||||
{
|
||||
return SupportedLanguages.TryGetValue(languageCode, out var displayName)
|
||||
? displayName
|
||||
: languageCode;
|
||||
}
|
||||
|
||||
public Dictionary<string, string> GetHreflangUrls(string currentUrl, string currentLanguage)
|
||||
{
|
||||
var cacheKey = $"hreflang_{currentUrl}_{currentLanguage}";
|
||||
|
||||
if (_cache.TryGetValue(cacheKey, out Dictionary<string, string>? cachedUrls) && cachedUrls != null)
|
||||
{
|
||||
return cachedUrls;
|
||||
}
|
||||
|
||||
var hreflangUrls = new Dictionary<string, string>();
|
||||
|
||||
try
|
||||
{
|
||||
var baseUrl = RemoveLanguageFromUrl(currentUrl, currentLanguage);
|
||||
|
||||
foreach (var language in SupportedLanguages.Keys)
|
||||
{
|
||||
var url = language == GetDefaultLanguage()
|
||||
? baseUrl
|
||||
: $"{baseUrl.TrimEnd('/')}/{language}";
|
||||
|
||||
hreflangUrls[language] = url;
|
||||
}
|
||||
|
||||
// Adicionar x-default para o idioma padrão
|
||||
hreflangUrls["x-default"] = hreflangUrls[GetDefaultLanguage()];
|
||||
|
||||
_cache.Set(cacheKey, hreflangUrls, TimeSpan.FromMinutes(15));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao gerar URLs hreflang para {CurrentUrl}", currentUrl);
|
||||
}
|
||||
|
||||
return hreflangUrls;
|
||||
}
|
||||
|
||||
public string GetCultureInfo(string languageCode)
|
||||
{
|
||||
return LanguageCultures.TryGetValue(languageCode, out var culture) ? culture : "pt-BR";
|
||||
}
|
||||
|
||||
public void SetLanguageCookie(HttpContext context, string languageCode)
|
||||
{
|
||||
if (!SupportedLanguages.ContainsKey(languageCode))
|
||||
return;
|
||||
|
||||
var cookieOptions = new CookieOptions
|
||||
{
|
||||
Expires = DateTimeOffset.UtcNow.AddYears(1),
|
||||
HttpOnly = false,
|
||||
Secure = context.Request.IsHttps,
|
||||
SameSite = SameSiteMode.Lax
|
||||
};
|
||||
|
||||
context.Response.Cookies.Append("preferred_language", languageCode, cookieOptions);
|
||||
}
|
||||
|
||||
public string GenerateLanguageSwitcher(HttpContext context, string currentLanguage)
|
||||
{
|
||||
var currentUrl = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}";
|
||||
var hreflangUrls = GetHreflangUrls(currentUrl, currentLanguage);
|
||||
|
||||
var switcher = new List<string>();
|
||||
|
||||
foreach (var language in SupportedLanguages)
|
||||
{
|
||||
var isActive = language.Key == currentLanguage;
|
||||
var cssClass = isActive ? "active" : "";
|
||||
var url = hreflangUrls.TryGetValue(language.Key, out var langUrl) ? langUrl : "/";
|
||||
|
||||
switcher.Add($"<a href=\"{url}\" class=\"language-link {cssClass}\" hreflang=\"{language.Key}\">{language.Value}</a>");
|
||||
}
|
||||
|
||||
return string.Join("", switcher);
|
||||
}
|
||||
|
||||
public bool IsRtlLanguage(string languageCode)
|
||||
{
|
||||
// Lista de idiomas RTL (Right-to-Left)
|
||||
var rtlLanguages = new[] { "ar", "he", "fa", "ur" };
|
||||
return rtlLanguages.Contains(languageCode);
|
||||
}
|
||||
|
||||
public string GetLanguageDirection(string languageCode)
|
||||
{
|
||||
return IsRtlLanguage(languageCode) ? "rtl" : "ltr";
|
||||
}
|
||||
|
||||
public bool IsValidLanguage(string languageCode)
|
||||
{
|
||||
return !string.IsNullOrEmpty(languageCode) && SupportedLanguages.ContainsKey(languageCode.ToLowerInvariant());
|
||||
}
|
||||
|
||||
public string NormalizeLanguageCode(string languageCode)
|
||||
{
|
||||
if (string.IsNullOrEmpty(languageCode))
|
||||
return GetDefaultLanguage();
|
||||
|
||||
var normalized = languageCode.ToLowerInvariant().Trim();
|
||||
|
||||
// Handle common variations
|
||||
normalized = normalized switch
|
||||
{
|
||||
"pt-br" or "pt_br" or "portuguese" => "pt",
|
||||
"en-us" or "en_us" or "english" => "en",
|
||||
"es-es" or "es_es" or "spanish" or "español" => "es",
|
||||
_ => normalized
|
||||
};
|
||||
|
||||
return IsValidLanguage(normalized) ? normalized : GetDefaultLanguage();
|
||||
}
|
||||
|
||||
private string GetLanguageFromBrowser(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
var acceptLanguage = context.Request.Headers.AcceptLanguage.ToString();
|
||||
if (string.IsNullOrEmpty(acceptLanguage))
|
||||
return string.Empty;
|
||||
|
||||
// Parse Accept-Language header
|
||||
var languages = acceptLanguage
|
||||
.Split(',')
|
||||
.Select(lang => lang.Split(';')[0].Trim())
|
||||
.Select(lang => lang.Length >= 2 ? lang.Substring(0, 2).ToLowerInvariant() : lang)
|
||||
.Where(lang => SupportedLanguages.ContainsKey(lang));
|
||||
|
||||
return languages.FirstOrDefault() ?? string.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao analisar Accept-Language header");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private string RemoveLanguageFromUrl(string url, string currentLanguage)
|
||||
{
|
||||
try
|
||||
{
|
||||
var uri = new Uri(url);
|
||||
var path = uri.AbsolutePath;
|
||||
|
||||
if (currentLanguage != GetDefaultLanguage())
|
||||
{
|
||||
var languagePrefix = $"/{currentLanguage}";
|
||||
if (path.StartsWith(languagePrefix))
|
||||
{
|
||||
path = path.Substring(languagePrefix.Length);
|
||||
if (string.IsNullOrEmpty(path))
|
||||
path = "/";
|
||||
}
|
||||
}
|
||||
|
||||
return $"{uri.Scheme}://{uri.Host}{(uri.Port != 80 && uri.Port != 443 ? $":{uri.Port}" : "")}{path}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao remover idioma da URL {Url}", url);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
373
OnlyOneAccessTemplate/Services/SeoService.cs
Normal file
373
OnlyOneAccessTemplate/Services/SeoService.cs
Normal file
@ -0,0 +1,373 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Linq;
|
||||
using OnlyOneAccessTemplate.Models;
|
||||
using global::OnlyOneAccessTemplate.Models;
|
||||
|
||||
namespace OnlyOneAccessTemplate.Services
|
||||
{
|
||||
|
||||
public class SeoService : ISeoService
|
||||
{
|
||||
private readonly ISiteConfigurationService _siteConfig;
|
||||
private readonly ILanguageService _languageService;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<SeoService> _logger;
|
||||
|
||||
public SeoService(
|
||||
ISiteConfigurationService siteConfig,
|
||||
ILanguageService languageService,
|
||||
IMemoryCache cache,
|
||||
IConfiguration configuration,
|
||||
ILogger<SeoService> logger)
|
||||
{
|
||||
_siteConfig = siteConfig;
|
||||
_languageService = languageService;
|
||||
_cache = cache;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<SeoMetadata> GenerateSeoMetadataAsync(string language, string pageName, PageContent? content = null)
|
||||
{
|
||||
var cacheKey = $"seo_metadata_{language}_{pageName}";
|
||||
|
||||
if (_cache.TryGetValue(cacheKey, out SeoMetadata? cachedMetadata) && cachedMetadata != null)
|
||||
{
|
||||
return cachedMetadata;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var configuration = await _siteConfig.GetConfigurationAsync(language);
|
||||
content = content ?? await _siteConfig.GetPageContentAsync(language, pageName);
|
||||
|
||||
var baseUrl = _configuration.GetValue<string>("SEO:DefaultDomain") ?? "https://localhost";
|
||||
var canonicalUrl = BuildCanonicalUrl(baseUrl, language, pageName);
|
||||
var hreflangUrls = _languageService.GetHreflangUrls(canonicalUrl, language);
|
||||
|
||||
var metadata = new SeoMetadata(
|
||||
Title: content?.MetaTitle ?? content?.Title ?? configuration.Seo.DefaultTitle,
|
||||
Description: content?.Description ?? configuration.Seo.DefaultDescription,
|
||||
Keywords: content?.Keywords ?? configuration.Seo.DefaultKeywords,
|
||||
OgTitle: content?.Title ?? configuration.Seo.DefaultTitle,
|
||||
OgDescription: content?.Description ?? configuration.Seo.DefaultDescription,
|
||||
OgImage: content?.OgImage ?? configuration.Seo.OgImage,
|
||||
CanonicalUrl: canonicalUrl,
|
||||
HreflangUrls: hreflangUrls,
|
||||
StructuredData: await GenerateStructuredDataAsync(language, "WebPage", new { content, configuration })
|
||||
);
|
||||
|
||||
_cache.Set(cacheKey, metadata, TimeSpan.FromMinutes(15));
|
||||
return metadata;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao gerar metadata SEO para {Language}/{PageName}", language, pageName);
|
||||
return CreateFallbackMetadata(language, pageName);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GenerateStructuredDataAsync(string language, string pageType, object data)
|
||||
{
|
||||
try
|
||||
{
|
||||
var configuration = await _siteConfig.GetConfigurationAsync(language);
|
||||
var baseUrl = _configuration.GetValue<string>("SEO:DefaultDomain") ?? "https://localhost";
|
||||
|
||||
var structuredData = pageType.ToLowerInvariant() switch
|
||||
{
|
||||
"webpage" => GenerateWebPageStructuredData(configuration, baseUrl, language, data),
|
||||
"organization" => GenerateOrganizationStructuredData(configuration, baseUrl),
|
||||
"breadcrumb" => GenerateBreadcrumbStructuredData(baseUrl, language, data),
|
||||
"faq" => GenerateFaqStructuredData(data),
|
||||
"product" => GenerateProductStructuredData(configuration, data),
|
||||
_ => GenerateWebPageStructuredData(configuration, baseUrl, language, data)
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(structuredData, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao gerar structured data para {PageType}", pageType);
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<SeoIssue>> ValidateSeoAsync(string url)
|
||||
{
|
||||
var issues = new List<SeoIssue>();
|
||||
|
||||
try
|
||||
{
|
||||
// Esta implementação seria expandida com validações reais
|
||||
// Por enquanto, retorna uma lista básica de verificações
|
||||
|
||||
if (string.IsNullOrEmpty(url))
|
||||
{
|
||||
issues.Add(new SeoIssue("URL", "URL não fornecida", "High", ""));
|
||||
}
|
||||
|
||||
// Adicionar mais validações conforme necessário
|
||||
// - Verificar se title tem tamanho adequado
|
||||
// - Verificar se description tem tamanho adequado
|
||||
// - Verificar se há headings estruturados
|
||||
// - Verificar se há alt text nas imagens
|
||||
// - Verificar velocidade da página
|
||||
// - Verificar responsividade
|
||||
|
||||
return issues;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao validar SEO para URL {Url}", url);
|
||||
issues.Add(new SeoIssue("Validation", "Erro na validação SEO", "High", ""));
|
||||
return issues;
|
||||
}
|
||||
}
|
||||
|
||||
private object GenerateWebPageStructuredData(SiteConfiguration config, string baseUrl, string language, object data)
|
||||
{
|
||||
return new
|
||||
{
|
||||
Context = "https://schema.org",
|
||||
Type = "WebPage",
|
||||
Name = config.Seo.SiteName,
|
||||
Description = config.Seo.DefaultDescription,
|
||||
Url = baseUrl,
|
||||
InLanguage = language,
|
||||
IsPartOf = new
|
||||
{
|
||||
Type = "WebSite",
|
||||
Name = config.Seo.SiteName,
|
||||
Url = baseUrl
|
||||
},
|
||||
DatePublished = DateTime.UtcNow.ToString("yyyy-MM-dd"),
|
||||
DateModified = DateTime.UtcNow.ToString("yyyy-MM-dd")
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<SeoMetadata> GenerateMetadataForPageAsync(HttpContext context, string language, string pageName, PageContent? customContent = null)
|
||||
{
|
||||
var currentUrl = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}";
|
||||
return await GenerateSeoMetadataAsync(language, pageName, customContent);
|
||||
}
|
||||
|
||||
public async Task<string> GenerateCanonicalUrlAsync(HttpContext context, string language, string pageName)
|
||||
{
|
||||
var baseUrl = $"{context.Request.Scheme}://{context.Request.Host}";
|
||||
return BuildCanonicalUrl(baseUrl, language, pageName);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, string>> GenerateMetaTagsAsync(SeoMetadata metadata)
|
||||
{
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
{ "title", metadata.Title },
|
||||
{ "description", metadata.Description },
|
||||
{ "keywords", metadata.Keywords },
|
||||
{ "canonical", metadata.CanonicalUrl },
|
||||
{ "author", metadata.Author ?? "" }
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<string> GenerateOpenGraphTagsAsync(SeoMetadata metadata)
|
||||
{
|
||||
var tags = new StringBuilder();
|
||||
tags.AppendLine($"<meta property=\"og:title\" content=\"{metadata.OgTitle}\" />");
|
||||
tags.AppendLine($"<meta property=\"og:description\" content=\"{metadata.OgDescription}\" />");
|
||||
tags.AppendLine($"<meta property=\"og:image\" content=\"{metadata.OgImage}\" />");
|
||||
tags.AppendLine($"<meta property=\"og:url\" content=\"{metadata.CanonicalUrl}\" />");
|
||||
tags.AppendLine($"<meta property=\"og:type\" content=\"{metadata.OgType}\" />");
|
||||
return tags.ToString();
|
||||
}
|
||||
|
||||
public async Task<string> GenerateTwitterCardTagsAsync(SeoMetadata metadata)
|
||||
{
|
||||
var tags = new StringBuilder();
|
||||
tags.AppendLine($"<meta name=\"twitter:card\" content=\"{metadata.TwitterCard}\" />");
|
||||
tags.AppendLine($"<meta name=\"twitter:title\" content=\"{metadata.OgTitle}\" />");
|
||||
tags.AppendLine($"<meta name=\"twitter:description\" content=\"{metadata.OgDescription}\" />");
|
||||
tags.AppendLine($"<meta name=\"twitter:image\" content=\"{metadata.OgImage}\" />");
|
||||
return tags.ToString();
|
||||
}
|
||||
|
||||
public async Task<string> GenerateJsonLdAsync(string language, string pageType, object data)
|
||||
{
|
||||
return await GenerateStructuredDataAsync(language, pageType, data);
|
||||
}
|
||||
|
||||
public bool ValidateMetadata(SeoMetadata metadata, out List<SeoIssue> issues)
|
||||
{
|
||||
issues = new List<SeoIssue>();
|
||||
|
||||
if (string.IsNullOrEmpty(metadata.Title))
|
||||
{
|
||||
issues.Add(new SeoIssue("Title", "Título não pode estar vazio", "High", "title"));
|
||||
}
|
||||
else if (metadata.Title.Length > 60)
|
||||
{
|
||||
issues.Add(new SeoIssue("Title", "Título muito longo (máx. 60 caracteres)", "Medium", "title"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(metadata.Description))
|
||||
{
|
||||
issues.Add(new SeoIssue("Description", "Descrição não pode estar vazia", "High", "meta[name='description']"));
|
||||
}
|
||||
else if (metadata.Description.Length > 160)
|
||||
{
|
||||
issues.Add(new SeoIssue("Description", "Descrição muito longa (máx. 160 caracteres)", "Medium", "meta[name='description']"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(metadata.OgImage))
|
||||
{
|
||||
issues.Add(new SeoIssue("OpenGraph", "Imagem Open Graph não definida", "Medium", "meta[property='og:image']"));
|
||||
}
|
||||
|
||||
return !issues.Any(i => i.Severity == "High");
|
||||
}
|
||||
|
||||
private object GenerateOrganizationStructuredData(SiteConfiguration config, string baseUrl)
|
||||
{
|
||||
return new
|
||||
{
|
||||
Context = "https://schema.org",
|
||||
Type = "Organization",
|
||||
Name = config.Seo.SiteName,
|
||||
Url = baseUrl,
|
||||
Logo = new
|
||||
{
|
||||
Type = "ImageObject",
|
||||
Url = $"{baseUrl}/logo.png"
|
||||
},
|
||||
SameAs = new[]
|
||||
{
|
||||
"Facebook"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private object GenerateBreadcrumbStructuredData(string baseUrl, string language, object data)
|
||||
{
|
||||
return new
|
||||
{
|
||||
Context = "https://schema.org",
|
||||
Type = "BreadcrumbList",
|
||||
ItemListElement = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
Type = "ListItem",
|
||||
Position = 1,
|
||||
Name = language switch
|
||||
{
|
||||
"en" => "Home",
|
||||
"es" => "Inicio",
|
||||
_ => "Início"
|
||||
},
|
||||
Item = baseUrl
|
||||
}
|
||||
// Adicionar mais items do breadcrumb conforme necessário
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private object GenerateFaqStructuredData(object data)
|
||||
{
|
||||
return new
|
||||
{
|
||||
Context = "https://schema.org",
|
||||
Type = "FAQPage",
|
||||
MainEntity = new[]
|
||||
{
|
||||
// Exemplo de FAQ - expandir conforme necessário
|
||||
new
|
||||
{
|
||||
Type = "Question",
|
||||
Name = "Como funciona?",
|
||||
AcceptedAnswer = new
|
||||
{
|
||||
Type = "Answer",
|
||||
Text = "Explicação de como funciona..."
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private object GenerateProductStructuredData(SiteConfiguration config, object data)
|
||||
{
|
||||
return new
|
||||
{
|
||||
Context = "https://schema.org",
|
||||
Type = "Product",
|
||||
Name = "Nome do Produto",
|
||||
Description = "Descrição do produto",
|
||||
Brand = new
|
||||
{
|
||||
Type = "Brand",
|
||||
Name = config.Seo.SiteName
|
||||
},
|
||||
Offers = new
|
||||
{
|
||||
Type = "Offer",
|
||||
PriceCurrency = "BRL",
|
||||
Price = "0",
|
||||
Availability = "https://schema.org/InStock"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private string BuildCanonicalUrl(string baseUrl, string language, string pageName)
|
||||
{
|
||||
var url = baseUrl.TrimEnd('/');
|
||||
|
||||
if (language != _languageService.GetDefaultLanguage())
|
||||
{
|
||||
url += $"/{language}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(pageName) && pageName != "index")
|
||||
{
|
||||
url += $"/{pageName}";
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
private SeoMetadata CreateFallbackMetadata(string language, string pageName)
|
||||
{
|
||||
var defaultTitle = language switch
|
||||
{
|
||||
"en" => "Page Not Found",
|
||||
"es" => "Página No Encontrada",
|
||||
_ => "Página Não Encontrada"
|
||||
};
|
||||
|
||||
var defaultDescription = language switch
|
||||
{
|
||||
"en" => "The requested page could not be found.",
|
||||
"es" => "La página solicitada no pudo ser encontrada.",
|
||||
_ => "A página solicitada não pôde ser encontrada."
|
||||
};
|
||||
|
||||
return new SeoMetadata(
|
||||
Title: defaultTitle,
|
||||
Description: defaultDescription,
|
||||
Keywords: "",
|
||||
OgTitle: defaultTitle,
|
||||
OgDescription: defaultDescription,
|
||||
OgImage: "",
|
||||
CanonicalUrl: "",
|
||||
HreflangUrls: new Dictionary<string, string>(),
|
||||
StructuredData: "{}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
443
OnlyOneAccessTemplate/Services/SiteConfigurationService .cs
Normal file
443
OnlyOneAccessTemplate/Services/SiteConfigurationService .cs
Normal file
@ -0,0 +1,443 @@
|
||||
using MongoDB.Driver;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using System.Text;
|
||||
using OnlyOneAccessTemplate.Models;
|
||||
|
||||
namespace OnlyOneAccessTemplate.Services
|
||||
{
|
||||
public class SiteConfigurationService : ISiteConfigurationService
|
||||
{
|
||||
private readonly IMongoDatabase _database;
|
||||
private readonly IMongoCollection<SiteConfiguration> _configurations;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly ILogger<SiteConfigurationService> _logger;
|
||||
private readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(30);
|
||||
|
||||
public SiteConfigurationService(
|
||||
IMongoDatabase database,
|
||||
IMemoryCache cache,
|
||||
IConfiguration config,
|
||||
ILogger<SiteConfigurationService> logger)
|
||||
{
|
||||
_database = database;
|
||||
_cache = cache;
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
_configurations = _database.GetCollection<SiteConfiguration>("site_configurations");
|
||||
}
|
||||
|
||||
public async Task<SiteConfiguration> GetConfigurationAsync(string language)
|
||||
{
|
||||
var cacheKey = $"site_config_{language}";
|
||||
|
||||
if (_cache.TryGetValue(cacheKey, out SiteConfiguration? cachedConfig) && cachedConfig != null)
|
||||
{
|
||||
return cachedConfig;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var filter = Builders<SiteConfiguration>.Filter.Eq(x => x.Language, language);
|
||||
var configuration = await _configurations.Find(filter).FirstOrDefaultAsync();
|
||||
|
||||
if (configuration == null)
|
||||
{
|
||||
configuration = await CreateDefaultConfigurationAsync(language);
|
||||
}
|
||||
|
||||
_cache.Set(cacheKey, configuration, _cacheExpiration);
|
||||
return configuration;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao buscar configuração para idioma {Language}", language);
|
||||
return await CreateDefaultConfigurationAsync(language);
|
||||
}
|
||||
}
|
||||
|
||||
public SiteConfiguration GetConfiguration(string language)
|
||||
{
|
||||
var cacheKey = $"site_config_{language}";
|
||||
|
||||
if (_cache.TryGetValue(cacheKey, out SiteConfiguration? cachedConfig) && cachedConfig != null)
|
||||
{
|
||||
return cachedConfig;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var filter = Builders<SiteConfiguration>.Filter.Eq(x => x.Language, language);
|
||||
var configuration = _configurations.Find(filter).FirstOrDefault();
|
||||
|
||||
if (configuration == null)
|
||||
{
|
||||
configuration = CreateDefaultConfiguration(language);
|
||||
}
|
||||
|
||||
_cache.Set(cacheKey, configuration, _cacheExpiration);
|
||||
return configuration;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao buscar configuração para idioma {Language}", language);
|
||||
return CreateDefaultConfiguration(language);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PageContent?> GetPageContentAsync(string language, string pageName)
|
||||
{
|
||||
var configuration = await GetConfigurationAsync(language);
|
||||
|
||||
if (configuration.Pages.TryGetValue(pageName.ToLowerInvariant(), out var pageContent))
|
||||
{
|
||||
return pageContent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<string> GenerateSitemapAsync()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||
sb.AppendLine("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">");
|
||||
|
||||
var baseUrl = _config.GetValue<string>("SEO:DefaultDomain") ?? "https://localhost";
|
||||
var languages = await GetAvailableLanguagesAsync();
|
||||
|
||||
// Páginas principais para cada idioma
|
||||
var mainPages = new[] { "", "about", "contact", "privacy", "terms" };
|
||||
|
||||
foreach (var page in mainPages)
|
||||
{
|
||||
foreach (var lang in languages)
|
||||
{
|
||||
var url = BuildUrl(baseUrl, lang, page);
|
||||
var lastMod = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
||||
var priority = page == "" ? "1.0" : "0.8";
|
||||
|
||||
sb.AppendLine(" <url>");
|
||||
sb.AppendLine($" <loc>{url}</loc>");
|
||||
sb.AppendLine($" <lastmod>{lastMod}</lastmod>");
|
||||
sb.AppendLine($" <changefreq>weekly</changefreq>");
|
||||
sb.AppendLine($" <priority>{priority}</priority>");
|
||||
|
||||
// Adicionar links alternativos (hreflang)
|
||||
foreach (var altLang in languages)
|
||||
{
|
||||
var altUrl = BuildUrl(baseUrl, altLang, page);
|
||||
sb.AppendLine($" <xhtml:link rel=\"alternate\" hreflang=\"{altLang}\" href=\"{altUrl}\" />");
|
||||
}
|
||||
|
||||
sb.AppendLine(" </url>");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("</urlset>");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public async Task<string> GenerateRobotsAsync()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var baseUrl = _config.GetValue<string>("SEO:DefaultDomain") ?? "https://localhost";
|
||||
|
||||
sb.AppendLine("User-agent: *");
|
||||
sb.AppendLine("Allow: /");
|
||||
sb.AppendLine("");
|
||||
sb.AppendLine("# Sitemaps");
|
||||
sb.AppendLine($"Sitemap: {baseUrl}/sitemap.xml");
|
||||
sb.AppendLine("");
|
||||
sb.AppendLine("# Crawl delay");
|
||||
sb.AppendLine("Crawl-delay: 1");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public async Task UpdateConfigurationAsync(SiteConfiguration config)
|
||||
{
|
||||
try
|
||||
{
|
||||
config.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
var filter = Builders<SiteConfiguration>.Filter.Eq(x => x.Language, config.Language);
|
||||
var options = new ReplaceOptions { IsUpsert = true };
|
||||
|
||||
await _configurations.ReplaceOneAsync(filter, config, options);
|
||||
|
||||
// Invalidar cache
|
||||
var cacheKey = $"site_config_{config.Language}";
|
||||
_cache.Remove(cacheKey);
|
||||
|
||||
_logger.LogInformation("Configuração atualizada para idioma {Language}", config.Language);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao atualizar configuração para idioma {Language}", config.Language);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetAvailableLanguagesAsync()
|
||||
{
|
||||
const string cacheKey = "available_languages";
|
||||
|
||||
if (_cache.TryGetValue(cacheKey, out List<string>? cachedLanguages) && cachedLanguages != null)
|
||||
{
|
||||
return cachedLanguages;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var projection = Builders<SiteConfiguration>.Projection.Include(x => x.Language);
|
||||
var languages = await _configurations
|
||||
.Find(Builders<SiteConfiguration>.Filter.Empty)
|
||||
.Project<SiteConfiguration>(projection)
|
||||
.ToListAsync();
|
||||
|
||||
var availableLanguages = languages.Select(x => x.Language).Distinct().ToList();
|
||||
|
||||
if (!availableLanguages.Any())
|
||||
{
|
||||
availableLanguages = new List<string> { "pt", "en", "es" };
|
||||
}
|
||||
|
||||
_cache.Set(cacheKey, availableLanguages, TimeSpan.FromHours(1));
|
||||
return availableLanguages;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao buscar idiomas disponíveis");
|
||||
return new List<string> { "pt", "en", "es" };
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<SiteConfiguration> CreateDefaultConfigurationAsync(string language)
|
||||
{
|
||||
var defaultConfig = new SiteConfiguration
|
||||
{
|
||||
Language = language,
|
||||
Seo = CreateDefaultSeoConfig(language),
|
||||
Pages = CreateDefaultPages(language),
|
||||
Conversion = CreateDefaultConversionConfig(language),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await _configurations.InsertOneAsync(defaultConfig);
|
||||
_logger.LogInformation("Configuração padrão criada para idioma {Language}", language);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao criar configuração padrão para idioma {Language}", language);
|
||||
}
|
||||
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
private SiteConfiguration CreateDefaultConfiguration(string language)
|
||||
{
|
||||
var defaultConfig = new SiteConfiguration
|
||||
{
|
||||
Language = language,
|
||||
Seo = CreateDefaultSeoConfig(language),
|
||||
Pages = CreateDefaultPages(language),
|
||||
Conversion = CreateDefaultConversionConfig(language),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
public async Task<bool> ConfigurationExistsAsync(string language)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filter = Builders<SiteConfiguration>.Filter.Eq(x => x.Language, language);
|
||||
var count = await _configurations.CountDocumentsAsync(filter);
|
||||
return count > 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao verificar se configuração existe para idioma {Language}", language);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteConfigurationAsync(string language)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filter = Builders<SiteConfiguration>.Filter.Eq(x => x.Language, language);
|
||||
await _configurations.DeleteOneAsync(filter);
|
||||
|
||||
// Invalidar cache
|
||||
var cacheKey = $"site_config_{language}";
|
||||
_cache.Remove(cacheKey);
|
||||
|
||||
_logger.LogInformation("Configuração deletada para idioma {Language}", language);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao deletar configuração para idioma {Language}", language);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private SeoConfig CreateDefaultSeoConfig(string language)
|
||||
{
|
||||
var defaultSiteName = _config.GetValue<string>("SEO:DefaultSiteName") ?? "Site de Conversão";
|
||||
|
||||
return language switch
|
||||
{
|
||||
"en" => new SeoConfig
|
||||
{
|
||||
SiteName = defaultSiteName,
|
||||
DefaultTitle = "Conversion Site - Transform Your Business",
|
||||
DefaultDescription = "Professional conversion solutions to boost your business results",
|
||||
DefaultKeywords = "conversion, business, marketing, results",
|
||||
TwitterCard = "summary_large_image",
|
||||
GoogleTagManagerId = _config.GetValue<string>("SEO:GoogleTagManagerId") ?? "",
|
||||
GoogleAnalyticsId = _config.GetValue<string>("SEO:GoogleAnalyticsId") ?? "",
|
||||
Author = "Conversion Site Team"
|
||||
},
|
||||
"es" => new SeoConfig
|
||||
{
|
||||
SiteName = defaultSiteName,
|
||||
DefaultTitle = "Sitio de Conversión - Transforma Tu Negocio",
|
||||
DefaultDescription = "Soluciones profesionales de conversión para potenciar los resultados de tu negocio",
|
||||
DefaultKeywords = "conversión, negocio, marketing, resultados",
|
||||
TwitterCard = "summary_large_image",
|
||||
GoogleTagManagerId = _config.GetValue<string>("SEO:GoogleTagManagerId") ?? "",
|
||||
GoogleAnalyticsId = _config.GetValue<string>("SEO:GoogleAnalyticsId") ?? "",
|
||||
Author = "Equipo Sitio de Conversión"
|
||||
},
|
||||
_ => new SeoConfig
|
||||
{
|
||||
SiteName = defaultSiteName,
|
||||
DefaultTitle = "Site de Conversão - Transforme Seu Negócio",
|
||||
DefaultDescription = "Soluções profissionais de conversão para alavancar os resultados do seu negócio",
|
||||
DefaultKeywords = "conversão, negócio, marketing, resultados",
|
||||
TwitterCard = "summary_large_image",
|
||||
GoogleTagManagerId = _config.GetValue<string>("SEO:GoogleTagManagerId") ?? "",
|
||||
GoogleAnalyticsId = _config.GetValue<string>("SEO:GoogleAnalyticsId") ?? "",
|
||||
Author = "Equipe Site de Conversão"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Dictionary<string, PageContent> CreateDefaultPages(string language)
|
||||
{
|
||||
return language switch
|
||||
{
|
||||
"en" => new Dictionary<string, PageContent>
|
||||
{
|
||||
["index"] = new PageContent
|
||||
{
|
||||
Title = "Home - Transform Your Business",
|
||||
Description = "Professional conversion solutions to boost your business results",
|
||||
H1 = "Transform Your Business Today",
|
||||
MetaTitle = "Business Transformation | Conversion Solutions"
|
||||
},
|
||||
["about"] = new PageContent
|
||||
{
|
||||
Title = "About Us - Our Mission",
|
||||
Description = "Learn about our mission to help businesses achieve better conversion rates",
|
||||
H1 = "About Our Company",
|
||||
MetaTitle = "About Us | Conversion Experts"
|
||||
}
|
||||
},
|
||||
"es" => new Dictionary<string, PageContent>
|
||||
{
|
||||
["index"] = new PageContent
|
||||
{
|
||||
Title = "Inicio - Transforma Tu Negocio",
|
||||
Description = "Soluciones profesionales de conversión para potenciar los resultados de tu negocio",
|
||||
H1 = "Transforma Tu Negocio Hoy",
|
||||
MetaTitle = "Transformación Empresarial | Soluciones de Conversión"
|
||||
},
|
||||
["about"] = new PageContent
|
||||
{
|
||||
Title = "Acerca de Nosotros - Nuestra Misión",
|
||||
Description = "Conoce nuestra misión de ayudar a las empresas a lograr mejores tasas de conversión",
|
||||
H1 = "Acerca de Nuestra Empresa",
|
||||
MetaTitle = "Acerca de Nosotros | Expertos en Conversión"
|
||||
}
|
||||
},
|
||||
_ => new Dictionary<string, PageContent>
|
||||
{
|
||||
["index"] = new PageContent
|
||||
{
|
||||
Title = "Início - Transforme Seu Negócio",
|
||||
Description = "Soluções profissionais de conversão para alavancar os resultados do seu negócio",
|
||||
H1 = "Transforme Seu Negócio Hoje",
|
||||
MetaTitle = "Transformação Empresarial | Soluções de Conversão"
|
||||
},
|
||||
["about"] = new PageContent
|
||||
{
|
||||
Title = "Sobre Nós - Nossa Missão",
|
||||
Description = "Conheça nossa missão de ajudar empresas a alcançar melhores taxas de conversão",
|
||||
H1 = "Sobre Nossa Empresa",
|
||||
MetaTitle = "Sobre Nós | Especialistas em Conversão"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private ConversionConfig CreateDefaultConversionConfig(string language)
|
||||
{
|
||||
return new ConversionConfig
|
||||
{
|
||||
FormAction = "/conversion/submit",
|
||||
ThankYouPage = language switch
|
||||
{
|
||||
"en" => "/thank-you",
|
||||
"es" => "/gracias",
|
||||
_ => "/obrigado"
|
||||
},
|
||||
FormFields = language switch
|
||||
{
|
||||
"en" => new List<FormField>
|
||||
{
|
||||
new() { Name = "name", Type = "text", Label = "Full Name", Placeholder = "Enter your full name", Required = true, Order = 1 },
|
||||
new() { Name = "email", Type = "email", Label = "Email", Placeholder = "Enter your email", Required = true, Order = 2 },
|
||||
new() { Name = "phone", Type = "tel", Label = "Phone", Placeholder = "Enter your phone number", Required = false, Order = 3 }
|
||||
},
|
||||
"es" => new List<FormField>
|
||||
{
|
||||
new() { Name = "name", Type = "text", Label = "Nombre Completo", Placeholder = "Ingresa tu nombre completo", Required = true, Order = 1 },
|
||||
new() { Name = "email", Type = "email", Label = "Correo Electrónico", Placeholder = "Ingresa tu correo", Required = true, Order = 2 },
|
||||
new() { Name = "phone", Type = "tel", Label = "Teléfono", Placeholder = "Ingresa tu número de teléfono", Required = false, Order = 3 }
|
||||
},
|
||||
_ => new List<FormField>
|
||||
{
|
||||
new() { Name = "name", Type = "text", Label = "Nome Completo", Placeholder = "Digite seu nome completo", Required = true, Order = 1 },
|
||||
new() { Name = "email", Type = "email", Label = "E-mail", Placeholder = "Digite seu e-mail", Required = true, Order = 2 },
|
||||
new() { Name = "phone", Type = "tel", Label = "Telefone", Placeholder = "Digite seu telefone", Required = false, Order = 3 }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private string BuildUrl(string baseUrl, string language, string page)
|
||||
{
|
||||
var url = baseUrl.TrimEnd('/');
|
||||
|
||||
if (language != "pt") // português é o padrão
|
||||
{
|
||||
url += $"/{language}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(page))
|
||||
{
|
||||
url += $"/{page}";
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
154
OnlyOneAccessTemplate/Views/Conversion/ThankYou.cshtml
Normal file
154
OnlyOneAccessTemplate/Views/Conversion/ThankYou.cshtml
Normal file
@ -0,0 +1,154 @@
|
||||
@{
|
||||
ViewData["Title"] = ViewBag.Title ?? "Obrigado!";
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
|
||||
@section Head {
|
||||
<!-- Conversion tracking pixel -->
|
||||
<script>
|
||||
// Facebook Pixel
|
||||
if (typeof fbq !== 'undefined') {
|
||||
fbq('track', 'Lead');
|
||||
}
|
||||
|
||||
// Google Analytics
|
||||
if (typeof gtag !== 'undefined') {
|
||||
gtag('event', 'conversion', {
|
||||
'send_to': 'AW-CONVERSION_ID/CONVERSION_LABEL',
|
||||
'value': 1.0,
|
||||
'currency': 'BRL'
|
||||
});
|
||||
}
|
||||
|
||||
// Custom conversion tracking
|
||||
if (window.analytics) {
|
||||
window.analytics.sendEvent({
|
||||
event: 'conversion_page_view',
|
||||
page: 'thank-you',
|
||||
language: '@ViewBag.Language'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
<div class="thank-you-page py-5">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center text-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="thank-you-content" data-aos="fade-up">
|
||||
<!-- Success Icon -->
|
||||
<div class="success-icon mb-4">
|
||||
<i class="fas fa-check-circle fa-5x text-success"></i>
|
||||
</div>
|
||||
|
||||
<!-- Main Message -->
|
||||
<h1 class="display-4 fw-bold mb-4 text-success">
|
||||
@ViewBag.SuccessTitle
|
||||
</h1>
|
||||
|
||||
<p class="lead mb-4">
|
||||
@ViewBag.SuccessMessage
|
||||
</p>
|
||||
|
||||
<!-- What Happens Next -->
|
||||
<div class="next-steps bg-light rounded-3 p-4 mb-5" data-aos="fade-up" data-aos-delay="200">
|
||||
<h3 class="h4 fw-bold mb-3">@ViewBag.NextStepsTitle</h3>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="step-item">
|
||||
<div class="step-icon mb-3">
|
||||
<i class="fas fa-phone fa-2x text-primary"></i>
|
||||
</div>
|
||||
<h5 class="fw-bold">@ViewBag.Step1Title</h5>
|
||||
<p class="mb-0">@ViewBag.Step1Description</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="step-item">
|
||||
<div class="step-icon mb-3">
|
||||
<i class="fas fa-calendar fa-2x text-primary"></i>
|
||||
</div>
|
||||
<h5 class="fw-bold">@ViewBag.Step2Title</h5>
|
||||
<p class="mb-0">@ViewBag.Step2Description</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="step-item">
|
||||
<div class="step-icon mb-3">
|
||||
<i class="fas fa-rocket fa-2x text-primary"></i>
|
||||
</div>
|
||||
<h5 class="fw-bold">@ViewBag.Step3Title</h5>
|
||||
<p class="mb-0">@ViewBag.Step3Description</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Information -->
|
||||
<div class="contact-info mb-5" data-aos="fade-up" data-aos-delay="400">
|
||||
<h3 class="h4 fw-bold mb-3">@ViewBag.ContactInfoTitle</h3>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="contact-item d-flex align-items-center mb-3">
|
||||
<i class="fas fa-envelope fa-lg text-primary me-3"></i>
|
||||
<div>
|
||||
<strong>E-mail:</strong><br>
|
||||
<a href="mailto:@ViewBag.ContactEmail">@ViewBag.ContactEmail</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contact-item d-flex align-items-center mb-3">
|
||||
<i class="fas fa-phone fa-lg text-primary me-3"></i>
|
||||
<div>
|
||||
<strong>Telefone:</strong><br>
|
||||
<a href="tel:@ViewBag.ContactPhone">@ViewBag.ContactPhone</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Proof -->
|
||||
<div class="social-proof mb-5" data-aos="fade-up" data-aos-delay="600">
|
||||
<div class="d-flex justify-content-center align-items-center flex-wrap gap-3">
|
||||
<div class="stars text-warning">
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
</div>
|
||||
<span class="fw-bold">@ViewBag.TestimonialsCount</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons" data-aos="fade-up" data-aos-delay="800">
|
||||
<a href="@ViewBag.HomeUrl" class="btn btn-primary btn-lg me-3">
|
||||
<i class="fas fa-home me-2"></i>
|
||||
@ViewBag.BackToHomeText
|
||||
</a>
|
||||
|
||||
@if (!string.IsNullOrEmpty(ViewBag.WhatsAppNumber as string))
|
||||
{
|
||||
<a href="https://wa.me/@ViewBag.WhatsAppNumber" target="_blank" class="btn btn-success btn-lg">
|
||||
<i class="fab fa-whatsapp me-2"></i>
|
||||
@ViewBag.WhatsAppText
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
// Auto-redirect after 30 seconds (optional)
|
||||
setTimeout(() => {
|
||||
if (confirm('@ViewBag.RedirectConfirmText')) {
|
||||
window.location.href = '@ViewBag.HomeUrl';
|
||||
}
|
||||
}, 30000);
|
||||
</script>
|
||||
}
|
||||
212
OnlyOneAccessTemplate/Views/Home/Index.cshtml
Normal file
212
OnlyOneAccessTemplate/Views/Home/Index.cshtml
Normal file
@ -0,0 +1,212 @@
|
||||
@model OnlyOneAccessTemplate.Models.PageContent
|
||||
@{
|
||||
ViewData["Title"] = Model?.MetaTitle ?? Model?.Title ?? ViewBag.Title;
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
|
||||
@section Head {
|
||||
<!-- Page specific meta tags -->
|
||||
<meta name="description" content="@(Model?.Description ?? ViewBag.Description)">
|
||||
<meta name="keywords" content="@(Model?.Keywords ?? ViewBag.Keywords)">
|
||||
|
||||
<!-- Custom CSS for animations -->
|
||||
<style>
|
||||
.hover-lift {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.15) !important;
|
||||
}
|
||||
|
||||
.animate-bounce {
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
.scroll-smooth {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.bg-gradient-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.countdown-number {
|
||||
min-width: 60px;
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
<!-- Hero Section -->
|
||||
@{
|
||||
var heroBlock = Model?.Blocks?.FirstOrDefault(b => b.Type == "hero");
|
||||
}
|
||||
@await Html.PartialAsync("Components/_Hero", heroBlock)
|
||||
|
||||
<!-- Features Section -->
|
||||
@{
|
||||
var featuresBlock = Model?.Blocks?.FirstOrDefault(b => b.Type == "features");
|
||||
}
|
||||
@await Html.PartialAsync("Components/_Features", featuresBlock)
|
||||
|
||||
<!-- Benefits/How it Works Section -->
|
||||
@if (Model?.Blocks?.Any(b => b.Type == "benefits") == true)
|
||||
{
|
||||
var benefitsBlock = Model.Blocks.First(b => b.Type == "benefits");
|
||||
<section class="benefits-section py-5">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center mb-5">
|
||||
<div class="col-lg-8 text-center">
|
||||
<h2 class="display-5 fw-bold mb-3" data-aos="fade-up">
|
||||
@benefitsBlock.Title
|
||||
</h2>
|
||||
<p class="lead text-muted" data-aos="fade-up" data-aos-delay="100">
|
||||
@benefitsBlock.Content
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6" data-aos="fade-right">
|
||||
<div class="benefit-item d-flex mb-4">
|
||||
<div class="benefit-icon me-4">
|
||||
<i class="fas fa-check-circle fa-2x text-success"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="fw-bold mb-2">Resultado Garantido</h5>
|
||||
<p class="text-muted mb-0">Metodologia comprovada com mais de 1000 casos de sucesso documentados.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="benefit-item d-flex mb-4">
|
||||
<div class="benefit-icon me-4">
|
||||
<i class="fas fa-clock fa-2x text-primary"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="fw-bold mb-2">Implementação Rápida</h5>
|
||||
<p class="text-muted mb-0">Veja os primeiros resultados em até 30 dias após a implementação.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="benefit-item d-flex">
|
||||
<div class="benefit-icon me-4">
|
||||
<i class="fas fa-users fa-2x text-warning"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="fw-bold mb-2">Suporte Especializado</h5>
|
||||
<p class="text-muted mb-0">Equipe dedicada para garantir o seu sucesso em cada etapa.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6" data-aos="fade-left">
|
||||
@if (!string.IsNullOrEmpty(benefitsBlock.ImageUrl))
|
||||
{
|
||||
<img src="@benefitsBlock.ImageUrl" alt="@benefitsBlock.Title" class="img-fluid rounded-3 shadow">
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="stats-container bg-light p-4 rounded-3">
|
||||
<div class="row text-center">
|
||||
<div class="col-4">
|
||||
<div class="stat-item">
|
||||
<h3 class="fw-bold text-primary mb-1">1000+</h3>
|
||||
<small class="text-muted">Clientes Atendidos</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="stat-item">
|
||||
<h3 class="fw-bold text-success mb-1">95%</h3>
|
||||
<small class="text-muted">Taxa de Sucesso</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="stat-item">
|
||||
<h3 class="fw-bold text-warning mb-1">24h</h3>
|
||||
<small class="text-muted">Suporte</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- Testimonials Section -->
|
||||
@{
|
||||
var testimonialsBlock = Model?.Blocks?.FirstOrDefault(b => b.Type == "testimonials");
|
||||
}
|
||||
@await Html.PartialAsync("Components/_Testimonials", testimonialsBlock)
|
||||
|
||||
<!-- Conversion Form Section -->
|
||||
@{
|
||||
var conversionConfig = ViewBag.ConversionConfig as OnlyOneAccessTemplate.Models.ConversionConfig;
|
||||
}
|
||||
@await Html.PartialAsync("Components/_ConversionForm", conversionConfig)
|
||||
|
||||
<!-- CTA Section -->
|
||||
@{
|
||||
var ctaBlock = Model?.Blocks?.FirstOrDefault(b => b.Type == "cta");
|
||||
}
|
||||
@await Html.PartialAsync("Components/_CTA", ctaBlock)
|
||||
|
||||
@section Scripts {
|
||||
<!-- AOS Animation JS -->
|
||||
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
|
||||
|
||||
<script>
|
||||
// Initialize AOS
|
||||
AOS.init({
|
||||
duration: 800,
|
||||
once: true,
|
||||
offset: 100
|
||||
});
|
||||
|
||||
// Smooth scroll for anchor links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Countdown timer
|
||||
const countdown = document.querySelector('[data-countdown]');
|
||||
if (countdown) {
|
||||
const targetDate = new Date(countdown.dataset.countdown).getTime();
|
||||
|
||||
setInterval(() => {
|
||||
const now = new Date().getTime();
|
||||
const distance = targetDate - now;
|
||||
|
||||
const days = Math.floor(distance / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
|
||||
|
||||
countdown.querySelector('[data-days]').textContent = days.toString().padStart(2, '0');
|
||||
countdown.querySelector('[data-hours]').textContent = hours.toString().padStart(2, '0');
|
||||
countdown.querySelector('[data-minutes]').textContent = minutes.toString().padStart(2, '0');
|
||||
countdown.querySelector('[data-seconds]').textContent = seconds.toString().padStart(2, '0');
|
||||
|
||||
if (distance < 0) {
|
||||
countdown.style.display = 'none';
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
}
|
||||
73
OnlyOneAccessTemplate/Views/Home/Privacy.cshtml
Normal file
73
OnlyOneAccessTemplate/Views/Home/Privacy.cshtml
Normal file
@ -0,0 +1,73 @@
|
||||
@{
|
||||
ViewData["Title"] = ViewBag.Title ?? "Política de Privacidade";
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="content-wrapper">
|
||||
<h1 class="display-5 fw-bold mb-4">@ViewData["Title"]</h1>
|
||||
<p class="lead text-muted mb-5">Última atualização: @DateTime.Now.ToString("dd/MM/yyyy")</p>
|
||||
|
||||
<div class="privacy-content">
|
||||
<section class="mb-5">
|
||||
<h2 class="h3 fw-bold mb-3">1. Informações que Coletamos</h2>
|
||||
<p>Coletamos as seguintes informações quando você utiliza nossos serviços:</p>
|
||||
<ul>
|
||||
<li><strong>Informações pessoais:</strong> Nome, e-mail, telefone e outras informações fornecidas voluntariamente</li>
|
||||
<li><strong>Informações técnicas:</strong> Endereço IP, tipo de navegador, páginas visitadas</li>
|
||||
<li><strong>Cookies:</strong> Utilizamos cookies para melhorar sua experiência</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-5">
|
||||
<h2 class="h3 fw-bold mb-3">2. Como Utilizamos suas Informações</h2>
|
||||
<p>Utilizamos suas informações para:</p>
|
||||
<ul>
|
||||
<li>Fornecer e melhorar nossos serviços</li>
|
||||
<li>Responder às suas solicitações</li>
|
||||
<li>Enviar comunicações relevantes</li>
|
||||
<li>Análises e melhorias dos nossos produtos</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-5">
|
||||
<h2 class="h3 fw-bold mb-3">3. Compartilhamento de Informações</h2>
|
||||
<p>Não vendemos, alugamos ou compartilhamos suas informações pessoais com terceiros, exceto:</p>
|
||||
<ul>
|
||||
<li>Com seu consentimento explícito</li>
|
||||
<li>Para cumprir obrigações legais</li>
|
||||
<li>Com prestadores de serviços que nos ajudam a operar nosso negócio</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-5">
|
||||
<h2 class="h3 fw-bold mb-3">4. Seus Direitos</h2>
|
||||
<p>De acordo com a LGPD, você tem o direito de:</p>
|
||||
<ul>
|
||||
<li>Acessar seus dados pessoais</li>
|
||||
<li>Corrigir dados incompletos ou incorretos</li>
|
||||
<li>Solicitar a exclusão de seus dados</li>
|
||||
<li>Revogar seu consentimento a qualquer momento</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-5">
|
||||
<h2 class="h3 fw-bold mb-3">5. Segurança</h2>
|
||||
<p>Implementamos medidas de segurança técnicas e organizacionais adequadas para proteger suas informações pessoais contra acesso não autorizado, alteração, divulgação ou destruição.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-5">
|
||||
<h2 class="h3 fw-bold mb-3">6. Contato</h2>
|
||||
<p>Para questões sobre esta política de privacidade ou seus dados pessoais, entre em contato conosco:</p>
|
||||
<ul>
|
||||
<li><strong>E-mail:</strong> @ViewBag.ContactEmail</li>
|
||||
<li><strong>Telefone:</strong> @ViewBag.ContactPhone</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
73
OnlyOneAccessTemplate/Views/Home/Terms.cshtml
Normal file
73
OnlyOneAccessTemplate/Views/Home/Terms.cshtml
Normal file
@ -0,0 +1,73 @@
|
||||
@{
|
||||
ViewData["Title"] = ViewBag.Title ?? "Termos de Uso";
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="content-wrapper">
|
||||
<h1 class="display-5 fw-bold mb-4">@ViewData["Title"]</h1>
|
||||
<p class="lead text-muted mb-5">Última atualização: @DateTime.Now.ToString("dd/MM/yyyy")</p>
|
||||
|
||||
<div class="terms-content">
|
||||
<section class="mb-5">
|
||||
<h2 class="h3 fw-bold mb-3">1. Aceitação dos Termos</h2>
|
||||
<p>Ao acessar e utilizar nossos serviços, você concorda em estar vinculado a estes Termos de Uso. Se você não concordar com qualquer parte destes termos, não utilize nossos serviços.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-5">
|
||||
<h2 class="h3 fw-bold mb-3">2. Descrição dos Serviços</h2>
|
||||
<p>Oferecemos soluções de conversão e otimização para empresas, incluindo:</p>
|
||||
<ul>
|
||||
<li>Consultoria especializada</li>
|
||||
<li>Implementação de estratégias</li>
|
||||
<li>Suporte técnico</li>
|
||||
<li>Análise de resultados</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-5">
|
||||
<h2 class="h3 fw-bold mb-3">3. Responsabilidades do Usuário</h2>
|
||||
<p>Ao utilizar nossos serviços, você se compromete a:</p>
|
||||
<ul>
|
||||
<li>Fornecer informações verdadeiras e atualizadas</li>
|
||||
<li>Utilizar os serviços de forma legal e ética</li>
|
||||
<li>Não interferir no funcionamento dos serviços</li>
|
||||
<li>Respeitar os direitos de propriedade intelectual</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-5">
|
||||
<h2 class="h3 fw-bold mb-3">4. Propriedade Intelectual</h2>
|
||||
<p>Todo o conteúdo presente em nossos serviços, incluindo textos, imagens, logos e software, é protegido por direitos de propriedade intelectual e pertence a nós ou aos nossos licenciadores.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-5">
|
||||
<h2 class="h3 fw-bold mb-3">5. Limitação de Responsabilidade</h2>
|
||||
<p>Nossos serviços são fornecidos "como estão". Não garantimos que sejam livres de erros ou interrupções. Nossa responsabilidade é limitada ao valor pago pelos serviços.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-5">
|
||||
<h2 class="h3 fw-bold mb-3">6. Modificações</h2>
|
||||
<p>Reservamo-nos o direito de modificar estes termos a qualquer momento. As alterações entrarão em vigor imediatamente após a publicação.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-5">
|
||||
<h2 class="h3 fw-bold mb-3">7. Lei Aplicável</h2>
|
||||
<p>Estes termos são regidos pelas leis brasileiras. Qualquer disputa será resolvida nos tribunais brasileiros.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-5">
|
||||
<h2 class="h3 fw-bold mb-3">8. Contato</h2>
|
||||
<p>Para questões sobre estes termos, entre em contato conosco:</p>
|
||||
<ul>
|
||||
<li><strong>E-mail:</strong> @ViewBag.ContactEmail</li>
|
||||
<li><strong>Telefone:</strong> @ViewBag.ContactPhone</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
76
OnlyOneAccessTemplate/Views/Shared/Components/_CTA.cshtml
Normal file
76
OnlyOneAccessTemplate/Views/Shared/Components/_CTA.cshtml
Normal file
@ -0,0 +1,76 @@
|
||||
@model OnlyOneAccessTemplate.Models.ContentBlock
|
||||
|
||||
<section class="cta-section py-5 bg-gradient-primary text-white"
|
||||
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center text-center">
|
||||
<div class="col-lg-8">
|
||||
<div data-aos="fade-up">
|
||||
@if (!string.IsNullOrEmpty(Model?.Properties.GetValueOrDefault("urgency_badge")?.ToString()))
|
||||
{
|
||||
<span class="badge bg-warning text-dark mb-3 px-3 py-2 rounded-pill fw-bold">
|
||||
@Model.Properties["urgency_badge"]
|
||||
</span>
|
||||
}
|
||||
|
||||
<h2 class="display-5 fw-bold mb-4">
|
||||
@Html.Raw(Model?.Title ?? ViewBag.DefaultCtaTitle)
|
||||
</h2>
|
||||
|
||||
<p class="lead mb-4 opacity-90">
|
||||
@Html.Raw(Model?.Content ?? ViewBag.DefaultCtaSubtitle)
|
||||
</p>
|
||||
|
||||
@if (Model?.Properties.ContainsKey("countdown") == true && Model.Properties["countdown"] is string countdownDate)
|
||||
{
|
||||
<div class="countdown-timer mb-4" data-countdown="@countdownDate">
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
<div class="countdown-item">
|
||||
<div class="countdown-number bg-white text-primary rounded p-2 fw-bold fs-4" data-days>00</div>
|
||||
<small class="d-block mt-1">@ViewBag.DaysText</small>
|
||||
</div>
|
||||
<div class="countdown-item">
|
||||
<div class="countdown-number bg-white text-primary rounded p-2 fw-bold fs-4" data-hours>00</div>
|
||||
<small class="d-block mt-1">@ViewBag.HoursText</small>
|
||||
</div>
|
||||
<div class="countdown-item">
|
||||
<div class="countdown-number bg-white text-primary rounded p-2 fw-bold fs-4" data-minutes>00</div>
|
||||
<small class="d-block mt-1">@ViewBag.MinutesText</small>
|
||||
</div>
|
||||
<div class="countdown-item">
|
||||
<div class="countdown-number bg-white text-primary rounded p-2 fw-bold fs-4" data-seconds>00</div>
|
||||
<small class="d-block mt-1">@ViewBag.SecondsText</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="cta-buttons">
|
||||
<a href="#conversion-form" class="btn btn-light btn-lg me-3 scroll-to-form">
|
||||
<i class="fas fa-rocket me-2"></i>
|
||||
@(Model?.ButtonText ?? ViewBag.DefaultCtaButtonText)
|
||||
</a>
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model?.Properties.GetValueOrDefault("secondary_button_text")?.ToString()))
|
||||
{
|
||||
<a href="@Model.Properties.GetValueOrDefault("secondary_button_url")"
|
||||
class="btn btn-outline-light btn-lg">
|
||||
@Model.Properties["secondary_button_text"]
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Model?.Properties.ContainsKey("guarantee") == true)
|
||||
{
|
||||
<div class="guarantee-badge mt-4 pt-4 border-top border-light border-opacity-25">
|
||||
<i class="fas fa-shield-alt fa-2x mb-2"></i>
|
||||
<p class="mb-0">
|
||||
<strong>@Model.Properties["guarantee"]</strong>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -0,0 +1,238 @@
|
||||
@model OnlyOneAccessTemplate.Models.ConversionConfig
|
||||
|
||||
<section id="conversion-form" class="conversion-section py-5">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="conversion-form-wrapper">
|
||||
<div class="row g-0 shadow-lg rounded-3 overflow-hidden">
|
||||
<!-- Form Side -->
|
||||
<div class="col-md-7 bg-white p-5">
|
||||
<div class="form-header text-center mb-4">
|
||||
<h3 class="fw-bold mb-3">@ViewBag.FormTitle</h3>
|
||||
<p class="text-muted">@ViewBag.FormSubtitle</p>
|
||||
</div>
|
||||
|
||||
<form id="conversionForm" action="@(Model?.FormAction ?? "/conversion/submit")" method="post" class="needs-validation" novalidate>
|
||||
<input type="hidden" name="language" value="@ViewBag.Language" />
|
||||
<input type="hidden" name="pageName" value="@ViewBag.CurrentPage" />
|
||||
<input type="hidden" name="source" id="source" />
|
||||
<input type="hidden" name="medium" id="medium" />
|
||||
<input type="hidden" name="campaign" id="campaign" />
|
||||
|
||||
@if (Model?.FormFields?.Any() == true)
|
||||
{
|
||||
foreach (var field in Model.FormFields.OrderBy(f => f.Order))
|
||||
{
|
||||
<div class="mb-3">
|
||||
<label for="@field.Name" class="form-label">
|
||||
@field.Label
|
||||
@if (field.Required)
|
||||
{
|
||||
<span class="text-danger">*</span>
|
||||
}
|
||||
</label>
|
||||
|
||||
@{
|
||||
var fieldType = field.Type.ToLowerInvariant();
|
||||
var requiredAttr = field.Required ? "required" : "";
|
||||
var patternAttr = !string.IsNullOrEmpty(field.ValidationRegex) ? $"pattern=\"{field.ValidationRegex}\"" : "";
|
||||
}
|
||||
|
||||
@if (fieldType == "email")
|
||||
{
|
||||
<input type="email"
|
||||
class="form-control form-control-lg"
|
||||
id="@field.Name"
|
||||
name="@field.Name"
|
||||
placeholder="@field.Placeholder"
|
||||
@Html.Raw(requiredAttr) />
|
||||
<div class="invalid-feedback">
|
||||
@ViewBag.EmailValidationMessage
|
||||
</div>
|
||||
}
|
||||
else if (fieldType == "tel" || fieldType == "phone")
|
||||
{
|
||||
<input type="tel"
|
||||
class="form-control form-control-lg"
|
||||
id="@field.Name"
|
||||
name="@field.Name"
|
||||
placeholder="@field.Placeholder"
|
||||
@Html.Raw(requiredAttr) />
|
||||
<div class="invalid-feedback">
|
||||
@ViewBag.PhoneValidationMessage
|
||||
</div>
|
||||
}
|
||||
else if (fieldType == "textarea")
|
||||
{
|
||||
<textarea class="form-control form-control-lg"
|
||||
id="@field.Name"
|
||||
name="@field.Name"
|
||||
rows="4"
|
||||
placeholder="@field.Placeholder"
|
||||
@Html.Raw(requiredAttr)></textarea>
|
||||
<div class="invalid-feedback">
|
||||
@ViewBag.RequiredFieldMessage
|
||||
</div>
|
||||
}
|
||||
else if (fieldType == "select")
|
||||
{
|
||||
<select class="form-select form-select-lg"
|
||||
id="@field.Name"
|
||||
name="@field.Name"
|
||||
@Html.Raw(requiredAttr)>
|
||||
<option value="">@field.Placeholder</option>
|
||||
@if (field.Properties.ContainsKey("options") && field.Properties["options"] is IEnumerable<string> options)
|
||||
{
|
||||
foreach (var option in options)
|
||||
{
|
||||
<option value="@option">@option</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
<div class="invalid-feedback">
|
||||
@ViewBag.RequiredFieldMessage
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="@field.Type"
|
||||
class="form-control form-control-lg"
|
||||
id="@field.Name"
|
||||
name="@field.Name"
|
||||
placeholder="@field.Placeholder"
|
||||
@Html.Raw(requiredAttr)
|
||||
@Html.Raw(patternAttr) />
|
||||
<div class="invalid-feedback">
|
||||
@ViewBag.RequiredFieldMessage
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Campos padrão -->
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">@ViewBag.NameLabel <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control form-control-lg" id="name" name="name" placeholder="@ViewBag.NamePlaceholder" required />
|
||||
<div class="invalid-feedback">@ViewBag.RequiredFieldMessage</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">@ViewBag.EmailLabel <span class="text-danger">*</span></label>
|
||||
<input type="email" class="form-control form-control-lg" id="email" name="email" placeholder="@ViewBag.EmailPlaceholder" required />
|
||||
<div class="invalid-feedback">@ViewBag.EmailValidationMessage</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="phone" class="form-label">@ViewBag.PhoneLabel</label>
|
||||
<input type="tel" class="form-control form-control-lg" id="phone" name="phone" placeholder="@ViewBag.PhonePlaceholder" />
|
||||
<div class="invalid-feedback">@ViewBag.PhoneValidationMessage</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Checkbox de consentimento -->
|
||||
<div class="mb-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="consent" name="consent" required />
|
||||
<label class="form-check-label small" for="consent">
|
||||
@Html.Raw(ViewBag.ConsentText)
|
||||
</label>
|
||||
<div class="invalid-feedback">@ViewBag.ConsentValidationMessage</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg fw-bold" id="submitBtn">
|
||||
<span class="btn-text">@ViewBag.SubmitButtonText</span>
|
||||
<span class="btn-loading d-none">
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status"></span>
|
||||
@ViewBag.LoadingText
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Security badges -->
|
||||
<div class="security-badges text-center mt-3">
|
||||
<small class="text-muted d-flex align-items-center justify-content-center">
|
||||
<i class="fas fa-lock me-1"></i>
|
||||
@ViewBag.SecurityText
|
||||
</small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Benefits Side -->
|
||||
<div class="col-md-5 bg-primary text-white p-5 d-flex flex-column justify-content-center">
|
||||
<div class="benefits-content">
|
||||
<h4 class="fw-bold mb-4">@ViewBag.BenefitsTitle</h4>
|
||||
|
||||
<div class="benefit-list">
|
||||
@if (ViewBag.BenefitsList is string[] benefits)
|
||||
{
|
||||
foreach (var benefit in benefits)
|
||||
{
|
||||
<div class="d-flex align-items-start mb-3">
|
||||
<i class="fas fa-check-circle text-success me-3 mt-1"></i>
|
||||
<span>@benefit</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="d-flex align-items-start mb-3">
|
||||
<i class="fas fa-check-circle text-success me-3 mt-1"></i>
|
||||
<span>@ViewBag.DefaultBenefit1</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-start mb-3">
|
||||
<i class="fas fa-check-circle text-success me-3 mt-1"></i>
|
||||
<span>@ViewBag.DefaultBenefit2</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-start mb-3">
|
||||
<i class="fas fa-check-circle text-success me-3 mt-1"></i>
|
||||
<span>@ViewBag.DefaultBenefit3</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(ViewBag.TestimonialsCount as string))
|
||||
{
|
||||
<div class="testimonial-count mt-4 pt-4 border-top border-light border-opacity-25">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="stars text-warning me-2">
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
</div>
|
||||
<small>@ViewBag.TestimonialsCount</small>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Success Modal -->
|
||||
<div class="modal fade" id="successModal" tabindex="-1" aria-labelledby="successModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-body text-center p-5">
|
||||
<div class="success-icon mb-4">
|
||||
<i class="fas fa-check-circle fa-4x text-success"></i>
|
||||
</div>
|
||||
<h4 class="fw-bold mb-3">@ViewBag.SuccessTitle</h4>
|
||||
<p class="text-muted mb-4">@ViewBag.SuccessMessage</p>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">@ViewBag.CloseButtonText</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,83 @@
|
||||
@model OnlyOneAccessTemplate.Models.ContentBlock
|
||||
|
||||
<section id="features" class="features-section py-5 bg-light">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center mb-5">
|
||||
<div class="col-lg-8 text-center">
|
||||
<h2 class="display-5 fw-bold mb-3" data-aos="fade-up">
|
||||
@(Model?.Title ?? ViewBag.DefaultFeaturesTitle)
|
||||
</h2>
|
||||
<p class="lead text-muted" data-aos="fade-up" data-aos-delay="100">
|
||||
@(Model?.Content ?? ViewBag.DefaultFeaturesSubtitle)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
@if (Model?.Properties.ContainsKey("feature_list") == true && Model.Properties["feature_list"] is IEnumerable<object> featureList)
|
||||
{
|
||||
int index = 0;
|
||||
@foreach (var feature in featureList.Take(6))
|
||||
{
|
||||
var featureDict = feature as Dictionary<string, object>;
|
||||
var delay = (index * 100).ToString();
|
||||
index++;
|
||||
|
||||
|
||||
<div class="col-md-6 col-lg-4" data-aos="fade-up" data-aos-delay="@delay">
|
||||
<div class="feature-card h-100 text-center p-4 bg-white rounded-3 shadow-sm border-0 hover-lift">
|
||||
<div class="feature-icon mb-3">
|
||||
<i class="@(featureDict?.GetValueOrDefault("icon")?.ToString() ?? "fas fa-star") fa-3x text-primary"></i>
|
||||
</div>
|
||||
<h5 class="fw-bold mb-3">@featureDict?.GetValueOrDefault("title")</h5>
|
||||
<p class="text-muted mb-0">@featureDict?.GetValueOrDefault("description")</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Features padrão -->
|
||||
<div class="col-md-6 col-lg-4" data-aos="fade-up">
|
||||
<div class="feature-card h-100 text-center p-4 bg-white rounded-3 shadow-sm border-0 hover-lift">
|
||||
<div class="feature-icon mb-3">
|
||||
<i class="fas fa-rocket fa-3x text-primary"></i>
|
||||
</div>
|
||||
<h5 class="fw-bold mb-3">@ViewBag.Feature1Title</h5>
|
||||
<p class="text-muted mb-0">@ViewBag.Feature1Description</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4" data-aos="fade-up" data-aos-delay="100">
|
||||
<div class="feature-card h-100 text-center p-4 bg-white rounded-3 shadow-sm border-0 hover-lift">
|
||||
<div class="feature-icon mb-3">
|
||||
<i class="fas fa-shield-alt fa-3x text-primary"></i>
|
||||
</div>
|
||||
<h5 class="fw-bold mb-3">@ViewBag.Feature2Title</h5>
|
||||
<p class="text-muted mb-0">@ViewBag.Feature2Description</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4" data-aos="fade-up" data-aos-delay="200">
|
||||
<div class="feature-card h-100 text-center p-4 bg-white rounded-3 shadow-sm border-0 hover-lift">
|
||||
<div class="feature-icon mb-3">
|
||||
<i class="fas fa-users fa-3x text-primary"></i>
|
||||
</div>
|
||||
<h5 class="fw-bold mb-3">@ViewBag.Feature3Title</h5>
|
||||
<p class="text-muted mb-0">@ViewBag.Feature3Description</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model?.ButtonText))
|
||||
{
|
||||
<div class="text-center mt-5" data-aos="fade-up" data-aos-delay="300">
|
||||
<a href="#conversion-form" class="btn btn-primary btn-lg scroll-to-form">
|
||||
@Model.ButtonText
|
||||
<i class="fas fa-arrow-right ms-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
@ -0,0 +1,5 @@
|
||||
@*
|
||||
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
|
||||
*@
|
||||
@{
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
<form id="quickForm" action="/conversion/submit" method="post" class="needs-validation" novalidate>
|
||||
<input type="hidden" name="language" value="@ViewBag.Language">
|
||||
<input type="hidden" name="pageName" value="hero">
|
||||
<input type="hidden" name="formType" value="quick">
|
||||
|
||||
<div class="mb-3">
|
||||
<input type="text" class="form-control" name="name" placeholder="@ViewBag.NamePlaceholder" required>
|
||||
<div class="invalid-feedback">@ViewBag.RequiredFieldMessage</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<input type="email" class="form-control" name="email" placeholder="@ViewBag.EmailPlaceholder" required>
|
||||
<div class="invalid-feedback">@ViewBag.EmailValidationMessage</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<input type="tel" class="form-control" name="phone" placeholder="@ViewBag.PhonePlaceholder">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="quickConsent" name="consent" required>
|
||||
<label class="form-check-label small" for="quickConsent">
|
||||
@Html.Raw(ViewBag.QuickConsentText)
|
||||
</label>
|
||||
<div class="invalid-feedback">@ViewBag.ConsentValidationMessage</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary fw-bold">
|
||||
<span class="btn-text">@ViewBag.QuickSubmitText</span>
|
||||
<span class="btn-loading d-none">
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status"></span>
|
||||
@ViewBag.LoadingText
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -0,0 +1,148 @@
|
||||
@model OnlyOneAccessTemplate.Models.ContentBlock
|
||||
|
||||
<section class="testimonials-section py-5 bg-light">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center mb-5">
|
||||
<div class="col-lg-8 text-center">
|
||||
<h2 class="display-5 fw-bold mb-3" data-aos="fade-up">
|
||||
@(Model?.Title ?? ViewBag.DefaultTestimonialsTitle)
|
||||
</h2>
|
||||
<p class="lead text-muted" data-aos="fade-up" data-aos-delay="100">
|
||||
@(Model?.Content ?? ViewBag.DefaultTestimonialsSubtitle)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
@if (Model?.Properties.ContainsKey("testimonials") == true && Model.Properties["testimonials"] is IEnumerable<object> testimonials)
|
||||
{
|
||||
int index = 0;
|
||||
@foreach (var testimonial in testimonials.Take(3))
|
||||
{
|
||||
|
||||
var testimonialDict = testimonial as Dictionary<string, object>;
|
||||
var delay = (index * 100).ToString();
|
||||
index++;
|
||||
|
||||
|
||||
<div class="col-md-6 col-lg-4" data-aos="fade-up" data-aos-delay="@delay">
|
||||
<div class="testimonial-card h-100 bg-white p-4 rounded-3 shadow-sm border-0">
|
||||
<div class="stars text-warning mb-3">
|
||||
@for (int i = 0; i < 5; i++)
|
||||
{
|
||||
<i class="fas fa-star"></i>
|
||||
}
|
||||
</div>
|
||||
|
||||
<blockquote class="mb-4">
|
||||
<p class="mb-0 fst-italic">
|
||||
"@testimonialDict?.GetValueOrDefault("quote")"
|
||||
</p>
|
||||
</blockquote>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
@if (!string.IsNullOrEmpty(testimonialDict?.GetValueOrDefault("avatar")?.ToString()))
|
||||
{
|
||||
<img src="@testimonialDict["avatar"]"
|
||||
alt="@testimonialDict?.GetValueOrDefault("name")"
|
||||
class="rounded-circle me-3"
|
||||
width="50" height="50">
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="bg-primary rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px;">
|
||||
<i class="fas fa-user text-white"></i>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<h6 class="mb-0 fw-bold">@testimonialDict?.GetValueOrDefault("name")</h6>
|
||||
<small class="text-muted">@testimonialDict?.GetValueOrDefault("position")</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Testimonials padrão -->
|
||||
<div class="col-md-6 col-lg-4" data-aos="fade-up">
|
||||
<div class="testimonial-card h-100 bg-white p-4 rounded-3 shadow-sm border-0">
|
||||
<div class="stars text-warning mb-3">
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
</div>
|
||||
<blockquote class="mb-4">
|
||||
<p class="mb-0 fst-italic">"@ViewBag.Testimonial1Quote"</p>
|
||||
</blockquote>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-primary rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px;">
|
||||
<i class="fas fa-user text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-0 fw-bold">@ViewBag.Testimonial1Name</h6>
|
||||
<small class="text-muted">@ViewBag.Testimonial1Position</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4" data-aos="fade-up" data-aos-delay="100">
|
||||
<div class="testimonial-card h-100 bg-white p-4 rounded-3 shadow-sm border-0">
|
||||
<div class="stars text-warning mb-3">
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
</div>
|
||||
<blockquote class="mb-4">
|
||||
<p class="mb-0 fst-italic">"@ViewBag.Testimonial2Quote"</p>
|
||||
</blockquote>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-primary rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px;">
|
||||
<i class="fas fa-user text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-0 fw-bold">@ViewBag.Testimonial2Name</h6>
|
||||
<small class="text-muted">@ViewBag.Testimonial2Position</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4" data-aos="fade-up" data-aos-delay="200">
|
||||
<div class="testimonial-card h-100 bg-white p-4 rounded-3 shadow-sm border-0">
|
||||
<div class="stars text-warning mb-3">
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
</div>
|
||||
<blockquote class="mb-4">
|
||||
<p class="mb-0 fst-italic">"@ViewBag.Testimonial3Quote"</p>
|
||||
</blockquote>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-primary rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px;">
|
||||
<i class="fas fa-user text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-0 fw-bold">@ViewBag.Testimonial3Name</h6>
|
||||
<small class="text-muted">@ViewBag.Testimonial3Position</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
25
OnlyOneAccessTemplate/Views/Shared/Error.cshtml
Normal file
25
OnlyOneAccessTemplate/Views/Shared/Error.cshtml
Normal file
@ -0,0 +1,25 @@
|
||||
@model ErrorViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Error";
|
||||
}
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (Model.ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@Model.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>
|
||||
80
OnlyOneAccessTemplate/Views/Shared/_Footer.cshtml
Normal file
80
OnlyOneAccessTemplate/Views/Shared/_Footer.cshtml
Normal file
@ -0,0 +1,80 @@
|
||||
<footer class="bg-dark text-light py-5 mt-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-4">
|
||||
<h5 class="text-primary mb-3">@ViewBag.SiteName</h5>
|
||||
<p>@ViewBag.FooterDescription</p>
|
||||
<div class="social-links">
|
||||
@if (!string.IsNullOrEmpty(ViewBag.FacebookUrl as string))
|
||||
{
|
||||
<a href="@ViewBag.FacebookUrl" class="text-light me-3" target="_blank" rel="noopener">
|
||||
<i class="fab fa-facebook-f"></i>
|
||||
</a>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(ViewBag.InstagramUrl as string))
|
||||
{
|
||||
<a href="@ViewBag.InstagramUrl" class="text-light me-3" target="_blank" rel="noopener">
|
||||
<i class="fab fa-instagram"></i>
|
||||
</a>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(ViewBag.LinkedInUrl as string))
|
||||
{
|
||||
<a href="@ViewBag.LinkedInUrl" class="text-light me-3" target="_blank" rel="noopener">
|
||||
<i class="fab fa-linkedin-in"></i>
|
||||
</a>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(ViewBag.TwitterUrl as string))
|
||||
{
|
||||
<a href="@ViewBag.TwitterUrl" class="text-light me-3" target="_blank" rel="noopener">
|
||||
<i class="fab fa-twitter"></i>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 mb-4">
|
||||
<h6 class="text-primary mb-3">@ViewBag.FooterMenuTitle</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="@ViewBag.HomeUrl" class="text-light text-decoration-none">@ViewBag.MenuHome</a></li>
|
||||
<li><a href="@ViewBag.AboutUrl" class="text-light text-decoration-none">@ViewBag.MenuAbout</a></li>
|
||||
<li><a href="@ViewBag.ContactUrl" class="text-light text-decoration-none">@ViewBag.MenuContact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 mb-4">
|
||||
<h6 class="text-primary mb-3">@ViewBag.FooterLegalTitle</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="@ViewBag.PrivacyUrl" class="text-light text-decoration-none">@ViewBag.MenuPrivacy</a></li>
|
||||
<li><a href="@ViewBag.TermsUrl" class="text-light text-decoration-none">@ViewBag.MenuTerms</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-4">
|
||||
<h6 class="text-primary mb-3">@ViewBag.FooterContactTitle</h6>
|
||||
@if (!string.IsNullOrEmpty(ViewBag.ContactEmail as string))
|
||||
{
|
||||
<p><i class="fas fa-envelope me-2"></i> @ViewBag.ContactEmail</p>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(ViewBag.ContactPhone as string))
|
||||
{
|
||||
<p><i class="fas fa-phone me-2"></i> @ViewBag.ContactPhone</p>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(ViewBag.ContactAddress as string))
|
||||
{
|
||||
<p><i class="fas fa-map-marker-alt me-2"></i> @ViewBag.ContactAddress</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
<p class="mb-0">© @DateTime.Now.Year @ViewBag.SiteName. @ViewBag.FooterCopyright</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end">
|
||||
<small class="text-muted">@ViewBag.FooterMadeWith</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
71
OnlyOneAccessTemplate/Views/Shared/_Head.cshtml
Normal file
71
OnlyOneAccessTemplate/Views/Shared/_Head.cshtml
Normal file
@ -0,0 +1,71 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<!-- Google Tag Manager -->
|
||||
<script>
|
||||
(function (w, d, s, l, i) {
|
||||
w[l] = w[l] || []; w[l].push({
|
||||
'gtm.start':
|
||||
new Date().getTime(), event: 'gtm.js'
|
||||
}); var f = d.getElementsByTagName(s)[0],
|
||||
j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =
|
||||
'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
|
||||
})(window, document, 'script', 'dataLayer', '@ViewBag.GTMId');
|
||||
</script>
|
||||
<!-- End Google Tag Manager -->
|
||||
<!-- SEO Meta Tags -->
|
||||
<title>@ViewBag.Title</title>
|
||||
<meta name="description" content="@ViewBag.Description" />
|
||||
<meta name="keywords" content="@ViewBag.Keywords" />
|
||||
<meta name="author" content="@ViewBag.Author" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<link rel="canonical" href="@ViewBag.CanonicalUrl" />
|
||||
|
||||
<!-- Open Graph Meta Tags -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="@ViewBag.OgTitle" />
|
||||
<meta property="og:description" content="@ViewBag.OgDescription" />
|
||||
<meta property="og:image" content="@ViewBag.OgImage" />
|
||||
<meta property="og:url" content="@ViewBag.CanonicalUrl" />
|
||||
<meta property="og:site_name" content="@ViewBag.SiteName" />
|
||||
<meta property="og:locale" content="@ViewBag.OgLocale" />
|
||||
|
||||
<!-- Twitter Card Meta Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="@ViewBag.OgTitle" />
|
||||
<meta name="twitter:description" content="@ViewBag.OgDescription" />
|
||||
<meta name="twitter:image" content="@ViewBag.OgImage" />
|
||||
<meta name="twitter:site" content="@ViewBag.TwitterHandle" />
|
||||
|
||||
<!-- Language alternatives (hreflang) -->
|
||||
<link rel="alternate" hreflang="pt" href="@ViewBag.PtUrl" />
|
||||
<link rel="alternate" hreflang="en" href="@ViewBag.EnUrl" />
|
||||
<link rel="alternate" hreflang="es" href="@ViewBag.EsUrl" />
|
||||
<link rel="alternate" hreflang="x-default" href="@ViewBag.DefaultUrl" />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
||||
crossorigin="anonymous">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- AOS Animation CSS -->
|
||||
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||
|
||||
<!-- Preconnect to external domains -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
64
OnlyOneAccessTemplate/Views/Shared/_Header.cshtml
Normal file
64
OnlyOneAccessTemplate/Views/Shared/_Header.cshtml
Normal file
@ -0,0 +1,64 @@
|
||||
<header class="navbar navbar-expand-lg navbar-light bg-white shadow-sm sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="@ViewBag.HomeUrl">
|
||||
<img src="@ViewBag.LogoUrl" alt="@ViewBag.SiteName" height="40" class="me-2">
|
||||
<span class="fw-bold text-primary">@ViewBag.SiteName</span>
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @(ViewBag.CurrentPage == "home" ? "active" : "")"
|
||||
href="@ViewBag.HomeUrl">
|
||||
@ViewBag.MenuHome
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @(ViewBag.CurrentPage == "about" ? "active" : "")"
|
||||
href="@ViewBag.AboutUrl">
|
||||
@ViewBag.MenuAbout
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @(ViewBag.CurrentPage == "contact" ? "active" : "")"
|
||||
href="@ViewBag.ContactUrl">
|
||||
@ViewBag.MenuContact
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Language Switcher -->
|
||||
<div class="dropdown me-3">
|
||||
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button"
|
||||
id="languageDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fas fa-globe me-1"></i>
|
||||
@ViewBag.CurrentLanguageDisplay
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="languageDropdown">
|
||||
<li>
|
||||
<a class="dropdown-item @(ViewBag.Language == "pt" ? "active" : "")"
|
||||
href="@ViewBag.PtUrl">🇧🇷 Português</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item @(ViewBag.Language == "en" ? "active" : "")"
|
||||
href="@ViewBag.EnUrl">🇺🇸 English</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item @(ViewBag.Language == "es" ? "active" : "")"
|
||||
href="@ViewBag.EsUrl">🇪🇸 Español</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- CTA Button -->
|
||||
<a href="#conversion-form" class="btn btn-primary btn-sm scroll-to-form">
|
||||
@ViewBag.CtaButtonText
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
49
OnlyOneAccessTemplate/Views/Shared/_Layout.cshtml
Normal file
49
OnlyOneAccessTemplate/Views/Shared/_Layout.cshtml
Normal file
@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="@ViewBag.Language" prefix="og: http://ogp.me/ns#" dir="@ViewBag.Direction">
|
||||
<head>
|
||||
@await Html.PartialAsync("_Head")
|
||||
|
||||
<!-- Page specific head content -->
|
||||
@await RenderSectionAsync("Head", required: false)
|
||||
</head>
|
||||
<body class="@ViewBag.BodyClass">
|
||||
<!-- Google Tag Manager (noscript) -->
|
||||
<noscript>
|
||||
<iframe src="https://www.googletagmanager.com/ns.html?id=@ViewBag.GTMId"
|
||||
height="0" width="0" style="display:none;visibility:hidden"></iframe>
|
||||
</noscript>
|
||||
<!-- End Google Tag Manager (noscript) -->
|
||||
@await Html.PartialAsync("_Header")
|
||||
|
||||
<main role="main" id="main-content">
|
||||
@RenderBody()
|
||||
</main>
|
||||
|
||||
@await Html.PartialAsync("_Footer")
|
||||
|
||||
<!-- Schema.org structured data --
|
||||
<script type="application/ld+json">
|
||||
@Html.Raw(ViewBag.StructuredData ?? "{}")
|
||||
</script>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<!-- AOS Animation JS -->
|
||||
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
|
||||
|
||||
<!-- Custom JS -->
|
||||
<script src="~/js/conversion.js" asp-append-version="true"></script>
|
||||
<script src="~/js/analytics.js" asp-append-version="true"></script>
|
||||
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
|
||||
<!-- Conversion tracking -->
|
||||
@if (!string.IsNullOrEmpty(ViewBag.ConversionPixel as string))
|
||||
{
|
||||
<script>@Html.Raw(ViewBag.ConversionPixel)</script>
|
||||
}
|
||||
</body>
|
||||
</html>
|
||||
48
OnlyOneAccessTemplate/Views/Shared/_Layout.cshtml.css
Normal file
48
OnlyOneAccessTemplate/Views/Shared/_Layout.cshtml.css
Normal file
@ -0,0 +1,48 @@
|
||||
/* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
|
||||
for details on configuring this project to bundle and minify static web assets. */
|
||||
|
||||
a.navbar-brand {
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0077cc;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.border-top {
|
||||
border-top: 1px solid #e5e5e5;
|
||||
}
|
||||
.border-bottom {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.box-shadow {
|
||||
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
|
||||
}
|
||||
|
||||
button.accept-policy {
|
||||
font-size: 1rem;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
line-height: 60px;
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
|
||||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
|
||||
3
OnlyOneAccessTemplate/Views/_ViewImports.cshtml
Normal file
3
OnlyOneAccessTemplate/Views/_ViewImports.cshtml
Normal file
@ -0,0 +1,3 @@
|
||||
@using OnlyOneAccessTemplate
|
||||
@using OnlyOneAccessTemplate.Models
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
3
OnlyOneAccessTemplate/Views/_ViewStart.cshtml
Normal file
3
OnlyOneAccessTemplate/Views/_ViewStart.cshtml
Normal file
@ -0,0 +1,3 @@
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
||||
8
OnlyOneAccessTemplate/appsettings.Development.json
Normal file
8
OnlyOneAccessTemplate/appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
31
OnlyOneAccessTemplate/appsettings.json
Normal file
31
OnlyOneAccessTemplate/appsettings.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"MongoDB": "mongodb://admin:c4rn31r0@k3sw2:27017,k3ss1:27017/?authSource=admin"
|
||||
},
|
||||
"MongoDB": {
|
||||
"DatabaseName": "ConversionSite",
|
||||
"Collections": {
|
||||
"SiteConfigurations": "site_configurations",
|
||||
"ConversionData": "conversion_data",
|
||||
"Analytics": "analytics"
|
||||
}
|
||||
},
|
||||
"SEO": {
|
||||
"DefaultSiteName": "Seu Site de Conversão",
|
||||
"DefaultDomain": "https://seusite.com",
|
||||
"GoogleTagManagerId": "GTM-XXXXXXX",
|
||||
"GoogleAnalyticsId": "GA-XXXXXXXX",
|
||||
"FacebookPixelId": "XXXXXXXXX"
|
||||
},
|
||||
"ResponseCaching": {
|
||||
"Duration": 300,
|
||||
"VaryByHeader": "Accept-Language"
|
||||
}
|
||||
}
|
||||
652
OnlyOneAccessTemplate/wwwroot/css/site.css
Normal file
652
OnlyOneAccessTemplate/wwwroot/css/site.css
Normal file
@ -0,0 +1,652 @@
|
||||
/* Custom CSS for Conversion Template */
|
||||
|
||||
:root {
|
||||
--primary-color: #667eea;
|
||||
--secondary-color: #764ba2;
|
||||
--success-color: #28a745;
|
||||
--warning-color: #ffc107;
|
||||
--danger-color: #dc3545;
|
||||
--info-color: #17a2b8;
|
||||
--dark-color: #343a40;
|
||||
--light-color: #f8f9fa;
|
||||
--border-radius: 0.5rem;
|
||||
--box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Global Styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.scroll-smooth {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.display-1, .display-2, .display-3, .display-4, .display-5, .display-6 {
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-bounce {
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
.animate-fadeInUp {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.hover-lift {
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: var(--box-shadow-lg);
|
||||
}
|
||||
|
||||
.bg-gradient-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||||
}
|
||||
|
||||
.text-gradient {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.shadow-custom {
|
||||
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
|
||||
/* Header Styles */
|
||||
.navbar {
|
||||
transition: var(--transition);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.navbar.scrolled {
|
||||
background: rgba(255, 255, 255, 0.95) !important;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: 800;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-weight: 500;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link.active {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.nav-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--primary-color);
|
||||
transition: var(--transition);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.nav-link:hover::after,
|
||||
.nav-link.active::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero-section {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 100" preserveAspectRatio="none"><polygon fill="%23ffffff" points="1000,100 1000,0 0,100"/></svg>') no-repeat bottom;
|
||||
background-size: 100% 100px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-content h1 {
|
||||
font-size: clamp(2.5rem, 5vw, 4rem);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-features .fas {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.scroll-indicator {
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* Feature Cards */
|
||||
.feature-card {
|
||||
transition: var(--transition);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.feature-card:hover .feature-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Conversion Form */
|
||||
.conversion-form-wrapper {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.conversion-section {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
border: 2px solid #e9ecef;
|
||||
transition: var(--transition);
|
||||
font-size: 1.1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
.form-control-lg {
|
||||
font-size: 1.2rem;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-weight: 600;
|
||||
border-radius: var(--border-radius);
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
border: none;
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.btn-loading .spinner-border-sm {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
/* Security Badge */
|
||||
.security-badges {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.security-badges i {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
/* Benefits Side */
|
||||
.benefits-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.benefit-list .fas {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.stars {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Testimonials */
|
||||
.testimonial-card {
|
||||
transition: var(--transition);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.testimonial-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: var(--box-shadow-lg);
|
||||
}
|
||||
|
||||
.testimonial-card blockquote {
|
||||
font-style: italic;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.testimonial-card blockquote::before {
|
||||
content: '"';
|
||||
font-size: 3rem;
|
||||
color: var(--primary-color);
|
||||
position: absolute;
|
||||
top: -1rem;
|
||||
left: -1rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* CTA Section */
|
||||
.cta-section {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cta-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 100" preserveAspectRatio="none"><polygon fill="%23ffffff" points="0,0 0,100 1000,0"/></svg>') no-repeat top;
|
||||
background-size: 100% 100px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Countdown Timer */
|
||||
.countdown-timer {
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.countdown-number {
|
||||
min-width: 60px;
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
background: var(--dark-color);
|
||||
}
|
||||
|
||||
.social-links a {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.social-links a:hover {
|
||||
background: var(--primary-color);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
/* Modals */
|
||||
.modal-content {
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background: var(--light-color);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Success Modal */
|
||||
.success-icon {
|
||||
color: var(--success-color);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.scroll-progress {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
|
||||
z-index: 9999;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Scroll to Top Button */
|
||||
#scrollToTop {
|
||||
transition: var(--transition);
|
||||
box-shadow: var(--box-shadow-lg);
|
||||
}
|
||||
|
||||
#scrollToTop:hover {
|
||||
transform: translateY(-3px) scale(1.1);
|
||||
}
|
||||
|
||||
/* Language Switcher */
|
||||
.dropdown-item.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Form Validation */
|
||||
.was-validated .form-control:valid,
|
||||
.was-validated .form-select:valid {
|
||||
border-color: var(--success-color);
|
||||
}
|
||||
|
||||
.was-validated .form-control:valid:focus,
|
||||
.was-validated .form-select:valid:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
|
||||
}
|
||||
|
||||
.was-validated .form-control:invalid,
|
||||
.was-validated .form-select:invalid {
|
||||
border-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.was-validated .form-control:invalid:focus,
|
||||
.was-validated .form-select:invalid:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.valid-feedback {
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.hero-section {
|
||||
min-height: 80vh;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.hero-content h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.conversion-form-wrapper .row {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.countdown-number {
|
||||
min-width: 50px;
|
||||
min-height: 50px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.hero-content h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.display-5 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.countdown-timer .d-flex {
|
||||
gap: 1rem !important;
|
||||
}
|
||||
|
||||
.countdown-number {
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.navbar,
|
||||
.scroll-indicator,
|
||||
#scrollToTop,
|
||||
.scroll-progress,
|
||||
.cta-section,
|
||||
footer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
min-height: auto;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
* {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* High Contrast Mode */
|
||||
@media (prefers-contrast: high) {
|
||||
:root {
|
||||
--primary-color: #0000ff;
|
||||
--secondary-color: #000080;
|
||||
--border-radius: 0;
|
||||
}
|
||||
|
||||
.btn,
|
||||
.form-control,
|
||||
.form-select,
|
||||
.card {
|
||||
border: 2px solid #000;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced Motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
|
||||
.animate-bounce,
|
||||
.animate-pulse {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Mode Support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--dark-color: #ffffff;
|
||||
--light-color: #1a1a1a;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #1a1a1a;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: #2a2a2a !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.bg-light {
|
||||
background-color: #2a2a2a !important;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #cccccc !important;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
background-color: #2a2a2a;
|
||||
border-color: #404040;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
background-color: #2a2a2a;
|
||||
border-color: var(--primary-color);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #2a2a2a;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
background-color: #2a2a2a;
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: #404040;
|
||||
}
|
||||
}
|
||||
BIN
OnlyOneAccessTemplate/wwwroot/favicon.ico
Normal file
BIN
OnlyOneAccessTemplate/wwwroot/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
398
OnlyOneAccessTemplate/wwwroot/js/analytics.js
Normal file
398
OnlyOneAccessTemplate/wwwroot/js/analytics.js
Normal file
@ -0,0 +1,398 @@
|
||||
// Advanced analytics and heatmap tracking
|
||||
class AdvancedAnalytics {
|
||||
constructor() {
|
||||
this.sessionId = this.generateSessionId();
|
||||
this.startTime = Date.now();
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.trackSession();
|
||||
this.setupHeatmapTracking();
|
||||
this.setupScrollTracking();
|
||||
this.setupClickTracking();
|
||||
this.setupTimeOnPage();
|
||||
this.setupVisibilityTracking();
|
||||
}
|
||||
|
||||
generateSessionId() {
|
||||
return 'session_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
|
||||
}
|
||||
|
||||
// Track user session
|
||||
trackSession() {
|
||||
const sessionData = {
|
||||
sessionId: this.sessionId,
|
||||
userAgent: navigator.userAgent,
|
||||
screenResolution: `${screen.width}x${screen.height}`,
|
||||
windowSize: `${window.innerWidth}x${window.innerHeight}`,
|
||||
language: navigator.language,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
referrer: document.referrer,
|
||||
landingPage: window.location.pathname,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.sendAnalytics('session_start', sessionData);
|
||||
}
|
||||
|
||||
// Setup heatmap tracking
|
||||
setupHeatmapTracking() {
|
||||
let mousePositions = [];
|
||||
let lastSampleTime = 0;
|
||||
const sampleRate = 100; // Sample every 100ms
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
const now = Date.now();
|
||||
if (now - lastSampleTime > sampleRate) {
|
||||
mousePositions.push({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
timestamp: now
|
||||
});
|
||||
lastSampleTime = now;
|
||||
|
||||
// Send data in batches
|
||||
if (mousePositions.length >= 50) {
|
||||
this.sendAnalytics('heatmap_data', {
|
||||
sessionId: this.sessionId,
|
||||
positions: mousePositions,
|
||||
page: window.location.pathname
|
||||
});
|
||||
mousePositions = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Send remaining data on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (mousePositions.length > 0) {
|
||||
navigator.sendBeacon('/api/analytics/heatmap', JSON.stringify({
|
||||
sessionId: this.sessionId,
|
||||
positions: mousePositions,
|
||||
page: window.location.pathname
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Setup scroll depth tracking
|
||||
setupScrollTracking() {
|
||||
let maxScroll = 0;
|
||||
const scrollMilestones = [25, 50, 75, 90, 100];
|
||||
const triggered = new Set();
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
const scrollPercent = Math.round(
|
||||
(window.pageYOffset / (document.documentElement.scrollHeight - window.innerHeight)) * 100
|
||||
);
|
||||
|
||||
if (scrollPercent > maxScroll) {
|
||||
maxScroll = scrollPercent;
|
||||
}
|
||||
|
||||
// Track scroll milestones
|
||||
scrollMilestones.forEach(milestone => {
|
||||
if (scrollPercent >= milestone && !triggered.has(milestone)) {
|
||||
triggered.add(milestone);
|
||||
this.sendAnalytics('scroll_depth', {
|
||||
sessionId: this.sessionId,
|
||||
depth: milestone,
|
||||
page: window.location.pathname,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Setup click tracking
|
||||
setupClickTracking() {
|
||||
document.addEventListener('click', (e) => {
|
||||
const element = e.target;
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
|
||||
// Track important elements
|
||||
if (['a', 'button', 'input'].includes(tagName) || element.classList.contains('trackable')) {
|
||||
const clickData = {
|
||||
sessionId: this.sessionId,
|
||||
element: {
|
||||
tagName,
|
||||
id: element.id,
|
||||
className: element.className,
|
||||
text: element.textContent?.substring(0, 100),
|
||||
href: element.href,
|
||||
type: element.type
|
||||
},
|
||||
position: {
|
||||
x: e.pageX,
|
||||
y: e.pageY
|
||||
},
|
||||
page: window.location.pathname,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.sendAnalytics('click_tracking', clickData);
|
||||
}
|
||||
|
||||
// Track form field clicks
|
||||
if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
|
||||
this.sendAnalytics('form_interaction', {
|
||||
sessionId: this.sessionId,
|
||||
fieldName: element.name,
|
||||
fieldType: element.type,
|
||||
formId: element.closest('form')?.id,
|
||||
action: 'focus',
|
||||
page: window.location.pathname,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Setup time on page tracking
|
||||
setupTimeOnPage() {
|
||||
let pageStartTime = Date.now();
|
||||
let isActive = true;
|
||||
let totalActiveTime = 0;
|
||||
let lastActiveTime = pageStartTime;
|
||||
|
||||
// Track when page becomes active/inactive
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
const now = Date.now();
|
||||
|
||||
if (document.hidden) {
|
||||
if (isActive) {
|
||||
totalActiveTime += now - lastActiveTime;
|
||||
isActive = false;
|
||||
}
|
||||
} else {
|
||||
isActive = true;
|
||||
lastActiveTime = now;
|
||||
}
|
||||
});
|
||||
|
||||
// Send time data periodically
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
const currentActiveTime = isActive ? totalActiveTime + (now - lastActiveTime) : totalActiveTime;
|
||||
|
||||
this.sendAnalytics('time_on_page', {
|
||||
sessionId: this.sessionId,
|
||||
activeTime: Math.round(currentActiveTime / 1000), // in seconds
|
||||
totalTime: Math.round((now - pageStartTime) / 1000),
|
||||
page: window.location.pathname,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}, 30000); // Every 30 seconds
|
||||
|
||||
// Send final time on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
const now = Date.now();
|
||||
const finalActiveTime = isActive ? totalActiveTime + (now - lastActiveTime) : totalActiveTime;
|
||||
|
||||
navigator.sendBeacon('/api/analytics/time', JSON.stringify({
|
||||
sessionId: this.sessionId,
|
||||
activeTime: Math.round(finalActiveTime / 1000),
|
||||
totalTime: Math.round((now - pageStartTime) / 1000),
|
||||
page: window.location.pathname,
|
||||
timestamp: new Date().toISOString()
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// Setup visibility tracking for elements
|
||||
setupVisibilityTracking() {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const element = entry.target;
|
||||
this.sendAnalytics('element_view', {
|
||||
sessionId: this.sessionId,
|
||||
element: {
|
||||
id: element.id,
|
||||
className: element.className,
|
||||
tagName: element.tagName.toLowerCase()
|
||||
},
|
||||
page: window.location.pathname,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.5 });
|
||||
|
||||
// Track important sections
|
||||
document.querySelectorAll('section, .hero-section, .features-section, .conversion-section').forEach(el => {
|
||||
observer.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
// A/B Test tracking
|
||||
trackABTest(testName, variant) {
|
||||
this.sendAnalytics('ab_test', {
|
||||
sessionId: this.sessionId,
|
||||
testName,
|
||||
variant,
|
||||
page: window.location.pathname,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Error tracking
|
||||
trackError(error, context = {}) {
|
||||
this.sendAnalytics('javascript_error', {
|
||||
sessionId: this.sessionId,
|
||||
error: {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
filename: error.filename,
|
||||
lineno: error.lineno,
|
||||
colno: error.colno
|
||||
},
|
||||
context,
|
||||
page: window.location.pathname,
|
||||
userAgent: navigator.userAgent,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Send analytics data
|
||||
async sendAnalytics(event, data) {
|
||||
try {
|
||||
const payload = {
|
||||
event,
|
||||
data,
|
||||
url: window.location.href,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Use sendBeacon for important events if available
|
||||
if (navigator.sendBeacon && ['session_start', 'conversion', 'form_submit'].includes(event)) {
|
||||
navigator.sendBeacon('/api/analytics/track', JSON.stringify(payload));
|
||||
} else {
|
||||
// Fallback to fetch
|
||||
fetch('/api/analytics/track', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
}).catch(err => console.error('Analytics error:', err));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Analytics sending error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Get session statistics
|
||||
getSessionStats() {
|
||||
return {
|
||||
sessionId: this.sessionId,
|
||||
duration: Math.round((Date.now() - this.startTime) / 1000),
|
||||
page: window.location.pathname
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Performance monitoring
|
||||
class PerformanceMonitor {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Monitor page load performance
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(() => {
|
||||
this.trackPagePerformance();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Monitor Core Web Vitals
|
||||
this.trackWebVitals();
|
||||
}
|
||||
|
||||
trackPagePerformance() {
|
||||
if ('performance' in window) {
|
||||
const navigation = performance.getEntriesByType('navigation')[0];
|
||||
const paint = performance.getEntriesByType('paint');
|
||||
|
||||
const performanceData = {
|
||||
loadTime: Math.round(navigation.loadEventEnd - navigation.loadEventStart),
|
||||
domContentLoaded: Math.round(navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart),
|
||||
firstPaint: paint.find(p => p.name === 'first-paint')?.startTime || 0,
|
||||
firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0,
|
||||
pageSize: navigation.transferSize,
|
||||
resources: performance.getEntriesByType('resource').length
|
||||
};
|
||||
|
||||
window.analytics?.sendAnalytics('page_performance', performanceData);
|
||||
}
|
||||
}
|
||||
|
||||
trackWebVitals() {
|
||||
// Track Largest Contentful Paint (LCP)
|
||||
if ('PerformanceObserver' in window) {
|
||||
new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
const lastEntry = entries[entries.length - 1];
|
||||
|
||||
window.analytics?.sendAnalytics('web_vital_lcp', {
|
||||
value: lastEntry.startTime,
|
||||
rating: lastEntry.startTime > 2500 ? 'poor' : lastEntry.startTime > 1200 ? 'needs-improvement' : 'good'
|
||||
});
|
||||
}).observe({ entryTypes: ['largest-contentful-paint'] });
|
||||
|
||||
// Track First Input Delay (FID)
|
||||
new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
entries.forEach(entry => {
|
||||
window.analytics?.sendAnalytics('web_vital_fid', {
|
||||
value: entry.processingStart - entry.startTime,
|
||||
rating: entry.processingStart - entry.startTime > 100 ? 'poor' :
|
||||
entry.processingStart - entry.startTime > 25 ? 'needs-improvement' : 'good'
|
||||
});
|
||||
});
|
||||
}).observe({ entryTypes: ['first-input'] });
|
||||
|
||||
// Track Cumulative Layout Shift (CLS)
|
||||
let clsScore = 0;
|
||||
new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
entries.forEach(entry => {
|
||||
if (!entry.hadRecentInput) {
|
||||
clsScore += entry.value;
|
||||
}
|
||||
});
|
||||
|
||||
window.analytics?.sendAnalytics('web_vital_cls', {
|
||||
value: clsScore,
|
||||
rating: clsScore > 0.25 ? 'poor' : clsScore > 0.1 ? 'needs-improvement' : 'good'
|
||||
});
|
||||
}).observe({ entryTypes: ['layout-shift'] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize analytics
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
window.analytics = new AdvancedAnalytics();
|
||||
window.performanceMonitor = new PerformanceMonitor();
|
||||
|
||||
// Global error handling
|
||||
window.addEventListener('error', (event) => {
|
||||
window.analytics?.trackError(event.error, {
|
||||
type: 'javascript_error',
|
||||
source: event.filename,
|
||||
line: event.lineno,
|
||||
column: event.colno
|
||||
});
|
||||
});
|
||||
|
||||
// Promise rejection handling
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
window.analytics?.trackError(new Error(event.reason), {
|
||||
type: 'unhandled_promise_rejection'
|
||||
});
|
||||
});
|
||||
});
|
||||
404
OnlyOneAccessTemplate/wwwroot/js/conversion.js
Normal file
404
OnlyOneAccessTemplate/wwwroot/js/conversion.js
Normal file
@ -0,0 +1,404 @@
|
||||
// Conversion tracking and form handling
|
||||
class ConversionTracker {
|
||||
constructor() {
|
||||
this.init();
|
||||
this.setupFormHandlers();
|
||||
this.trackPageView();
|
||||
this.setupUTMTracking();
|
||||
}
|
||||
|
||||
init() {
|
||||
console.log('Conversion Tracker initialized');
|
||||
}
|
||||
|
||||
// Track page view
|
||||
trackPageView() {
|
||||
const data = {
|
||||
event: 'page_view',
|
||||
page: window.location.pathname,
|
||||
language: document.documentElement.lang || 'pt',
|
||||
referrer: document.referrer,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.sendEvent(data);
|
||||
}
|
||||
|
||||
// Setup UTM parameter tracking
|
||||
setupUTMTracking() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const utmParams = {
|
||||
source: urlParams.get('utm_source') || this.getSourceFromReferrer(),
|
||||
medium: urlParams.get('utm_medium') || this.getMediumFromReferrer(),
|
||||
campaign: urlParams.get('utm_campaign') || 'organic',
|
||||
term: urlParams.get('utm_term') || '',
|
||||
content: urlParams.get('utm_content') || ''
|
||||
};
|
||||
|
||||
// Store in hidden form fields
|
||||
document.querySelectorAll('input[name="source"]').forEach(input => {
|
||||
input.value = utmParams.source;
|
||||
});
|
||||
document.querySelectorAll('input[name="medium"]').forEach(input => {
|
||||
input.value = utmParams.medium;
|
||||
});
|
||||
document.querySelectorAll('input[name="campaign"]').forEach(input => {
|
||||
input.value = utmParams.campaign;
|
||||
});
|
||||
|
||||
// Store in localStorage for session tracking
|
||||
localStorage.setItem('utm_params', JSON.stringify(utmParams));
|
||||
}
|
||||
|
||||
getSourceFromReferrer() {
|
||||
const referrer = document.referrer;
|
||||
if (!referrer) return 'direct';
|
||||
|
||||
try {
|
||||
const hostname = new URL(referrer).hostname.toLowerCase();
|
||||
|
||||
if (hostname.includes('google')) return 'google';
|
||||
if (hostname.includes('facebook')) return 'facebook';
|
||||
if (hostname.includes('instagram')) return 'instagram';
|
||||
if (hostname.includes('youtube')) return 'youtube';
|
||||
if (hostname.includes('linkedin')) return 'linkedin';
|
||||
if (hostname.includes('twitter')) return 'twitter';
|
||||
if (hostname.includes('tiktok')) return 'tiktok';
|
||||
|
||||
return hostname;
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
getMediumFromReferrer() {
|
||||
const referrer = document.referrer;
|
||||
if (!referrer) return 'direct';
|
||||
|
||||
try {
|
||||
const hostname = new URL(referrer).hostname.toLowerCase();
|
||||
|
||||
if (hostname.includes('google')) return 'search';
|
||||
if (hostname.includes('facebook') || hostname.includes('instagram') ||
|
||||
hostname.includes('linkedin') || hostname.includes('twitter')) return 'social';
|
||||
if (hostname.includes('youtube') || hostname.includes('tiktok')) return 'video';
|
||||
|
||||
return 'referral';
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
// Setup form handlers
|
||||
setupFormHandlers() {
|
||||
// Main conversion form
|
||||
const conversionForm = document.getElementById('conversionForm');
|
||||
if (conversionForm) {
|
||||
conversionForm.addEventListener('submit', (e) => this.handleFormSubmit(e, 'main'));
|
||||
}
|
||||
|
||||
// Quick form
|
||||
const quickForm = document.getElementById('quickForm');
|
||||
if (quickForm) {
|
||||
quickForm.addEventListener('submit', (e) => this.handleFormSubmit(e, 'quick'));
|
||||
}
|
||||
|
||||
// Track form interactions
|
||||
this.trackFormInteractions();
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
async handleFormSubmit(event, formType) {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.target;
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const btnText = submitBtn.querySelector('.btn-text');
|
||||
const btnLoading = submitBtn.querySelector('.btn-loading');
|
||||
|
||||
// Validate form
|
||||
if (!form.checkValidity()) {
|
||||
form.classList.add('was-validated');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
submitBtn.disabled = true;
|
||||
btnText.classList.add('d-none');
|
||||
btnLoading.classList.remove('d-none');
|
||||
|
||||
try {
|
||||
// Collect form data
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
// Add tracking data
|
||||
data.userAgent = navigator.userAgent;
|
||||
data.timestamp = new Date().toISOString();
|
||||
data.formType = formType;
|
||||
|
||||
// Track form submission event
|
||||
this.sendEvent({
|
||||
event: 'form_submit',
|
||||
form_type: formType,
|
||||
page: window.location.pathname,
|
||||
language: data.language || 'pt'
|
||||
});
|
||||
|
||||
// Submit form
|
||||
const response = await fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
this.handleSuccess(result, formType);
|
||||
} else {
|
||||
throw new Error('Submission failed');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
this.handleError(error, formType);
|
||||
} finally {
|
||||
// Reset button state
|
||||
submitBtn.disabled = false;
|
||||
btnText.classList.remove('d-none');
|
||||
btnLoading.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle successful submission
|
||||
handleSuccess(result, formType) {
|
||||
// Track conversion
|
||||
this.sendEvent({
|
||||
event: 'conversion',
|
||||
form_type: formType,
|
||||
conversion_id: result.conversionId,
|
||||
page: window.location.pathname,
|
||||
language: document.documentElement.lang || 'pt'
|
||||
});
|
||||
|
||||
// Show success modal or redirect
|
||||
if (result.redirectUrl) {
|
||||
window.location.href = result.redirectUrl;
|
||||
} else {
|
||||
const successModal = new bootstrap.Modal(document.getElementById('successModal'));
|
||||
successModal.show();
|
||||
}
|
||||
|
||||
// Fire conversion pixels
|
||||
if (result.conversionPixels) {
|
||||
result.conversionPixels.forEach(pixel => {
|
||||
this.firePixel(pixel);
|
||||
});
|
||||
}
|
||||
|
||||
// GTM dataLayer push
|
||||
if (window.dataLayer) {
|
||||
window.dataLayer.push({
|
||||
event: 'conversion',
|
||||
form_type: formType,
|
||||
conversion_value: result.value || 1,
|
||||
conversion_currency: 'BRL'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle submission error
|
||||
handleError(error, formType) {
|
||||
console.error('Conversion error:', error);
|
||||
|
||||
// Track error
|
||||
this.sendEvent({
|
||||
event: 'conversion_error',
|
||||
form_type: formType,
|
||||
error: error.message,
|
||||
page: window.location.pathname
|
||||
});
|
||||
|
||||
// Show error message
|
||||
this.showError('Ocorreu um erro ao enviar o formulário. Tente novamente.');
|
||||
}
|
||||
|
||||
// Track form interactions
|
||||
trackFormInteractions() {
|
||||
// Track form start
|
||||
document.querySelectorAll('form input, form textarea, form select').forEach(field => {
|
||||
let hasInteracted = false;
|
||||
|
||||
field.addEventListener('focus', () => {
|
||||
if (!hasInteracted) {
|
||||
hasInteracted = true;
|
||||
this.sendEvent({
|
||||
event: 'form_start',
|
||||
form_id: field.closest('form').id,
|
||||
field_name: field.name,
|
||||
page: window.location.pathname
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Track field completion
|
||||
field.addEventListener('blur', () => {
|
||||
if (field.value.trim()) {
|
||||
this.sendEvent({
|
||||
event: 'form_field_complete',
|
||||
form_id: field.closest('form').id,
|
||||
field_name: field.name,
|
||||
page: window.location.pathname
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Send tracking event
|
||||
async sendEvent(data) {
|
||||
try {
|
||||
await fetch('/api/analytics/track', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Analytics tracking error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fire conversion pixel
|
||||
firePixel(pixelUrl) {
|
||||
try {
|
||||
const img = new Image();
|
||||
img.src = pixelUrl;
|
||||
} catch (error) {
|
||||
console.error('Pixel firing error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Show error message
|
||||
showError(message) {
|
||||
// Create or update error alert
|
||||
let errorAlert = document.getElementById('errorAlert');
|
||||
if (!errorAlert) {
|
||||
errorAlert = document.createElement('div');
|
||||
errorAlert.id = 'errorAlert';
|
||||
errorAlert.className = 'alert alert-danger alert-dismissible fade show position-fixed';
|
||||
errorAlert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; max-width: 400px;';
|
||||
document.body.appendChild(errorAlert);
|
||||
}
|
||||
|
||||
errorAlert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
// Auto-hide after 5 seconds
|
||||
setTimeout(() => {
|
||||
errorAlert.remove();
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll animations and interactions
|
||||
class ScrollAnimations {
|
||||
constructor() {
|
||||
this.setupScrollToTop();
|
||||
this.setupProgressBar();
|
||||
this.setupParallax();
|
||||
}
|
||||
|
||||
setupScrollToTop() {
|
||||
// Create scroll to top button
|
||||
const scrollBtn = document.createElement('button');
|
||||
scrollBtn.innerHTML = '<i class="fas fa-chevron-up"></i>';
|
||||
scrollBtn.className = 'btn btn-primary rounded-circle position-fixed';
|
||||
scrollBtn.style.cssText = 'bottom: 20px; right: 20px; z-index: 1000; width: 50px; height: 50px; opacity: 0; transition: opacity 0.3s;';
|
||||
scrollBtn.id = 'scrollToTop';
|
||||
document.body.appendChild(scrollBtn);
|
||||
|
||||
// Show/hide on scroll
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.pageYOffset > 300) {
|
||||
scrollBtn.style.opacity = '1';
|
||||
} else {
|
||||
scrollBtn.style.opacity = '0';
|
||||
}
|
||||
});
|
||||
|
||||
// Smooth scroll to top
|
||||
scrollBtn.addEventListener('click', () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupProgressBar() {
|
||||
// Create progress bar
|
||||
const progressBar = document.createElement('div');
|
||||
progressBar.className = 'scroll-progress';
|
||||
progressBar.style.cssText = 'position: fixed; top: 0; left: 0; width: 0%; height: 3px; background: linear-gradient(90deg, #667eea, #764ba2); z-index: 9999; transition: width 0.3s;';
|
||||
document.body.appendChild(progressBar);
|
||||
|
||||
// Update progress on scroll
|
||||
window.addEventListener('scroll', () => {
|
||||
const scrolled = (window.pageYOffset / (document.documentElement.scrollHeight - window.innerHeight)) * 100;
|
||||
progressBar.style.width = scrolled + '%';
|
||||
});
|
||||
}
|
||||
|
||||
setupParallax() {
|
||||
// Simple parallax effect for hero section
|
||||
const hero = document.querySelector('.hero-section');
|
||||
if (hero) {
|
||||
window.addEventListener('scroll', () => {
|
||||
const scrolled = window.pageYOffset;
|
||||
const rate = scrolled * -0.5;
|
||||
hero.style.transform = `translateY(${rate}px)`;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new ConversionTracker();
|
||||
new ScrollAnimations();
|
||||
|
||||
// Setup Bootstrap form validation
|
||||
const forms = document.querySelectorAll('.needs-validation');
|
||||
forms.forEach(form => {
|
||||
form.addEventListener('submit', event => {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
form.classList.add('was-validated');
|
||||
});
|
||||
});
|
||||
|
||||
// Phone number formatting
|
||||
const phoneInputs = document.querySelectorAll('input[type="tel"]');
|
||||
phoneInputs.forEach(input => {
|
||||
input.addEventListener('input', (e) => {
|
||||
let value = e.target.value.replace(/\D/g, '');
|
||||
if (value.length >= 10) {
|
||||
value = value.replace(/(\d{2})(\d{4,5})(\d{4})/, '($1) $2-$3');
|
||||
} else if (value.length >= 6) {
|
||||
value = value.replace(/(\d{2})(\d{4})/, '($1) $2');
|
||||
} else if (value.length >= 2) {
|
||||
value = value.replace(/(\d{2})/, '($1) ');
|
||||
}
|
||||
e.target.value = value;
|
||||
});
|
||||
});
|
||||
});
|
||||
4
OnlyOneAccessTemplate/wwwroot/js/site.js
Normal file
4
OnlyOneAccessTemplate/wwwroot/js/site.js
Normal file
@ -0,0 +1,4 @@
|
||||
// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
|
||||
// for details on configuring this project to bundle and minify static web assets.
|
||||
|
||||
// Write your JavaScript code.
|
||||
22
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/LICENSE
Normal file
22
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/LICENSE
Normal file
@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2011-2021 Twitter, Inc.
|
||||
Copyright (c) 2011-2021 The Bootstrap Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
4997
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
vendored
Normal file
4997
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
vendored
Normal file
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4996
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
vendored
Normal file
4996
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
vendored
Normal file
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
427
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
vendored
Normal file
427
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
vendored
Normal file
@ -0,0 +1,427 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
background-color: currentColor;
|
||||
border: 0;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
hr:not([size]) {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-bs-original-title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.2em;
|
||||
background-color: #fcf8e3;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0d6efd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: #0a58ca;
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1em;
|
||||
direction: ltr /* rtl:ignore */;
|
||||
unicode-bidi: bidi-override;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: #d63384;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 0.875em;
|
||||
color: #fff;
|
||||
background-color: #212529;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: #6c757d;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]::-webkit-calendar-picker-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
8
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
vendored
Normal file
8
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
|
||||
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
424
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
vendored
Normal file
424
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
vendored
Normal file
@ -0,0 +1,424 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
background-color: currentColor;
|
||||
border: 0;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
hr:not([size]) {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-bs-original-title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.2em;
|
||||
background-color: #fcf8e3;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0d6efd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: #0a58ca;
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1em;
|
||||
direction: ltr ;
|
||||
unicode-bidi: bidi-override;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: #d63384;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 0.875em;
|
||||
color: #fff;
|
||||
background-color: #212529;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: #6c757d;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]::-webkit-calendar-picker-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
||||
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
8
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
8
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */
|
||||
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4866
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
vendored
Normal file
4866
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
vendored
Normal file
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4857
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
vendored
Normal file
4857
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
11221
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap.css
vendored
Normal file
11221
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
vendored
Normal file
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
11197
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
vendored
Normal file
11197
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
vendored
Normal file
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6780
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
vendored
Normal file
6780
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
vendored
Normal file
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4977
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js
vendored
Normal file
4977
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js
vendored
Normal file
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
5026
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.js
vendored
Normal file
5026
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js
vendored
Normal file
7
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map
vendored
Normal file
1
OnlyOneAccessTemplate/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,23 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) .NET Foundation and Contributors
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
435
OnlyOneAccessTemplate/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js
vendored
Normal file
435
OnlyOneAccessTemplate/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js
vendored
Normal file
@ -0,0 +1,435 @@
|
||||
/**
|
||||
* @license
|
||||
* Unobtrusive validation support library for jQuery and jQuery Validate
|
||||
* Copyright (c) .NET Foundation. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
* @version v4.0.0
|
||||
*/
|
||||
|
||||
/*jslint white: true, browser: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true, strict: false */
|
||||
/*global document: false, jQuery: false */
|
||||
|
||||
(function (factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define("jquery.validate.unobtrusive", ['jquery-validation'], factory);
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
// CommonJS-like environments that support module.exports
|
||||
module.exports = factory(require('jquery-validation'));
|
||||
} else {
|
||||
// Browser global
|
||||
jQuery.validator.unobtrusive = factory(jQuery);
|
||||
}
|
||||
}(function ($) {
|
||||
var $jQval = $.validator,
|
||||
adapters,
|
||||
data_validation = "unobtrusiveValidation";
|
||||
|
||||
function setValidationValues(options, ruleName, value) {
|
||||
options.rules[ruleName] = value;
|
||||
if (options.message) {
|
||||
options.messages[ruleName] = options.message;
|
||||
}
|
||||
}
|
||||
|
||||
function splitAndTrim(value) {
|
||||
return value.replace(/^\s+|\s+$/g, "").split(/\s*,\s*/g);
|
||||
}
|
||||
|
||||
function escapeAttributeValue(value) {
|
||||
// As mentioned on http://api.jquery.com/category/selectors/
|
||||
return value.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g, "\\$1");
|
||||
}
|
||||
|
||||
function getModelPrefix(fieldName) {
|
||||
return fieldName.substr(0, fieldName.lastIndexOf(".") + 1);
|
||||
}
|
||||
|
||||
function appendModelPrefix(value, prefix) {
|
||||
if (value.indexOf("*.") === 0) {
|
||||
value = value.replace("*.", prefix);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function onError(error, inputElement) { // 'this' is the form element
|
||||
var container = $(this).find("[data-valmsg-for='" + escapeAttributeValue(inputElement[0].name) + "']"),
|
||||
replaceAttrValue = container.attr("data-valmsg-replace"),
|
||||
replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) !== false : null;
|
||||
|
||||
container.removeClass("field-validation-valid").addClass("field-validation-error");
|
||||
error.data("unobtrusiveContainer", container);
|
||||
|
||||
if (replace) {
|
||||
container.empty();
|
||||
error.removeClass("input-validation-error").appendTo(container);
|
||||
}
|
||||
else {
|
||||
error.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function onErrors(event, validator) { // 'this' is the form element
|
||||
var container = $(this).find("[data-valmsg-summary=true]"),
|
||||
list = container.find("ul");
|
||||
|
||||
if (list && list.length && validator.errorList.length) {
|
||||
list.empty();
|
||||
container.addClass("validation-summary-errors").removeClass("validation-summary-valid");
|
||||
|
||||
$.each(validator.errorList, function () {
|
||||
$("<li />").html(this.message).appendTo(list);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onSuccess(error) { // 'this' is the form element
|
||||
var container = error.data("unobtrusiveContainer");
|
||||
|
||||
if (container) {
|
||||
var replaceAttrValue = container.attr("data-valmsg-replace"),
|
||||
replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) : null;
|
||||
|
||||
container.addClass("field-validation-valid").removeClass("field-validation-error");
|
||||
error.removeData("unobtrusiveContainer");
|
||||
|
||||
if (replace) {
|
||||
container.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onReset(event) { // 'this' is the form element
|
||||
var $form = $(this),
|
||||
key = '__jquery_unobtrusive_validation_form_reset';
|
||||
if ($form.data(key)) {
|
||||
return;
|
||||
}
|
||||
// Set a flag that indicates we're currently resetting the form.
|
||||
$form.data(key, true);
|
||||
try {
|
||||
$form.data("validator").resetForm();
|
||||
} finally {
|
||||
$form.removeData(key);
|
||||
}
|
||||
|
||||
$form.find(".validation-summary-errors")
|
||||
.addClass("validation-summary-valid")
|
||||
.removeClass("validation-summary-errors");
|
||||
$form.find(".field-validation-error")
|
||||
.addClass("field-validation-valid")
|
||||
.removeClass("field-validation-error")
|
||||
.removeData("unobtrusiveContainer")
|
||||
.find(">*") // If we were using valmsg-replace, get the underlying error
|
||||
.removeData("unobtrusiveContainer");
|
||||
}
|
||||
|
||||
function validationInfo(form) {
|
||||
var $form = $(form),
|
||||
result = $form.data(data_validation),
|
||||
onResetProxy = $.proxy(onReset, form),
|
||||
defaultOptions = $jQval.unobtrusive.options || {},
|
||||
execInContext = function (name, args) {
|
||||
var func = defaultOptions[name];
|
||||
func && $.isFunction(func) && func.apply(form, args);
|
||||
};
|
||||
|
||||
if (!result) {
|
||||
result = {
|
||||
options: { // options structure passed to jQuery Validate's validate() method
|
||||
errorClass: defaultOptions.errorClass || "input-validation-error",
|
||||
errorElement: defaultOptions.errorElement || "span",
|
||||
errorPlacement: function () {
|
||||
onError.apply(form, arguments);
|
||||
execInContext("errorPlacement", arguments);
|
||||
},
|
||||
invalidHandler: function () {
|
||||
onErrors.apply(form, arguments);
|
||||
execInContext("invalidHandler", arguments);
|
||||
},
|
||||
messages: {},
|
||||
rules: {},
|
||||
success: function () {
|
||||
onSuccess.apply(form, arguments);
|
||||
execInContext("success", arguments);
|
||||
}
|
||||
},
|
||||
attachValidation: function () {
|
||||
$form
|
||||
.off("reset." + data_validation, onResetProxy)
|
||||
.on("reset." + data_validation, onResetProxy)
|
||||
.validate(this.options);
|
||||
},
|
||||
validate: function () { // a validation function that is called by unobtrusive Ajax
|
||||
$form.validate();
|
||||
return $form.valid();
|
||||
}
|
||||
};
|
||||
$form.data(data_validation, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
$jQval.unobtrusive = {
|
||||
adapters: [],
|
||||
|
||||
parseElement: function (element, skipAttach) {
|
||||
/// <summary>
|
||||
/// Parses a single HTML element for unobtrusive validation attributes.
|
||||
/// </summary>
|
||||
/// <param name="element" domElement="true">The HTML element to be parsed.</param>
|
||||
/// <param name="skipAttach" type="Boolean">[Optional] true to skip attaching the
|
||||
/// validation to the form. If parsing just this single element, you should specify true.
|
||||
/// If parsing several elements, you should specify false, and manually attach the validation
|
||||
/// to the form when you are finished. The default is false.</param>
|
||||
var $element = $(element),
|
||||
form = $element.parents("form")[0],
|
||||
valInfo, rules, messages;
|
||||
|
||||
if (!form) { // Cannot do client-side validation without a form
|
||||
return;
|
||||
}
|
||||
|
||||
valInfo = validationInfo(form);
|
||||
valInfo.options.rules[element.name] = rules = {};
|
||||
valInfo.options.messages[element.name] = messages = {};
|
||||
|
||||
$.each(this.adapters, function () {
|
||||
var prefix = "data-val-" + this.name,
|
||||
message = $element.attr(prefix),
|
||||
paramValues = {};
|
||||
|
||||
if (message !== undefined) { // Compare against undefined, because an empty message is legal (and falsy)
|
||||
prefix += "-";
|
||||
|
||||
$.each(this.params, function () {
|
||||
paramValues[this] = $element.attr(prefix + this);
|
||||
});
|
||||
|
||||
this.adapt({
|
||||
element: element,
|
||||
form: form,
|
||||
message: message,
|
||||
params: paramValues,
|
||||
rules: rules,
|
||||
messages: messages
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$.extend(rules, { "__dummy__": true });
|
||||
|
||||
if (!skipAttach) {
|
||||
valInfo.attachValidation();
|
||||
}
|
||||
},
|
||||
|
||||
parse: function (selector) {
|
||||
/// <summary>
|
||||
/// Parses all the HTML elements in the specified selector. It looks for input elements decorated
|
||||
/// with the [data-val=true] attribute value and enables validation according to the data-val-*
|
||||
/// attribute values.
|
||||
/// </summary>
|
||||
/// <param name="selector" type="String">Any valid jQuery selector.</param>
|
||||
|
||||
// $forms includes all forms in selector's DOM hierarchy (parent, children and self) that have at least one
|
||||
// element with data-val=true
|
||||
var $selector = $(selector),
|
||||
$forms = $selector.parents()
|
||||
.addBack()
|
||||
.filter("form")
|
||||
.add($selector.find("form"))
|
||||
.has("[data-val=true]");
|
||||
|
||||
$selector.find("[data-val=true]").each(function () {
|
||||
$jQval.unobtrusive.parseElement(this, true);
|
||||
});
|
||||
|
||||
$forms.each(function () {
|
||||
var info = validationInfo(this);
|
||||
if (info) {
|
||||
info.attachValidation();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
adapters = $jQval.unobtrusive.adapters;
|
||||
|
||||
adapters.add = function (adapterName, params, fn) {
|
||||
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation.</summary>
|
||||
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
|
||||
/// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param>
|
||||
/// <param name="params" type="Array" optional="true">[Optional] An array of parameter names (strings) that will
|
||||
/// be extracted from the data-val-nnnn-mmmm HTML attributes (where nnnn is the adapter name, and
|
||||
/// mmmm is the parameter name).</param>
|
||||
/// <param name="fn" type="Function">The function to call, which adapts the values from the HTML
|
||||
/// attributes into jQuery Validate rules and/or messages.</param>
|
||||
/// <returns type="jQuery.validator.unobtrusive.adapters" />
|
||||
if (!fn) { // Called with no params, just a function
|
||||
fn = params;
|
||||
params = [];
|
||||
}
|
||||
this.push({ name: adapterName, params: params, adapt: fn });
|
||||
return this;
|
||||
};
|
||||
|
||||
adapters.addBool = function (adapterName, ruleName) {
|
||||
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where
|
||||
/// the jQuery Validate validation rule has no parameter values.</summary>
|
||||
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
|
||||
/// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param>
|
||||
/// <param name="ruleName" type="String" optional="true">[Optional] The name of the jQuery Validate rule. If not provided, the value
|
||||
/// of adapterName will be used instead.</param>
|
||||
/// <returns type="jQuery.validator.unobtrusive.adapters" />
|
||||
return this.add(adapterName, function (options) {
|
||||
setValidationValues(options, ruleName || adapterName, true);
|
||||
});
|
||||
};
|
||||
|
||||
adapters.addMinMax = function (adapterName, minRuleName, maxRuleName, minMaxRuleName, minAttribute, maxAttribute) {
|
||||
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where
|
||||
/// the jQuery Validate validation has three potential rules (one for min-only, one for max-only, and
|
||||
/// one for min-and-max). The HTML parameters are expected to be named -min and -max.</summary>
|
||||
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
|
||||
/// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param>
|
||||
/// <param name="minRuleName" type="String">The name of the jQuery Validate rule to be used when you only
|
||||
/// have a minimum value.</param>
|
||||
/// <param name="maxRuleName" type="String">The name of the jQuery Validate rule to be used when you only
|
||||
/// have a maximum value.</param>
|
||||
/// <param name="minMaxRuleName" type="String">The name of the jQuery Validate rule to be used when you
|
||||
/// have both a minimum and maximum value.</param>
|
||||
/// <param name="minAttribute" type="String" optional="true">[Optional] The name of the HTML attribute that
|
||||
/// contains the minimum value. The default is "min".</param>
|
||||
/// <param name="maxAttribute" type="String" optional="true">[Optional] The name of the HTML attribute that
|
||||
/// contains the maximum value. The default is "max".</param>
|
||||
/// <returns type="jQuery.validator.unobtrusive.adapters" />
|
||||
return this.add(adapterName, [minAttribute || "min", maxAttribute || "max"], function (options) {
|
||||
var min = options.params.min,
|
||||
max = options.params.max;
|
||||
|
||||
if (min && max) {
|
||||
setValidationValues(options, minMaxRuleName, [min, max]);
|
||||
}
|
||||
else if (min) {
|
||||
setValidationValues(options, minRuleName, min);
|
||||
}
|
||||
else if (max) {
|
||||
setValidationValues(options, maxRuleName, max);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
adapters.addSingleVal = function (adapterName, attribute, ruleName) {
|
||||
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where
|
||||
/// the jQuery Validate validation rule has a single value.</summary>
|
||||
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
|
||||
/// in the data-val-nnnn HTML attribute(where nnnn is the adapter name).</param>
|
||||
/// <param name="attribute" type="String">[Optional] The name of the HTML attribute that contains the value.
|
||||
/// The default is "val".</param>
|
||||
/// <param name="ruleName" type="String" optional="true">[Optional] The name of the jQuery Validate rule. If not provided, the value
|
||||
/// of adapterName will be used instead.</param>
|
||||
/// <returns type="jQuery.validator.unobtrusive.adapters" />
|
||||
return this.add(adapterName, [attribute || "val"], function (options) {
|
||||
setValidationValues(options, ruleName || adapterName, options.params[attribute]);
|
||||
});
|
||||
};
|
||||
|
||||
$jQval.addMethod("__dummy__", function (value, element, params) {
|
||||
return true;
|
||||
});
|
||||
|
||||
$jQval.addMethod("regex", function (value, element, params) {
|
||||
var match;
|
||||
if (this.optional(element)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
match = new RegExp(params).exec(value);
|
||||
return (match && (match.index === 0) && (match[0].length === value.length));
|
||||
});
|
||||
|
||||
$jQval.addMethod("nonalphamin", function (value, element, nonalphamin) {
|
||||
var match;
|
||||
if (nonalphamin) {
|
||||
match = value.match(/\W/g);
|
||||
match = match && match.length >= nonalphamin;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
|
||||
if ($jQval.methods.extension) {
|
||||
adapters.addSingleVal("accept", "mimtype");
|
||||
adapters.addSingleVal("extension", "extension");
|
||||
} else {
|
||||
// for backward compatibility, when the 'extension' validation method does not exist, such as with versions
|
||||
// of JQuery Validation plugin prior to 1.10, we should use the 'accept' method for
|
||||
// validating the extension, and ignore mime-type validations as they are not supported.
|
||||
adapters.addSingleVal("extension", "extension", "accept");
|
||||
}
|
||||
|
||||
adapters.addSingleVal("regex", "pattern");
|
||||
adapters.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url");
|
||||
adapters.addMinMax("length", "minlength", "maxlength", "rangelength").addMinMax("range", "min", "max", "range");
|
||||
adapters.addMinMax("minlength", "minlength").addMinMax("maxlength", "minlength", "maxlength");
|
||||
adapters.add("equalto", ["other"], function (options) {
|
||||
var prefix = getModelPrefix(options.element.name),
|
||||
other = options.params.other,
|
||||
fullOtherName = appendModelPrefix(other, prefix),
|
||||
element = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(fullOtherName) + "']")[0];
|
||||
|
||||
setValidationValues(options, "equalTo", element);
|
||||
});
|
||||
adapters.add("required", function (options) {
|
||||
// jQuery Validate equates "required" with "mandatory" for checkbox elements
|
||||
if (options.element.tagName.toUpperCase() !== "INPUT" || options.element.type.toUpperCase() !== "CHECKBOX") {
|
||||
setValidationValues(options, "required", true);
|
||||
}
|
||||
});
|
||||
adapters.add("remote", ["url", "type", "additionalfields"], function (options) {
|
||||
var value = {
|
||||
url: options.params.url,
|
||||
type: options.params.type || "GET",
|
||||
data: {}
|
||||
},
|
||||
prefix = getModelPrefix(options.element.name);
|
||||
|
||||
$.each(splitAndTrim(options.params.additionalfields || options.element.name), function (i, fieldName) {
|
||||
var paramName = appendModelPrefix(fieldName, prefix);
|
||||
value.data[paramName] = function () {
|
||||
var field = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(paramName) + "']");
|
||||
// For checkboxes and radio buttons, only pick up values from checked fields.
|
||||
if (field.is(":checkbox")) {
|
||||
return field.filter(":checked").val() || field.filter(":hidden").val() || '';
|
||||
}
|
||||
else if (field.is(":radio")) {
|
||||
return field.filter(":checked").val() || '';
|
||||
}
|
||||
return field.val();
|
||||
};
|
||||
});
|
||||
|
||||
setValidationValues(options, "remote", value);
|
||||
});
|
||||
adapters.add("password", ["min", "nonalphamin", "regex"], function (options) {
|
||||
if (options.params.min) {
|
||||
setValidationValues(options, "minlength", options.params.min);
|
||||
}
|
||||
if (options.params.nonalphamin) {
|
||||
setValidationValues(options, "nonalphamin", options.params.nonalphamin);
|
||||
}
|
||||
if (options.params.regex) {
|
||||
setValidationValues(options, "regex", options.params.regex);
|
||||
}
|
||||
});
|
||||
adapters.add("fileextensions", ["extensions"], function (options) {
|
||||
setValidationValues(options, "extension", options.params.extensions);
|
||||
});
|
||||
|
||||
$(function () {
|
||||
$jQval.unobtrusive.parse(document);
|
||||
});
|
||||
|
||||
return $jQval.unobtrusive;
|
||||
}));
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
=====================
|
||||
|
||||
Copyright Jörn Zaefferer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
1512
OnlyOneAccessTemplate/wwwroot/lib/jquery-validation/dist/additional-methods.js
vendored
Normal file
1512
OnlyOneAccessTemplate/wwwroot/lib/jquery-validation/dist/additional-methods.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4
OnlyOneAccessTemplate/wwwroot/lib/jquery-validation/dist/additional-methods.min.js
vendored
Normal file
4
OnlyOneAccessTemplate/wwwroot/lib/jquery-validation/dist/additional-methods.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user