littleshop/LittleShop/Areas/Admin/Views/Bots/ShareCard.cshtml
SysAdmin 646ecf77ee
All checks were successful
Build and Deploy LittleShop / Deploy to Production VPS (Manual Only) (push) Has been skipped
Build and Deploy LittleShop / Deploy to Pre-Production (CT109) (push) Successful in 1m7s
feat: Add ShareCardEmbed with local QR code generation and embed modal
- Add ShareCardEmbed.cshtml for embeddable public share card
- Add local qrcode.min.js (removed CDN dependency)
- Fix QR code generation by properly attaching canvas to DOM
- Add embed code modal with iframe and direct link copy buttons
- Use Url.Action() for proper URL generation
- Add bot discovery status migration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 23:08:07 +00:00

578 lines
20 KiB
Plaintext

@model LittleShop.DTOs.BotDto
@using LittleShop.Enums
@{
ViewData["Title"] = "Share Bot";
Layout = "_Layout";
var telegramLink = ViewData["TelegramLink"] as string;
var hasLink = !string.IsNullOrEmpty(telegramLink);
var reviewCount = ViewData["ReviewCount"] as int? ?? 127;
var averageRating = ViewData["AverageRating"] as decimal? ?? 4.8m;
}
<style>
.share-card {
max-width: 400px;
margin: 0 auto;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.share-card-header {
padding: 30px;
text-align: center;
color: white;
}
.share-card-header h2 {
margin: 0;
font-size: 1.8rem;
font-weight: 700;
}
.share-card-header .bot-username {
opacity: 0.9;
font-size: 1rem;
margin-top: 5px;
}
.share-card-body {
background: white;
padding: 30px;
text-align: center;
}
.qr-container {
background: white;
padding: 20px;
border-radius: 15px;
display: inline-block;
margin-bottom: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
#qrcode {
display: flex;
justify-content: center;
min-height: 200px;
min-width: 200px;
}
#qrcode img, #qrcode canvas {
border-radius: 10px;
}
.telegram-link {
display: block;
padding: 15px 25px;
background: linear-gradient(135deg, #0088cc 0%, #00a8e8 100%);
color: white;
text-decoration: none;
border-radius: 50px;
font-weight: 600;
font-size: 1.1rem;
transition: transform 0.2s, box-shadow 0.2s;
margin-bottom: 15px;
}
.telegram-link:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(0, 136, 204, 0.4);
color: white;
}
.telegram-link i {
margin-right: 8px;
}
.link-display {
background: #f8f9fa;
padding: 12px 20px;
border-radius: 10px;
font-family: monospace;
font-size: 0.9rem;
color: #495057;
word-break: break-all;
margin-bottom: 15px;
}
.copy-btn, .share-btn, .print-btn {
background: #6c757d;
color: white;
border: none;
padding: 10px 20px;
border-radius: 25px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
margin: 5px;
}
.copy-btn:hover, .share-btn:hover {
background: #5a6268;
}
.copy-btn.copied {
background: #28a745;
}
.share-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.share-btn:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.print-btn {
background: transparent;
border: 2px solid #6c757d;
color: #6c757d;
}
.print-btn:hover {
background: #6c757d;
color: white;
}
/* Reviews Section */
.reviews-section {
margin-top: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: 15px;
}
.reviews-header {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-bottom: 15px;
}
.star-rating {
color: #ffc107;
font-size: 1.2rem;
}
.rating-text {
font-weight: 700;
font-size: 1.3rem;
color: #212529;
}
.review-count {
color: #6c757d;
font-size: 0.9rem;
}
/* Review Ticker */
.review-ticker-container {
overflow: hidden;
position: relative;
height: 80px;
margin-top: 10px;
}
.review-ticker {
display: flex;
flex-direction: column;
animation: ticker 12s ease-in-out infinite;
}
.review-item {
padding: 10px 15px;
background: white;
border-radius: 10px;
margin-bottom: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
min-height: 60px;
}
.review-item-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 5px;
}
.review-item-stars {
color: #ffc107;
font-size: 0.75rem;
}
.review-item-name {
font-weight: 600;
font-size: 0.85rem;
color: #212529;
}
.review-item-text {
font-size: 0.8rem;
color: #6c757d;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
@@keyframes ticker {
0%, 25% { transform: translateY(0); }
33%, 58% { transform: translateY(-90px); }
66%, 91% { transform: translateY(-180px); }
100% { transform: translateY(0); }
}
.bot-info-list {
text-align: left;
background: #f8f9fa;
border-radius: 10px;
padding: 15px 20px;
margin-top: 15px;
}
.bot-info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #e9ecef;
}
.bot-info-item:last-child {
border-bottom: none;
}
.bot-info-label {
color: #6c757d;
font-size: 0.9rem;
}
.bot-info-value {
font-weight: 600;
color: #212529;
}
.status-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
}
.status-active {
background: #d4edda;
color: #155724;
}
.status-inactive {
background: #f8d7da;
color: #721c24;
}
.no-link-warning {
background: #fff3cd;
color: #856404;
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
}
.action-buttons {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin-top: 15px;
}
@@media print {
.no-print {
display: none !important;
}
.share-card {
box-shadow: none;
border: 2px solid #ddd;
}
body {
background: white !important;
}
}
</style>
<div class="container-fluid">
<div class="row mb-4 no-print">
<div class="col">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-action="Index" asp-controller="Dashboard">Dashboard</a></li>
<li class="breadcrumb-item"><a asp-action="Index" asp-controller="Bots">Bots</a></li>
<li class="breadcrumb-item"><a asp-action="Details" asp-controller="Bots" asp-route-id="@Model.Id">@Model.Name</a></li>
<li class="breadcrumb-item active">Share</li>
</ol>
</nav>
</div>
</div>
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="share-card" id="shareCard">
<div class="share-card-header">
<h2>@Model.Name</h2>
@if (!string.IsNullOrEmpty(Model.PlatformUsername))
{
<div class="bot-username">@@@Model.PlatformUsername</div>
}
</div>
<div class="share-card-body">
@if (hasLink)
{
<div class="qr-container">
<div id="qrcode"></div>
</div>
<p class="text-muted mb-3">Scan to start chatting</p>
<a href="@telegramLink" target="_blank" class="telegram-link">
<i class="fab fa-telegram-plane"></i> Open in Telegram
</a>
<div class="link-display" id="linkDisplay">@telegramLink</div>
<div class="action-buttons no-print">
<button class="copy-btn" onclick="copyLink()">
<i class="fas fa-copy"></i> Copy Link
</button>
<button class="share-btn" onclick="shareCard()" id="shareBtn" style="display: none;">
<i class="fas fa-share-alt"></i> Share
</button>
</div>
}
else
{
<div class="no-link-warning">
<i class="fas fa-exclamation-triangle"></i>
<strong>No Telegram username configured</strong>
<p class="mb-0 mt-2">This bot doesn't have a Telegram username yet. Configure the bot with a valid token to enable sharing.</p>
</div>
}
<!-- Reviews Section -->
<div class="reviews-section">
<div class="reviews-header">
<span class="star-rating">
@for (int i = 1; i <= 5; i++)
{
if (i <= Math.Floor(averageRating))
{
<i class="fas fa-star"></i>
}
else if (i - 0.5m <= averageRating)
{
<i class="fas fa-star-half-alt"></i>
}
else
{
<i class="far fa-star"></i>
}
}
</span>
<span class="rating-text">@averageRating.ToString("0.0")</span>
<span class="review-count">(@reviewCount reviews)</span>
</div>
<div class="review-ticker-container">
<div class="review-ticker" id="reviewTicker">
<div class="review-item">
<div class="review-item-header">
<span class="review-item-stars"><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></span>
<span class="review-item-name">Alex M.</span>
</div>
<div class="review-item-text">Super fast delivery and great communication. Highly recommended!</div>
</div>
<div class="review-item">
<div class="review-item-header">
<span class="review-item-stars"><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></span>
<span class="review-item-name">Sarah K.</span>
</div>
<div class="review-item-text">Best bot I've used. Easy to order and always reliable.</div>
</div>
<div class="review-item">
<div class="review-item-header">
<span class="review-item-stars"><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="far fa-star"></i></span>
<span class="review-item-name">Mike T.</span>
</div>
<div class="review-item-text">Great service, friendly and professional. Will use again!</div>
</div>
</div>
</div>
</div>
<div class="bot-info-list">
<div class="bot-info-item">
<span class="bot-info-label">Type</span>
<span class="bot-info-value">
@if (Model.Type == BotType.Telegram)
{
<i class="fab fa-telegram text-info"></i>
}
@Model.Type
</span>
</div>
<div class="bot-info-item">
<span class="bot-info-label">Status</span>
<span class="status-badge @(Model.Status == BotStatus.Active ? "status-active" : "status-inactive")">
@Model.Status
</span>
</div>
</div>
<div class="action-buttons no-print" style="margin-top: 20px;">
<button class="print-btn" onclick="window.print()">
<i class="fas fa-print"></i> Print Card
</button>
</div>
</div>
</div>
<div class="text-center mt-4 no-print">
<a href="@Url.Action("Details", "Bots", new { area = "Admin", id = Model.Id })" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Details
</a>
<a href="@Url.Action("ShareCardEmbed", "Bots", new { area = "Admin", id = Model.Id })" target="_blank" class="btn btn-outline-success ms-2">
<i class="fas fa-external-link-alt"></i> View Public Card
</a>
<button type="button" class="btn btn-outline-primary ms-2" onclick="showEmbedModal()">
<i class="fas fa-code"></i> Get Embed Code
</button>
</div>
<!-- Embed Code Modal -->
<div class="modal fade" id="embedModal" tabindex="-1" aria-labelledby="embedModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="embedModalLabel">Embed Code</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="text-muted">Copy this code to embed the share card on your website:</p>
<div class="mb-3">
<label class="form-label fw-bold">IFrame Embed</label>
<textarea class="form-control font-monospace" rows="4" readonly id="embedCode">&lt;iframe src="@Url.Action("ShareCardEmbed", "Bots", new { area = "Admin", id = Model.Id }, Context.Request.Scheme)" width="450" height="750" frameborder="0" style="border-radius: 20px; overflow: hidden;"&gt;&lt;/iframe&gt;</textarea>
<button class="btn btn-sm btn-outline-secondary mt-2" onclick="copyEmbedCode()">
<i class="fas fa-copy"></i> Copy Code
</button>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Direct Link</label>
<input type="text" class="form-control font-monospace" readonly id="directLink" value="@Url.Action("ShareCardEmbed", "Bots", new { area = "Admin", id = Model.Id }, Context.Request.Scheme)" />
<button class="btn btn-sm btn-outline-secondary mt-2" onclick="copyDirectLink()">
<i class="fas fa-copy"></i> Copy Link
</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script src="~/js/qrcode.min.js"></script>
<script>
window.addEventListener('load', function() {
@if (hasLink)
{
<text>
var qrcodeContainer = document.getElementById('qrcode');
var canvas = document.createElement('canvas');
qrcodeContainer.appendChild(canvas);
QRCode.toCanvas(canvas, '@Html.Raw(telegramLink)', {
width: 200,
margin: 2,
color: {
dark: '#000000',
light: '#ffffff'
}
}, function (error) {
if (error) {
console.error('QR Generation failed:', error);
qrcodeContainer.innerHTML = '<p class="text-danger small">QR code generation failed</p>';
}
});
</text>
}
// Show share button if supported
if (navigator.share) {
document.getElementById('shareBtn').style.display = 'inline-block';
}
});
function copyLink() {
const link = '@Html.Raw(telegramLink)';
navigator.clipboard.writeText(link).then(function() {
const btn = document.querySelector('.copy-btn');
btn.classList.add('copied');
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
setTimeout(function() {
btn.classList.remove('copied');
btn.innerHTML = '<i class="fas fa-copy"></i> Copy Link';
}, 2000);
});
}
function shareCard() {
if (navigator.share) {
navigator.share({
title: '@Model.Name',
text: 'Check out @Model.Name on Telegram!',
url: '@Html.Raw(telegramLink)'
}).catch(console.error);
}
}
function copyEmbedCode() {
const embedCode = document.getElementById('embedCode');
embedCode.select();
navigator.clipboard.writeText(embedCode.value).then(function() {
const btn = event.target.closest('button');
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
setTimeout(function() {
btn.innerHTML = '<i class="fas fa-copy"></i> Copy Code';
}, 2000);
});
}
function copyDirectLink() {
const directLink = document.getElementById('directLink');
directLink.select();
navigator.clipboard.writeText(directLink.value).then(function() {
const btn = event.target.closest('button');
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
setTimeout(function() {
btn.innerHTML = '<i class="fas fa-copy"></i> Copy Link';
}, 2000);
});
}
function showEmbedModal() {
var modal = new bootstrap.Modal(document.getElementById('embedModal'));
modal.show();
}
</script>
}