virtual guarded network page added.

This commit is contained in:
birkan 2025-10-07 17:59:13 +03:00
parent 5657457e17
commit 785b783034
14 changed files with 1186 additions and 39 deletions

BIN
src/assets/securelink1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
src/assets/securelink2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

BIN
src/assets/securelink3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -66,7 +66,7 @@
height: 100%;
background-color: #0b0d10;
display: flex;
align-items: center;
align-items: flex-start;
justify-content: center;
overflow: hidden;
padding: 20px;
@ -80,17 +80,20 @@
.attack-surface-modal-image-wrapper {
position: relative;
max-width: 95vw;
max-height: 95vh;
width: fit-content;
height: fit-content;
max-height: calc(100vh - 100px);
width: auto;
height: auto;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
}
.attack-surface-modal-image {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
max-width: 95vw;
max-height: calc(100vh - 100px);
object-fit: contain;
display: block;
}
@ -166,7 +169,7 @@
background-color: rgba(0, 0, 0, 0.3);
display: flex;
gap: 2px;
z-index: 1001;
z-index: 1002;
backdrop-filter: blur(10px);
animation: attackSurfaceProgressBarFadeIn 250ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@ -230,7 +233,7 @@
font-size: 14px;
font-weight: 600;
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
z-index: 10;
z-index: 1003;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

View File

@ -118,10 +118,13 @@ export default function AttackSurfaceTour({ autoStart = false, onClose }: Attack
if (currentStep < attackSurfaceSteps.length - 1) {
setTransitionDirection('next')
setIsTransitioning(true)
// Allow slide-out, then change step, keep transitioning for a tick to slide-in
setTimeout(() => {
setCurrentStep(currentStep + 1)
setIsTransitioning(false)
}, 500)
setTimeout(() => {
setIsTransitioning(false)
}, 50)
}, 300)
}
}
@ -131,8 +134,10 @@ export default function AttackSurfaceTour({ autoStart = false, onClose }: Attack
setIsTransitioning(true)
setTimeout(() => {
setCurrentStep(currentStep - 1)
setIsTransitioning(false)
}, 500)
setTimeout(() => {
setIsTransitioning(false)
}, 50)
}, 300)
}
}
@ -142,8 +147,10 @@ export default function AttackSurfaceTour({ autoStart = false, onClose }: Attack
setIsTransitioning(true)
setTimeout(() => {
setCurrentStep(stepIndex)
setIsTransitioning(false)
}, 500)
setTimeout(() => {
setIsTransitioning(false)
}, 50)
}, 300)
}
}

View File

@ -273,7 +273,7 @@
height: 100%;
background-color: #0b0d10;
display: flex;
align-items: center;
align-items: flex-start;
justify-content: center;
overflow: hidden;
animation: modalContentSlideIn 300ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
@ -290,17 +290,20 @@
.modal-image-wrapper {
position: relative;
max-width: 95vw;
max-height: 95vh;
width: fit-content;
height: fit-content;
max-height: calc(100vh - 100px);
width: auto;
height: auto;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
}
.modal-image {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
max-width: 95vw;
max-height: calc(100vh - 100px);
object-fit: contain;
display: block;
}
@ -529,7 +532,7 @@
background-color: rgba(0, 0, 0, 0.3);
display: flex;
gap: 2px;
z-index: 1001;
z-index: 1002;
backdrop-filter: blur(10px);
animation: progressBarFadeIn 250ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@ -594,7 +597,7 @@
font-size: 14px;
font-weight: 600;
transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
z-index: 10;
z-index: 1003;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

View File

@ -2,8 +2,12 @@ import { useState } from 'react'
import dashboardImg from '../assets/dashboard.jpeg'
import guardpotImg from '../assets/guardpot.jpeg'
import attackSurfaceImg from '../assets/attacksurface1.png'
import secureLinkImg from '../assets/securelink1.png'
import vgnImg from '../assets/virtualguardednetwork1.png'
import GuardpotTour from './GuardpotTour'
import AttackSurfaceTour from './AttackSurfaceTour'
import SecureLinkTour from './SecureLinkTour'
import VirtualGuardedNetworkTour from './VirtualGuardedNetworkTour'
import './DashboardTour.css'
type TourStep = {
@ -38,12 +42,13 @@ const dashboardSteps: TourStep[] = [
},
]
const tourOptions = [
type TourOption = { id: string; name: string; description: string; disabled?: boolean }
const tourOptions: TourOption[] = [
{ id: 'dashboard', name: 'Dashboard', description: 'Dashboard overview and features' },
{ id: 'guardpot', name: 'Guardpot', description: 'Guardpot management system' },
{ id: 'attack-surface', name: 'Attack Surface', description: 'Attack surface analysis' },
{ id: 'secure-link', name: 'Secure Link', description: 'Secure link management', disabled: true },
{ id: 'virtual-guarded-network', name: 'Virtual Guarded Network', description: 'Virtual network configuration', disabled: true }
{ id: 'secure-link', name: 'Secure Link', description: 'Secure link management' },
{ id: 'virtual-guarded-network', name: 'Virtual Guarded Network', description: 'Virtual network configuration' }
]
export default function DashboardTour() {
@ -145,12 +150,16 @@ export default function DashboardTour() {
selectedTour === 'dashboard' ? dashboardImg :
selectedTour === 'guardpot' ? guardpotImg :
selectedTour === 'attack-surface' ? attackSurfaceImg :
selectedTour === 'secure-link' ? secureLinkImg :
selectedTour === 'virtual-guarded-network' ? vgnImg :
dashboardImg
}
alt={
selectedTour === 'dashboard' ? 'Guardpot Dashboard' :
selectedTour === 'guardpot' ? 'Guardpot Management' :
selectedTour === 'attack-surface' ? 'Attack Surface' :
selectedTour === 'secure-link' ? 'Secure Link' :
selectedTour === 'virtual-guarded-network' ? 'Virtual Guarded Network' :
'Tour Preview'
}
/>
@ -160,6 +169,8 @@ export default function DashboardTour() {
{selectedTour === 'dashboard' && 'Start Dashboard Tour'}
{selectedTour === 'guardpot' && 'Start Guardpot Tour'}
{selectedTour === 'attack-surface' && 'Start Attack Surface Tour'}
{selectedTour === 'secure-link' && 'Start Secure Link Tour'}
{selectedTour === 'virtual-guarded-network' && 'Start VGN Tour'}
{!selectedTour && 'Select a Tour'}
</button>
</div>
@ -181,6 +192,18 @@ export default function DashboardTour() {
</div>
)}
{(isModalOpen || isClosing) && selectedTour === 'secure-link' && (
<div className="tour-modal-container">
<SecureLinkTour autoStart={true} onClose={handleClose} />
</div>
)}
{(isModalOpen || isClosing) && selectedTour === 'virtual-guarded-network' && (
<div className="tour-modal-container">
<VirtualGuardedNetworkTour autoStart={true} onClose={handleClose} />
</div>
)}
{/* Modal - Dashboard */}
{isModalOpen && selectedTour === 'dashboard' && (
<>

View File

@ -66,7 +66,7 @@
height: 100%;
background-color: #0b0d10;
display: flex;
align-items: center;
align-items: flex-start;
justify-content: center;
overflow: hidden;
padding: 20px;
@ -80,17 +80,20 @@
.guardpot-modal-image-wrapper {
position: relative;
max-width: 95vw;
max-height: 95vh;
width: fit-content;
height: fit-content;
max-height: calc(100vh - 100px);
width: auto;
height: auto;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
}
.guardpot-modal-image {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
max-width: 95vw;
max-height: calc(100vh - 100px);
object-fit: contain;
display: block;
}
@ -334,7 +337,7 @@
background-color: rgba(0, 0, 0, 0.3);
display: flex;
gap: 2px;
z-index: 1001;
z-index: 1002;
backdrop-filter: blur(10px);
animation: guardpotProgressBarFadeIn 250ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@ -398,7 +401,7 @@
font-size: 14px;
font-weight: 600;
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
z-index: 10;
z-index: 1003;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

View File

@ -0,0 +1,480 @@
/* SecureLinkTour Component Styles */
/* Main Container */
.secure-link-tour-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
/* Preview Section */
.secure-link-preview-section {
width: 100%;
max-width: 800px;
margin: 0 auto;
}
.secure-link-preview-image-wrapper {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
}
.secure-link-preview-image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.secure-link-preview-overlay {
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(0, 0, 0, 0.7), rgba(11, 13, 16, 0.85));
display: flex;
align-items: center;
justify-content: center;
transition: background 300ms ease;
}
.secure-link-preview-overlay:hover {
background: linear-gradient(135deg, rgba(0, 0, 0, 0.6), rgba(11, 13, 16, 0.75));
}
/* Modal */
.secure-link-modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #0f0f12;
z-index: 1000;
animation: secureLinkModalBackdropFadeIn 250ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
overflow: hidden;
}
.secure-link-modal-backdrop.closing {
animation: secureLinkModalBackdropFadeOut 250ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.secure-link-modal-content {
position: relative;
width: 100%;
height: 100%;
background-color: #0b0d10;
display: flex;
align-items: flex-start;
justify-content: center;
overflow: hidden;
padding: 10px 20px 20px 20px;
animation: secureLinkModalContentSlideIn 300ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.secure-link-modal-content.closing {
animation: secureLinkModalContentSlideOut 250ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.secure-link-modal-image-wrapper {
position: relative;
max-width: 95vw;
max-height: calc(100vh - 80px);
width: auto;
height: auto;
margin: 0 auto;
margin-top: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.secure-link-modal-image {
width: auto;
height: auto;
max-width: 95vw;
max-height: calc(100vh - 80px);
object-fit: contain;
display: block;
}
/* Overlay System */
.secure-link-overlay-top {
position: absolute;
top: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.85);
pointer-events: none;
transition: all 500ms ease-in-out;
opacity: 1;
}
.secure-link-overlay-left {
position: absolute;
left: 0;
background: rgba(0, 0, 0, 0.85);
pointer-events: none;
transition: all 500ms ease-in-out;
opacity: 1;
}
.secure-link-overlay-right {
position: absolute;
right: 0;
background: rgba(0, 0, 0, 0.85);
pointer-events: none;
transition: all 500ms ease-in-out;
opacity: 1;
}
.secure-link-overlay-bottom {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
pointer-events: none;
transition: all 500ms ease-in-out;
opacity: 1;
}
.secure-link-overlay-transitioning {
opacity: 0.3;
}
.secure-link-highlight-border {
position: absolute;
border: 2px solid #b02f34;
border-radius: 10px;
pointer-events: none;
box-shadow: 0 0 0 2px rgba(176, 47, 52, 0.3);
transition: all 500ms ease-in-out;
opacity: 1;
transform: scale(1);
}
.secure-link-highlight-border.transitioning {
opacity: 0.5;
transform: scale(0.98);
}
/* Progress Bar */
.secure-link-tour-progress-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 8px;
background-color: rgba(0, 0, 0, 0.3);
display: flex;
gap: 2px;
z-index: 1002;
backdrop-filter: blur(10px);
animation: secureLinkProgressBarFadeIn 250ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.secure-link-tour-progress-bar.closing {
animation: secureLinkProgressBarFadeOut 250ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.secure-link-progress-segment {
flex: 1;
position: relative;
cursor: pointer;
overflow: visible;
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
background-color: rgba(255, 255, 255, 0.1);
}
.secure-link-progress-segment:hover {
background-color: rgba(255, 255, 255, 0.15);
}
.secure-link-progress-segment-fill {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, #b02f34 0%, #d24449 100%);
transform: scaleX(0);
transform-origin: left;
transition: transform 500ms cubic-bezier(0.4, 0, 0.2, 1);
}
.secure-link-progress-segment.completed .secure-link-progress-segment-fill {
transform: scaleX(1);
}
.secure-link-progress-segment.active .secure-link-progress-segment-fill {
transform: scaleX(1);
animation: secureLinkProgressFillPulse 2s ease-in-out infinite;
}
/* Progress Step Numbers */
.secure-link-progress-step-number {
position: absolute;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
width: 32px;
height: 32px;
border-radius: 50%;
background-color: #1a1d25;
border: 2px solid #262a33;
color: #6b7280;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1003;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.secure-link-progress-segment:hover .secure-link-progress-step-number {
transform: translateX(-50%) scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.secure-link-progress-step-number.completed {
background-color: #8e262a;
border-color: #b02f34;
color: #f1f2f5;
box-shadow: 0 2px 8px rgba(176, 47, 52, 0.3);
}
.secure-link-progress-step-number.active {
background: linear-gradient(135deg, #b02f34 0%, #d24449 100%);
border-color: #d24449;
color: white;
box-shadow: 0 4px 16px rgba(176, 47, 52, 0.6);
animation: stepNumberPulse 2s infinite;
}
/* Secure Link Step Popup - Animated */
.secure-link-step-popup {
position: absolute;
background-color: #111319;
border: 1px solid #262a33;
border-radius: 12px;
padding: 20px;
color: #f1f2f5;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(176, 47, 52, 0.3);
min-width: 300px;
max-width: 400px;
animation: secureLinkPopupSlideIn 600ms ease-out forwards;
transition: all 500ms ease-in-out;
}
.secure-link-step-popup.transitioning {
opacity: 0.3;
transition: all 500ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.secure-link-step-popup.transitioning.transition-next {
transform: translateX(80px) scale(0.95);
}
.secure-link-step-popup.transitioning.transition-prev {
transform: translateX(-80px) scale(0.95);
}
.secure-link-step-popup.closing {
opacity: 0;
transform: translateY(20px) scale(0.8);
transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
}
/* Popup Animated Border */
.secure-link-popup-animated-border {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 12px;
border: 2px solid transparent;
background: linear-gradient(45deg, rgba(176, 47, 52, 0.3), rgba(176, 47, 52, 0.1));
background-clip: border-box;
animation: pulseGlow 2s infinite;
transition: all 400ms ease-in-out;
pointer-events: none;
}
/* Quit Button */
.secure-link-quit-button {
position: absolute;
top: 12px;
right: 12px;
width: 28px;
height: 28px;
background: rgba(0, 0, 0, 0.5);
border: 1px solid #262a33;
border-radius: 50%;
color: #b9bfca;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: normal;
line-height: 1;
z-index: 20;
transition: all 200ms ease;
}
.secure-link-quit-button:hover {
background: rgba(176, 47, 52, 0.8);
color: white;
transform: scale(1.1);
}
/* Popup Content */
.secure-link-popup-content {
position: relative;
z-index: 10;
transition: all 400ms ease-in-out;
}
/* Animations */
@keyframes secureLinkPopupSlideIn {
from {
opacity: 0;
transform: translateY(20px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes pulseGlow {
0% { box-shadow: 0 0 0 0 rgba(176, 47, 52, 0.4); }
50% { box-shadow: 0 0 8px 4px rgba(176, 47, 52, 0.6); }
100% { box-shadow: 0 0 0 0 rgba(176, 47, 52, 0.4); }
}
@keyframes secureLinkModalBackdropFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes secureLinkModalBackdropFadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes secureLinkModalContentSlideIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes secureLinkModalContentSlideOut {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}
@keyframes secureLinkProgressBarFadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes secureLinkProgressBarFadeOut {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(10px);
}
}
@keyframes secureLinkProgressFillPulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
@keyframes stepNumberPulse {
0%, 100% {
transform: translateX(-50%) scale(1);
box-shadow: 0 4px 16px rgba(176, 47, 52, 0.6);
}
50% {
transform: translateX(-50%) scale(1.05);
box-shadow: 0 6px 20px rgba(176, 47, 52, 0.8);
}
}
/* Responsive Design */
@media (max-width: 768px) {
.secure-link-step-popup {
min-width: 250px;
max-width: 90vw;
padding: 16px;
}
.secure-link-modal-content {
padding: 5px 10px 10px 10px;
}
.secure-link-modal-image-wrapper {
margin-top: 5px;
max-height: calc(100vh - 60px);
}
.secure-link-modal-image {
max-width: 98vw;
max-height: calc(100vh - 60px);
}
}
@media (min-width: 769px) and (max-width: 1440px) {
.secure-link-modal-image-wrapper {
max-height: calc(100vh - 70px);
}
.secure-link-modal-image {
max-height: calc(100vh - 70px);
}
}
@media (min-width: 1441px) {
.secure-link-modal-image-wrapper {
max-height: calc(100vh - 90px);
}
.secure-link-modal-image {
max-height: calc(100vh - 90px);
}
}

View File

@ -0,0 +1,342 @@
import { useState } from 'react'
import secureLink1 from '../assets/securelink1.png'
import secureLink2 from '../assets/securelink2.png'
import secureLink3 from '../assets/securelink3.png'
import './SecureLinkTour.css'
type TourStep = {
id: string
title: string
description: string
image: string
region: { leftPct: number; topPct: number; widthPct: number; heightPct: number }
popupPosition?: { leftPct?: number; topPct?: number; rightPct?: number; bottomPct?: number }
}
const secureLinkSteps: TourStep[] = [
{
id: 'overview',
title: 'Secure Link Overview',
description: 'Secure Link sayfasına hoş geldiniz! Bu sayfada Guardpot\'lar arasındaki ağ performansını ve bağlantı doğrulamasını görüntüleyebilirsiniz.',
image: secureLink1,
region: { leftPct: 0, topPct: 0, widthPct: 100, heightPct: 100 },
popupPosition: { rightPct: 3, topPct: 5 },
},
{
id: 'network-topology',
title: 'Network Topology',
description: 'Bu interaktif ağ topolojisi haritası, tüm Guardpot bağlantılarınızı görsel olarak gösterir. Her düğüm bir Guardpot\'u temsil eder.',
image: secureLink1,
region: { leftPct: 15, topPct: 1, widthPct: 83, heightPct: 10 },
popupPosition: { rightPct: 3, topPct: 13 },
},
{
id: 'network-stats',
title: 'Network Statistics',
description: 'Üst kısımda ağınızın sağlık durumunu gösteren istatistikler bulunur. Healthy, Errors, Running ve Total bağlantı sayılarını buradan takip edebilirsiniz.',
image: secureLink1,
region: { leftPct: 15, topPct: 14, widthPct: 84, heightPct: 75 },
popupPosition: { rightPct: 70, topPct: 23 },
},
{
id: 'connection-details',
title: 'Connection Details',
description: 'Bağlantılar arasında tıklayarak detaylı ping, jitter ve paket kaybı istatistiklerini görüntüleyebilirsiniz. Grafikler gerçek zamanlı performansı gösterir.',
image: secureLink2,
region: { leftPct: 19, topPct: 1, widthPct: 74, heightPct: 93 },
popupPosition: { leftPct: 2, topPct: 14 },
},
{
id: 'navigation-controls',
title: 'Navigation Controls',
description: 'Sol alt köşedeki kontroller ile ağ haritasını döndürebilir, yakınlaştırabilir ve istediğiniz açıdan görüntüleyebilirsiniz.',
image: secureLink1,
region: { leftPct: 92, topPct: 42, widthPct: 8, heightPct: 15 },
popupPosition: { leftPct: 62, bottomPct: 45 },
},
{
id: 'graph-view',
title: 'Graph View Options',
description: 'Sağ üst köşede Graph, Link Checks ve List seçenekleri ile görünümü özelleştirebilirsiniz. Her görünüm farklı perspektifler sunar.',
image: secureLink3,
region: { leftPct: 28, topPct: 5, widthPct: 44, heightPct: 90 },
popupPosition: { leftPct: 72, topPct: 14 },
},
]
type SecureLinkTourProps = {
autoStart?: boolean
onClose?: () => void
}
export default function SecureLinkTour({ autoStart = false, onClose }: SecureLinkTourProps) {
const [isModalOpen, setIsModalOpen] = useState(autoStart)
const [currentStep, setCurrentStep] = useState(0)
const [isTransitioning, setIsTransitioning] = useState(false)
const [isModalAnimating, setIsModalAnimating] = useState(false)
const [transitionDirection, setTransitionDirection] = useState<'next' | 'prev'>('next')
const handleStartTour = () => {
setIsModalOpen(true)
setCurrentStep(0)
}
const handleClose = () => {
setIsModalAnimating(true)
// Parent'ı hemen bilgilendir
if (onClose) {
onClose()
}
setTimeout(() => {
setIsModalOpen(false)
setCurrentStep(0)
setIsTransitioning(false)
setTimeout(() => {
setIsModalAnimating(false)
}, 50)
}, 250)
}
const handleNext = () => {
if (currentStep < secureLinkSteps.length - 1) {
setTransitionDirection('next')
setIsTransitioning(true)
setTimeout(() => {
setCurrentStep(currentStep + 1)
setIsTransitioning(false)
}, 500)
}
}
const handlePrevious = () => {
if (currentStep > 0) {
setTransitionDirection('prev')
setIsTransitioning(true)
setTimeout(() => {
setCurrentStep(currentStep - 1)
setIsTransitioning(false)
}, 500)
}
}
const handleStepClick = (stepIndex: number) => {
if (stepIndex !== currentStep) {
setTransitionDirection(stepIndex > currentStep ? 'next' : 'prev')
setIsTransitioning(true)
setTimeout(() => {
setCurrentStep(stepIndex)
setIsTransitioning(false)
}, 500)
}
}
const activeStep = secureLinkSteps[currentStep]
if (!autoStart && !isModalOpen) {
return (
<div className="secure-link-tour-container">
<div className="secure-link-preview-section">
<div className="secure-link-preview-image-wrapper">
<img src={secureLink1} alt="Secure Link Preview" className="secure-link-preview-image" />
<div className="secure-link-preview-overlay">
<button className="start-tour-button" onClick={handleStartTour}>
<span className="button-icon"></span>
Start Secure Link Tour
</button>
</div>
</div>
</div>
</div>
)
}
return (
<>
<div className={`secure-link-modal-backdrop ${isModalAnimating ? 'closing' : ''}`} onClick={handleClose}>
<div className={`secure-link-modal-content ${isModalAnimating ? 'closing' : ''}`} onClick={(e) => e.stopPropagation()}>
<div className="secure-link-modal-image-wrapper">
<img className="secure-link-modal-image" src={activeStep.image} alt="Secure Link" />
{/* Dark overlay with highlight window - only show if not first step */}
{currentStep > 0 && (
<>
{/* Top overlay */}
{activeStep.region.topPct > 0 && (
<div
className={`secure-link-overlay-top ${isTransitioning ? 'secure-link-overlay-transitioning' : ''}`}
style={{ height: `${activeStep.region.topPct}%` }}
/>
)}
{/* Left overlay */}
{activeStep.region.leftPct > 0 && (
<div
className={`secure-link-overlay-left ${isTransitioning ? 'secure-link-overlay-transitioning' : ''}`}
style={{ top: `${activeStep.region.topPct}%`, width: `${activeStep.region.leftPct}%`, height: `${activeStep.region.heightPct}%` }}
/>
)}
{/* Right overlay */}
{activeStep.region.leftPct + activeStep.region.widthPct < 100 && (
<div
className={`secure-link-overlay-right ${isTransitioning ? 'secure-link-overlay-transitioning' : ''}`}
style={{ top: `${activeStep.region.topPct}%`, left: `${activeStep.region.leftPct + activeStep.region.widthPct}%`, height: `${activeStep.region.heightPct}%` }}
/>
)}
{/* Bottom overlay */}
{activeStep.region.topPct + activeStep.region.heightPct < 100 && (
<div
className={`secure-link-overlay-bottom ${isTransitioning ? 'secure-link-overlay-transitioning' : ''}`}
style={{ top: `${activeStep.region.topPct + activeStep.region.heightPct}%` }}
/>
)}
{/* Highlight border */}
<div
className={`secure-link-highlight-border ${isTransitioning ? 'transitioning' : ''}`}
style={{ left: `${activeStep.region.leftPct}%`, top: `${activeStep.region.topPct}%`, width: `${activeStep.region.widthPct}%`, height: `${activeStep.region.heightPct}%` }}
/>
</>
)}
{/* Step info popup */}
<div
className={`secure-link-step-popup ${isTransitioning ? `transitioning transition-${transitionDirection}` : ''} ${isModalAnimating ? 'closing' : ''}`}
style={{
...(activeStep.popupPosition?.leftPct && { left: `${activeStep.popupPosition.leftPct}%` }),
...(activeStep.popupPosition?.topPct && { top: `${activeStep.popupPosition.topPct}%` }),
...(activeStep.popupPosition?.rightPct && { right: `${activeStep.popupPosition.rightPct}%` }),
...(activeStep.popupPosition?.bottomPct && { bottom: `${activeStep.popupPosition.bottomPct}%` }),
}}
>
{/* Subtle animated border */}
<div className="secure-link-popup-animated-border" />
{/* Quit button */}
<button className="secure-link-quit-button" onClick={handleClose}>×</button>
{/* Content with higher z-index */}
<div className="secure-link-popup-content">
<div style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
marginBottom: '12px'
}}>
<div style={{
background: '#b02f34',
color: 'white',
fontSize: '12px',
padding: '4px 8px',
borderRadius: '999px',
fontWeight: 600
}}>
{currentStep + 1}/{secureLinkSteps.length}
</div>
<h3 style={{
margin: 0,
fontSize: '18px',
fontWeight: 600,
color: '#f1f2f5'
}}>
{activeStep.title}
</h3>
</div>
<p style={{
margin: 0,
fontSize: '14px',
lineHeight: 1.6,
color: '#b9bfca'
}}>
{activeStep.description}
</p>
<div style={{
display: 'flex',
gap: '10px',
marginTop: '20px',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<button
onClick={handlePrevious}
disabled={currentStep === 0}
style={{
background: currentStep === 0 ? 'rgba(0, 0, 0, 0.3)' : '#1a1d26',
border: '1px solid #262a33',
color: currentStep === 0 ? '#555' : '#b9bfca',
padding: '8px 16px',
borderRadius: '6px',
cursor: currentStep === 0 ? 'not-allowed' : 'pointer',
fontSize: '14px',
fontWeight: 500,
transition: 'all 200ms ease'
}}
onMouseEnter={(e) => {
if (currentStep !== 0) {
e.currentTarget.style.background = '#262a33'
e.currentTarget.style.color = '#f1f2f5'
}
}}
onMouseLeave={(e) => {
if (currentStep !== 0) {
e.currentTarget.style.background = '#1a1d26'
e.currentTarget.style.color = '#b9bfca'
}
}}
>
Previous
</button>
<button
onClick={currentStep === secureLinkSteps.length - 1 ? handleClose : handleNext}
style={{
background: 'linear-gradient(135deg, #b02f34 0%, #8a252a 100%)',
border: '1px solid #b02f34',
color: 'white',
padding: '8px 20px',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: 600,
transition: 'all 200ms ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, #c93640 0%, #b02f34 100%)'
e.currentTarget.style.transform = 'translateY(-1px)'
e.currentTarget.style.boxShadow = '0 4px 12px rgba(176, 47, 52, 0.4)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, #b02f34 0%, #8a252a 100%)'
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = 'none'
}}
>
{currentStep === secureLinkSteps.length - 1 ? 'Finish' : 'Next →'}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Progress Bar */}
<div className={`secure-link-tour-progress-bar ${isModalAnimating ? 'closing' : ''}`}>
{secureLinkSteps.map((step, index) => (
<div
key={step.id}
className={`secure-link-progress-segment ${index < currentStep ? 'completed' : ''} ${index === currentStep ? 'active' : ''}`}
onClick={() => handleStepClick(index)}
title={step.title}
>
<div className="secure-link-progress-segment-fill" />
<div className={`secure-link-progress-step-number ${index < currentStep ? 'completed' : ''} ${index === currentStep ? 'active' : ''}`}>
{index + 1}
</div>
</div>
))}
</div>
</>
)
}

View File

@ -0,0 +1,88 @@
/* VGN Tour Styles - mirrors other tours */
.vgn-modal-backdrop {
position: fixed;
inset: 0;
background: #0f0f12;
z-index: 1000;
animation: vgnBackdropIn 250ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.vgn-modal-backdrop.closing { animation: vgnBackdropOut 250ms cubic-bezier(0.4, 0, 0.2, 1) forwards; }
.vgn-modal-content {
position: relative;
width: 100%;
height: 100%;
background: #0b0d10;
display: flex;
align-items: flex-start;
justify-content: center;
overflow: hidden;
padding: 20px;
animation: vgnContentIn 300ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.vgn-modal-content.closing { animation: vgnContentOut 250ms cubic-bezier(0.4, 0, 0.2, 1) forwards; }
.vgn-modal-image-wrapper {
position: relative;
max-width: 95vw;
max-height: calc(100vh - 100px);
width: auto;
height: auto;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
}
.vgn-modal-image { width: auto; height: auto; max-width: 95vw; max-height: calc(100vh - 100px); object-fit: contain; display: block; }
/* Overlays */
.vgn-overlay-top, .vgn-overlay-left, .vgn-overlay-right, .vgn-overlay-bottom {
position: absolute;
background: rgba(0,0,0,0.85);
pointer-events: none;
transition: all 500ms ease-in-out;
opacity: 1;
}
.vgn-overlay-top { top: 0; left: 0; right: 0; }
.vgn-overlay-left { left: 0; }
.vgn-overlay-right { right: 0; }
.vgn-overlay-bottom { left: 0; right: 0; bottom: 0; }
.vgn-overlay-transitioning { opacity: 0.3; }
.vgn-highlight-border { position: absolute; border: 2px solid #b02f34; border-radius: 10px; pointer-events: none; transition: all 500ms ease-in-out; box-shadow: 0 0 0 2px rgba(176,47,52,0.3); opacity: 1; transform: scale(1); }
.vgn-highlight-border.transitioning { opacity: .5; transform: scale(.98); }
/* Step Popup */
.vgn-step-popup { position: absolute; background:#111319; border:1px solid #262a33; border-radius:12px; padding:20px; color:#f1f2f5; min-width:300px; max-width:400px; box-shadow:0 10px 40px rgba(0,0,0,.5),0 0 0 1px rgba(176,47,52,.3); animation: vgnPopupIn 600ms ease-out forwards; transition: all 500ms ease-in-out; }
.vgn-step-popup.transitioning { opacity:.3; transition: all 500ms cubic-bezier(0.25,0.46,0.45,0.94); }
.vgn-step-popup.transitioning.transition-next { transform: translateX(80px) scale(.95); }
.vgn-step-popup.transitioning.transition-prev { transform: translateX(-80px) scale(.95); }
.vgn-step-popup.closing { opacity:0; transform: translateY(20px) scale(.8); transition: all 250ms cubic-bezier(0.4,0,0.2,1); }
.vgn-popup-animated-border { position:absolute; inset:0; border-radius:12px; border:2px solid transparent; background: linear-gradient(45deg, rgba(176,47,52,.3), rgba(176,47,52,.1)); background-clip:border-box; animation:pulseGlow 2s infinite; pointer-events:none; }
.vgn-popup-content { position:relative; z-index:10; }
.vgn-quit-button { position:absolute; top:12px; right:12px; width:28px; height:28px; background:rgba(0,0,0,.5); border:1px solid #262a33; border-radius:50%; color:#b9bfca; cursor:pointer; display:flex; align-items:center; justify-content:center; font-size:16px; line-height:1; z-index:20; transition:all 200ms ease; }
.vgn-quit-button:hover { background: rgba(176,47,52,.8); color:#fff; transform: scale(1.1); }
/* Progress Bar */
.vgn-tour-progress-bar { position: fixed; bottom: 0; left: 0; right: 0; height: 8px; background: rgba(0,0,0,.3); display:flex; gap:2px; z-index:1002; backdrop-filter: blur(10px); animation: vgnBarIn 250ms cubic-bezier(0.4,0,0.2,1) forwards; }
.vgn-tour-progress-bar.closing { animation: vgnBarOut 250ms cubic-bezier(0.4,0,0.2,1) forwards; }
.vgn-progress-segment { flex:1; position:relative; cursor:pointer; overflow:visible; transition: all 300ms cubic-bezier(0.4,0,0.2,1); background: rgba(255,255,255,.1); }
.vgn-progress-segment:hover { background: rgba(255,255,255,.15); }
.vgn-progress-segment-fill { position:absolute; inset:0; background: linear-gradient(90deg,#b02f34 0%,#d24449 100%); transform: scaleX(0); transform-origin:left; transition: transform 500ms cubic-bezier(0.4,0,0.2,1); }
.vgn-progress-segment.completed .vgn-progress-segment-fill, .vgn-progress-segment.active .vgn-progress-segment-fill { transform: scaleX(1); }
.vgn-progress-step-number { position:absolute; bottom:12px; left:50%; transform: translateX(-50%); width:32px; height:32px; border-radius:50%; background:#1a1d25; border:2px solid #262a33; color:#6b7280; display:flex; align-items:center; justify-content:center; font-size:14px; font-weight:600; transition: all 250ms cubic-bezier(0.4,0,0.2,1); z-index:1003; box-shadow:0 2px 8px rgba(0,0,0,.3); }
.vgn-progress-segment:hover .vgn-progress-step-number { transform: translateX(-50%) scale(1.1); box-shadow: 0 4px 12px rgba(0,0,0,.4); }
.vgn-progress-step-number.completed { background:#8e262a; border-color:#b02f34; color:#f1f2f5; box-shadow: 0 2px 8px rgba(176,47,52,.3); }
.vgn-progress-step-number.active { background: linear-gradient(135deg,#b02f34 0%, #d24449 100%); border-color:#d24449; color:#fff; box-shadow: 0 4px 16px rgba(176,47,52,.6); animation: stepNumberPulse 2s infinite; }
@keyframes pulseGlow { 0%{ box-shadow:0 0 0 0 rgba(176,47,52,.4);} 50%{ box-shadow:0 0 8px 4px rgba(176,47,52,.6);} 100%{ box-shadow:0 0 0 0 rgba(176,47,52,.4);} }
@keyframes vgnBackdropIn { from{opacity:0} to{opacity:1}}
@keyframes vgnBackdropOut { from{opacity:1} to{opacity:0}}
@keyframes vgnContentIn { from{opacity:0; transform: scale(.95)} to{opacity:1; transform: scale(1)}}
@keyframes vgnContentOut { from{opacity:1; transform: scale(1)} to{opacity:0; transform: scale(.95)}}
@keyframes vgnBarIn { from{opacity:0; transform: translateY(10px)} to{opacity:1; transform: translateY(0)}}
@keyframes vgnBarOut { from{opacity:1; transform: translateY(0)} to{opacity:0; transform: translateY(10px)}}
@keyframes vgnPopupIn { from{opacity:0; transform: translateY(20px) scale(.9)} to{opacity:1; transform: translateY(0) scale(1)}}

View File

@ -0,0 +1,198 @@
import { useState } from 'react'
import vgn1 from '../assets/virtualguardednetwork1.png'
import vgn2 from '../assets/virtualguardednetwork2.png'
import './VirtualGuardedNetworkTour.css'
type TourStep = {
id: string
title: string
description: string
image: string
region: { leftPct: number; topPct: number; widthPct: number; heightPct: number }
popupPosition?: { leftPct?: number; topPct?: number; rightPct?: number; bottomPct?: number }
}
const vgnSteps: TourStep[] = [
{
id: 'overview',
title: 'Virtual Guarded Network (VGN) Overview',
description: 'VGN bölümüne hoş geldiniz! Zincirler, erişim, MFA politikaları ve logları buradan yönetebilirsiniz.',
image: vgn1,
region: { leftPct: 0, topPct: 0, widthPct: 100, heightPct: 100 },
popupPosition: { rightPct: 35, topPct: 60 },
},
{
id: 'chain-header',
title: 'Chain Header & Controls',
description: 'Zincirin adı, mod, MTU, DHCP ve routing gibi meta veriler bu başlıkta yer alır. Arama ve Add New ile yeni zincirler ekleyebilirsiniz.',
image: vgn1,
region: { leftPct: 15, topPct: 2, widthPct: 84, heightPct: 10 },
popupPosition: { rightPct: 2, topPct: 14 },
},
{
id: 'chain-flow',
title: 'Chain Flow',
description: 'Kullanıcı girişinden başlayıp, iç düğümler ve Internet Exit\'e giden görsel akış burada gösterilir.',
image: vgn1,
region: { leftPct: 15, topPct: 16, widthPct: 84, heightPct: 43 },
popupPosition: { leftPct: 15, bottomPct: 20 },
},
{
id: 'actions-and-policies',
title: 'Actions & Policies',
description: 'Sağ üstte yer alan VGN Chains, Access, MFA Policies ve Logs butonları ile politikaları ve günlükleri yönetebilirsiniz.',
image: vgn2,
region: { leftPct: 71, topPct: 7, widthPct: 24, heightPct: 7 },
popupPosition: { rightPct: 8, topPct: 16 },
},
]
type VirtualGuardedNetworkTourProps = {
autoStart?: boolean
onClose?: () => void
}
export default function VirtualGuardedNetworkTour({ autoStart = false, onClose }: VirtualGuardedNetworkTourProps) {
const [isModalOpen, setIsModalOpen] = useState(autoStart)
const [currentStep, setCurrentStep] = useState(0)
const [isTransitioning, setIsTransitioning] = useState(false)
const [isModalAnimating, setIsModalAnimating] = useState(false)
const [transitionDirection, setTransitionDirection] = useState<'next' | 'prev'>('next')
const handleStartTour = () => {
setIsModalOpen(true)
setCurrentStep(0)
}
const handleClose = () => {
setIsModalAnimating(true)
if (onClose) onClose()
setTimeout(() => {
setIsModalOpen(false)
setCurrentStep(0)
setIsTransitioning(false)
setTimeout(() => setIsModalAnimating(false), 50)
}, 250)
}
const handleNext = () => {
if (currentStep < vgnSteps.length - 1) {
setTransitionDirection('next')
setIsTransitioning(true)
setTimeout(() => {
setCurrentStep(currentStep + 1)
setTimeout(() => setIsTransitioning(false), 50)
}, 300)
}
}
const handlePrevious = () => {
if (currentStep > 0) {
setTransitionDirection('prev')
setIsTransitioning(true)
setTimeout(() => {
setCurrentStep(currentStep - 1)
setTimeout(() => setIsTransitioning(false), 50)
}, 300)
}
}
const handleStepClick = (stepIndex: number) => {
if (stepIndex !== currentStep) {
setTransitionDirection(stepIndex > currentStep ? 'next' : 'prev')
setIsTransitioning(true)
setTimeout(() => {
setCurrentStep(stepIndex)
setTimeout(() => setIsTransitioning(false), 50)
}, 300)
}
}
const activeStep = vgnSteps[currentStep]
if (!autoStart && !isModalOpen) {
return (
<div className="vgn-tour-container">
<div className="vgn-preview-section">
<div className="vgn-preview-image-wrapper">
<img src={vgn1} alt="VGN Preview" className="vgn-preview-image" />
<div className="vgn-preview-overlay">
<button className="start-tour-button" onClick={handleStartTour}>
<span className="button-icon"></span>
Start VGN Tour
</button>
</div>
</div>
</div>
</div>
)
}
return (
<>
<div className={`vgn-modal-backdrop ${isModalAnimating ? 'closing' : ''}`} onClick={handleClose}>
<div className={`vgn-modal-content ${isModalAnimating ? 'closing' : ''}`} onClick={(e) => e.stopPropagation()}>
<div className="vgn-modal-image-wrapper">
<img className="vgn-modal-image" src={activeStep.image} alt="VGN" />
{currentStep > 0 && (
<>
{activeStep.region.topPct > 0 && (
<div className={`vgn-overlay-top ${isTransitioning ? 'vgn-overlay-transitioning' : ''}`} style={{ height: `${activeStep.region.topPct}%` }} />
)}
{activeStep.region.leftPct > 0 && (
<div className={`vgn-overlay-left ${isTransitioning ? 'vgn-overlay-transitioning' : ''}`} style={{ top: `${activeStep.region.topPct}%`, width: `${activeStep.region.leftPct}%`, height: `${activeStep.region.heightPct}%` }} />
)}
{activeStep.region.leftPct + activeStep.region.widthPct < 100 && (
<div className={`vgn-overlay-right ${isTransitioning ? 'vgn-overlay-transitioning' : ''}`} style={{ top: `${activeStep.region.topPct}%`, left: `${activeStep.region.leftPct + activeStep.region.widthPct}%`, height: `${activeStep.region.heightPct}%` }} />
)}
{activeStep.region.topPct + activeStep.region.heightPct < 100 && (
<div className={`vgn-overlay-bottom ${isTransitioning ? 'vgn-overlay-transitioning' : ''}`} style={{ top: `${activeStep.region.topPct + activeStep.region.heightPct}%` }} />
)}
<div className={`vgn-highlight-border ${isTransitioning ? 'transitioning' : ''}`} style={{ left: `${activeStep.region.leftPct}%`, top: `${activeStep.region.topPct}%`, width: `${activeStep.region.widthPct}%`, height: `${activeStep.region.heightPct}%` }} />
</>
)}
<div
className={`vgn-step-popup ${isTransitioning ? `transitioning transition-${transitionDirection}` : ''} ${isModalAnimating ? 'closing' : ''}`}
style={{
...(activeStep.popupPosition?.leftPct && { left: `${activeStep.popupPosition.leftPct}%` }),
...(activeStep.popupPosition?.topPct && { top: `${activeStep.popupPosition.topPct}%` }),
...(activeStep.popupPosition?.rightPct && { right: `${activeStep.popupPosition.rightPct}%` }),
...(activeStep.popupPosition?.bottomPct && { bottom: `${activeStep.popupPosition.bottomPct}%` }),
}}
>
<div className="vgn-popup-animated-border" />
<button className="vgn-quit-button" onClick={handleClose}>×</button>
<div className="vgn-popup-content">
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
<div style={{ background: '#b02f34', color: 'white', fontSize: '12px', padding: '4px 8px', borderRadius: '999px', fontWeight: 600 }}>
{currentStep + 1}/{vgnSteps.length}
</div>
<h3 style={{ margin: 0, fontSize: '18px', fontWeight: 600, color: '#f1f2f5' }}>{activeStep.title}</h3>
</div>
<p style={{ margin: 0, fontSize: '14px', lineHeight: 1.6, color: '#b9bfca' }}>{activeStep.description}</p>
<div style={{ display: 'flex', gap: '10px', marginTop: '20px', justifyContent: 'space-between', alignItems: 'center' }}>
<button onClick={handlePrevious} disabled={currentStep === 0} style={{ background: currentStep === 0 ? 'rgba(0,0,0,0.3)' : '#1a1d26', border: '1px solid #262a33', color: currentStep === 0 ? '#555' : '#b9bfca', padding: '8px 16px', borderRadius: '6px', cursor: currentStep === 0 ? 'not-allowed' : 'pointer', fontSize: '14px', fontWeight: 500, transition: 'all 200ms ease' }}> Previous</button>
<button onClick={currentStep === vgnSteps.length - 1 ? handleClose : handleNext} style={{ background: 'linear-gradient(135deg, #b02f34 0%, #8a252a 100%)', border: '1px solid #b02f34', color: 'white', padding: '8px 20px', borderRadius: '6px', cursor: 'pointer', fontSize: '14px', fontWeight: 600, transition: 'all 200ms ease' }}>{currentStep === vgnSteps.length - 1 ? 'Finish' : 'Next →'}</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div className={`vgn-tour-progress-bar ${isModalAnimating ? 'closing' : ''}`}>
{vgnSteps.map((step, index) => (
<div key={step.id} className={`vgn-progress-segment ${index < currentStep ? 'completed' : ''} ${index === currentStep ? 'active' : ''}`} onClick={() => handleStepClick(index)} title={step.title}>
<div className="vgn-progress-segment-fill" />
<div className={`vgn-progress-step-number ${index < currentStep ? 'completed' : ''} ${index === currentStep ? 'active' : ''}`}>{index + 1}</div>
</div>
))}
</div>
</>
)
}