From 5a90dc157045b9645a77dfadb9bbaae13b25a617 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Tue, 21 Oct 2025 21:35:59 -0300 Subject: [PATCH] feat: tawk.to --- Views/Shared/_Layout.cshtml | 48 ++++ Views/Shared/_TelegramPremiumFab.cshtml | 94 +++++++ wwwroot/css/telegram-fab.css | 80 ++++++ wwwroot/js/telegram-fab.js | 327 ++++++++++++++++++++++++ 4 files changed, 549 insertions(+) create mode 100644 Views/Shared/_TelegramPremiumFab.cshtml create mode 100644 wwwroot/css/telegram-fab.css create mode 100644 wwwroot/js/telegram-fab.js diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index 5b87687..74e945c 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -7,6 +7,23 @@ @{ var isDevelopment = HostEnvironment?.IsDevelopment() ?? false; + var isPremiumUser = false; + + if (User?.Identity?.IsAuthenticated == true) + { + var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + if (!string.IsNullOrWhiteSpace(userId)) + { + try + { + isPremiumUser = await AdService.HasValidPremiumSubscription(userId); + } + catch + { + isPremiumUser = false; + } + } + } } @@ -390,6 +407,37 @@ @await Html.PartialAsync("_CookieConsent") + + + diff --git a/Views/Shared/_TelegramPremiumFab.cshtml b/Views/Shared/_TelegramPremiumFab.cshtml new file mode 100644 index 0000000..913d322 --- /dev/null +++ b/Views/Shared/_TelegramPremiumFab.cshtml @@ -0,0 +1,94 @@ +@using System.Globalization +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer +@inject Microsoft.Extensions.Configuration.IConfiguration Configuration + +@{ + var culture = CultureInfo.CurrentUICulture; + var telegramLang = culture.Name.Replace('-', '_'); + var botUsername = Configuration["Telegram:LoginWidgetBotUsername"] ?? string.Empty; + var botId = Configuration["Telegram:BotId"] ?? string.Empty; + var requestAccess = Configuration["Telegram:LoginWidgetRequestAccess"] ?? "write"; +} + +
+ + +
+
+
@Localizer["TelegramPremiumTitle"]
+ +
+
+ + +
+ + @Localizer["TelegramPremiumStatusLoading"] +
+ +
+

@Html.Raw(Localizer["TelegramPremiumStatusConnected", ""])

+
+ + +
+
+ +
+

@Localizer["TelegramPremiumLoginHint"]

+
+
+
+ + +
+
+
+
+ +
+
+
+
+ +
+ +
+
diff --git a/wwwroot/css/telegram-fab.css b/wwwroot/css/telegram-fab.css new file mode 100644 index 0000000..051aefa --- /dev/null +++ b/wwwroot/css/telegram-fab.css @@ -0,0 +1,80 @@ +.fab-telegram { + position: fixed; + bottom: var(--telegram-fab-offset, 1.5rem); + right: var(--telegram-fab-offset, 1.5rem); + width: 3rem; + height: 3rem; + border-radius: 50%; + box-shadow: 0 0.75rem 1.5rem rgba(0, 0, 0, 0.18); + z-index: 1052; + opacity: 0.95; + backdrop-filter: blur(2px); + transition: transform 0.2s ease, opacity 0.2s ease; +} + +.fab-telegram:hover, +.fab-telegram:focus-visible { + opacity: 1; + transform: translateY(-2px); +} + +.fab-telegram:focus-visible { + outline: 2px solid rgba(0, 123, 255, 0.6); + outline-offset: 2px; +} + +@media (max-width: 575.98px) { + .fab-telegram { + width: 2.75rem; + height: 2.75rem; + bottom: 1rem; + right: 1rem; + } +} + +.telegram-fab-root .toast-container { + z-index: 1080; +} + +.telegram-fab-loading .spinner-border { + width: 1.5rem; + height: 1.5rem; +} + +.telegram-fab-widget-placeholder { + min-height: 90px; + display: flex; + align-items: center; + justify-content: center; +} + +.telegram-fab-widget-skeleton .placeholder { + display: inline-block; + height: 0.875rem; +} + +.telegram-fab-widget-skeleton .placeholder.col-8 { + width: 70%; +} + +.telegram-fab-widget-skeleton .placeholder.col-6 { + width: 55%; +} + +.offcanvas-sm-bottom { + --bs-offcanvas-width: min(420px, 90vw); +} + +@media (max-width: 767.98px) { + .offcanvas-sm-bottom { + --bs-offcanvas-width: 100%; + --bs-offcanvas-height: min(80vh, 420px); + --bs-offcanvas-transform: translateY(100%); + border-radius: 1rem 1rem 0 0; + width: 100%; + top: auto; + bottom: 0; + left: 0; + right: 0; + } +} diff --git a/wwwroot/js/telegram-fab.js b/wwwroot/js/telegram-fab.js new file mode 100644 index 0000000..a909535 --- /dev/null +++ b/wwwroot/js/telegram-fab.js @@ -0,0 +1,327 @@ +(function () { + function initTelegramFab() { + const root = document.getElementById('telegramFabRoot'); + if (!root || root.dataset.initialized === 'true') { + return; + } + + root.dataset.initialized = 'true'; + + const dataset = root.dataset; + const offcanvasElement = document.getElementById('telegramPremiumOffcanvas'); + const loadingSection = document.getElementById('telegramFabLoading'); + const connectedSection = document.getElementById('telegramFabConnected'); + const disconnectedSection = document.getElementById('telegramFabDisconnected'); + const alertElement = document.getElementById('telegramFabAlert'); + const retryButton = root.querySelector('.telegram-fab-retry'); + const openButton = root.querySelector('.telegram-fab-open'); + const unlinkButton = root.querySelector('.telegram-fab-unlink'); + const usernameElement = root.querySelector('.telegram-fab-username'); + const widgetPlaceholder = document.getElementById('telegramLoginWidgetPlaceholder'); + const toastContainer = document.getElementById('telegramFabToasts'); + const fabButton = document.getElementById('telegramPremiumFab'); + + if (!offcanvasElement || !loadingSection || !connectedSection || !disconnectedSection) { + console.warn('[Telegram FAB] Missing required DOM nodes. Aborting initialization.'); + return; + } + + const offcanvas = bootstrap.Offcanvas.getOrCreateInstance(offcanvasElement); + let currentStatus = null; + let currentDeepLink = null; + let widgetMounted = false; + + const messages = { + loading: dataset.statusLoading || 'Loading...', + error: dataset.statusError || 'Something went wrong.', + connectedTemplate: dataset.statusConnected || 'Connected as {0}', + disconnected: dataset.statusDisconnected || '', + linkSuccess: dataset.linkSuccess || 'Linked successfully.', + unlinkSuccess: dataset.unlinkSuccess || 'Unlinked successfully.', + linkError: dataset.linkError || 'Unable to link. Try again.', + unlinkError: dataset.unlinkError || 'Unable to unlink. Try again.', + retry: dataset.retryLabel || 'Try again', + open: dataset.openLabel || 'Open', + unlink: dataset.unlinkLabel || 'Unlink' + }; + + function toggleSections(state) { + loadingSection.classList.toggle('d-none', state !== 'loading'); + connectedSection.classList.toggle('d-none', state !== 'connected'); + disconnectedSection.classList.toggle('d-none', state !== 'disconnected'); + } + + function showAlert(message) { + if (!alertElement) { + return; + } + + alertElement.textContent = message; + alertElement.classList.remove('d-none'); + } + + function hideAlert() { + if (!alertElement) { + return; + } + + alertElement.classList.add('d-none'); + alertElement.textContent = ''; + } + + function showToast(message, type) { + if (!toastContainer || !message) { + return; + } + + const palette = { + success: 'text-bg-success', + error: 'text-bg-danger', + info: 'text-bg-info' + }; + const toast = document.createElement('div'); + toast.className = `toast align-items-center ${palette[type] || palette.info} border-0`; + toast.role = 'status'; + toast.setAttribute('aria-live', 'polite'); + toast.innerHTML = [ + '
', + `
${message}
`, + '', + '
' + ].join(''); + + toastContainer.appendChild(toast); + + const toastInstance = bootstrap.Toast.getOrCreateInstance(toast, { + delay: 4000, + autohide: true + }); + + toast.addEventListener('hidden.bs.toast', function () { + toast.remove(); + }); + + toastInstance.show(); + } + + function updateConnectedView(username, deepLink) { + toggleSections('connected'); + hideAlert(); + + if (usernameElement) { + usernameElement.textContent = username || ''; + } + + currentDeepLink = deepLink || null; + } + + function updateDisconnectedView() { + toggleSections('disconnected'); + hideAlert(); + + if (!widgetMounted) { + mountLoginWidget(); + } + } + + function applyErrorState(message) { + toggleSections('disconnected'); + showAlert(message || messages.error); + + if (retryButton) { + retryButton.classList.remove('d-none'); + } + } + + async function fetchStatus() { + toggleSections('loading'); + hideAlert(); + + try { + const response = await fetch('/telegram/status', { + method: 'GET', + credentials: 'same-origin', + headers: { + 'Accept': 'application/json' + } + }); + + if (response.status === 401 || response.status === 403) { + if (fabButton) { + fabButton.setAttribute('hidden', 'hidden'); + fabButton.setAttribute('tabindex', '-1'); + } + root.setAttribute('aria-hidden', 'true'); + root.style.display = 'none'; + return; + } + + if (!response.ok) { + throw new Error('status_request_failed'); + } + + currentStatus = await response.json(); + + if (currentStatus && currentStatus.connected) { + updateConnectedView(currentStatus.username || '', currentStatus.deepLink || null); + } else { + updateDisconnectedView(); + } + } catch (error) { + console.warn('[Telegram FAB] Status request failed:', error); + applyErrorState(messages.error); + } + } + + function mountLoginWidget() { + const botUsernameRaw = (dataset.botUsername || '').trim(); + + if (!botUsernameRaw) { + console.warn('[Telegram FAB] Telegram bot username missing. Configure Telegram:LoginWidgetBotUsername.'); + applyErrorState(messages.error); + return; + } + + const botUsername = botUsernameRaw.startsWith('@') + ? botUsernameRaw.slice(1) + : botUsernameRaw; + + if (!widgetPlaceholder) { + return; + } + + widgetPlaceholder.innerHTML = ''; + + const widgetScript = document.createElement('script'); + widgetScript.src = 'https://telegram.org/js/telegram-widget.js?22'; + widgetScript.async = true; + widgetScript.dataset.telegramLogin = botUsername; + widgetScript.dataset.size = 'large'; + widgetScript.dataset.userpic = 'false'; + widgetScript.dataset.requestAccess = dataset.requestAccess || 'write'; + + const lang = (dataset.telegramLang || 'en').replace('-', '_'); + widgetScript.dataset.lang = lang; + + widgetScript.dataset.onauth = 'telegramFabOnAuth'; + + widgetPlaceholder.appendChild(widgetScript); + widgetMounted = true; + } + + async function linkTelegram(payload) { + toggleSections('loading'); + hideAlert(); + + try { + const response = await fetch('/telegram/link', { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + throw new Error('link_failed'); + } + + showToast(messages.linkSuccess, 'success'); + await fetchStatus(); + } catch (error) { + console.error('[Telegram FAB] Link failed:', error); + showToast(messages.linkError, 'error'); + applyErrorState(messages.linkError); + } + } + + async function unlinkTelegram() { + toggleSections('loading'); + hideAlert(); + + try { + const response = await fetch('/telegram/unlink', { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('unlink_failed'); + } + + showToast(messages.unlinkSuccess, 'success'); + widgetMounted = false; + await fetchStatus(); + } catch (error) { + console.error('[Telegram FAB] Unlink failed:', error); + showToast(messages.unlinkError, 'error'); + toggleSections('connected'); + showAlert(messages.unlinkError); + } + } + + offcanvasElement.addEventListener('show.bs.offcanvas', function () { + fetchStatus(); + }); + + if (retryButton) { + retryButton.addEventListener('click', function () { + hideAlert(); + fetchStatus(); + }); + } + + if (openButton) { + openButton.addEventListener('click', function () { + if (currentDeepLink) { + window.open(currentDeepLink, '_blank', 'noopener'); + } else { + fetchStatus(); + } + }); + } + + if (unlinkButton) { + unlinkButton.addEventListener('click', function () { + unlinkTelegram(); + }); + } + + window.telegramFabOnAuth = function (user) { + if (!user) { + showToast(messages.linkError, 'error'); + return; + } + + const payload = { + id: user.id, + hash: user.hash, + first_name: user.first_name, + last_name: user.last_name, + username: user.username, + photo_url: user.photo_url, + auth_date: user.auth_date, + bot_id: dataset.botId || null + }; + + linkTelegram(payload); + }; + + if (fabButton) { + fabButton.addEventListener('keydown', function (event) { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + offcanvas.show(); + } + }); + } + } + + document.addEventListener('DOMContentLoaded', initTelegramFab); + window.initTelegramFab = initTelegramFab; +})();