@@ -1139,6 +1139,7 @@ async function renderVerifyEmail(token) {
11391139 localStorage . setItem ( 'zh_user' , JSON . stringify ( data . user ) ) ;
11401140 history . replaceState ( { page : 'overview' } , '' , '/' ) ;
11411141 renderDashboard ( ) ;
1142+ checkAndStartOnboarding ( ) ;
11421143 showToast ( 'Email verified successfully!' , 'success' ) ;
11431144 } catch ( err ) {
11441145 app . innerHTML = html `
@@ -1481,6 +1482,10 @@ async function renderDashboard() {
14811482 < i data-lucide ="settings " style ="width:16px;height:16px "> </ i >
14821483 Settings
14831484 </ a >
1485+ < a class ="sidebar-user-dropdown-item " id ="user-dropdown-tour ">
1486+ < i data-lucide ="map " style ="width:16px;height:16px "> </ i >
1487+ Take a Tour
1488+ </ a >
14841489 < a class ="sidebar-user-dropdown-item " id ="user-dropdown-logout ">
14851490 < i data-lucide ="log-out " style ="width:16px;height:16px "> </ i >
14861491 Logout
@@ -1552,6 +1557,12 @@ async function renderDashboard() {
15521557 navigateTo ( 'account/info' ) ;
15531558 } ) ;
15541559
1560+ $ ( '#user-dropdown-tour' ) . addEventListener ( 'click' , ( e ) => {
1561+ e . preventDefault ( ) ;
1562+ closeUserDropdown ( ) ;
1563+ startOnboarding ( ) ;
1564+ } ) ;
1565+
15551566 $ ( '#user-dropdown-logout' ) . addEventListener ( 'click' , async ( e ) => {
15561567 e . preventDefault ( ) ;
15571568 closeUserDropdown ( ) ;
@@ -3997,6 +4008,159 @@ async function handleExportData() {
39974008 }
39984009}
39994010
4011+ // ===== ONBOARDING TOUR =====
4012+ const ONBOARDING_STEPS = [
4013+ {
4014+ title : 'Welcome to ZeroHost!' ,
4015+ description : 'Would you like a quick tour of your dashboard? It only takes a minute.' ,
4016+ highlight : null ,
4017+ buttons : [ 'Skip' , 'Start' ] ,
4018+ } ,
4019+ {
4020+ title : 'Dashboard Overview' ,
4021+ description : `This is your Dashboard — the central hub. Here you get an overview of all your servers, their status, and resource usage at a glance.` ,
4022+ highlight : '#sidebar-nav a.nav-item[data-page="overview"]' ,
4023+ buttons : [ 'Skip' , 'Next' ] ,
4024+ } ,
4025+ {
4026+ title : 'Create a Server' ,
4027+ description : 'Use this button to deploy a new server. You can choose from different game types and configurations.' ,
4028+ highlight : '#sidebar-nav a.nav-item[data-page="create"]' ,
4029+ buttons : [ 'Skip' , 'Next' ] ,
4030+ } ,
4031+ {
4032+ title : 'My Servers' ,
4033+ description : 'All your servers live here. Manage them, check their status, rename them, and more.' ,
4034+ highlight : '#nav-servers-toggle' ,
4035+ buttons : [ 'Skip' , 'Next' ] ,
4036+ } ,
4037+ {
4038+ title : 'Two things to remember' ,
4039+ description : `<strong>Dashboard</strong> — Where you create servers, change names, view status, and manage your account.<br><br><strong>Hydrodactyl Panel</strong> — Where you start/stop your server, manage files, use the console, and control everything.` ,
4040+ highlight : null ,
4041+ buttons : [ 'Skip' , 'Got it!' ] ,
4042+ } ,
4043+ ] ;
4044+
4045+ let onboardingActive = false ;
4046+ let onboardingStep = 0 ;
4047+ let onboardingHighlightEl = null ;
4048+
4049+ function clearOnboardingHighlight ( ) {
4050+ if ( onboardingHighlightEl ) {
4051+ onboardingHighlightEl . classList . remove ( 'onboarding-highlight' ) ;
4052+ onboardingHighlightEl = null ;
4053+ }
4054+ }
4055+
4056+ function applyOnboardingHighlight ( selector ) {
4057+ clearOnboardingHighlight ( ) ;
4058+ if ( ! selector ) return ;
4059+ const el = document . querySelector ( selector ) ;
4060+ if ( el ) {
4061+ el . classList . add ( 'onboarding-highlight' ) ;
4062+ onboardingHighlightEl = el ;
4063+ }
4064+ }
4065+
4066+ function renderOnboardingStep ( stepIndex ) {
4067+ const card = $ ( '#onboarding-card' ) ;
4068+ if ( ! card ) return ;
4069+
4070+ const step = ONBOARDING_STEPS [ stepIndex ] ;
4071+ const isLast = stepIndex === ONBOARDING_STEPS . length - 1 ;
4072+ const isFirst = stepIndex === 0 ;
4073+
4074+ let dotsHtml = '<div class="onboarding-dots">' ;
4075+ ONBOARDING_STEPS . forEach ( ( _ , i ) => {
4076+ dotsHtml += `<span class="onboarding-dot ${ i === stepIndex ? 'active' : '' } "></span>` ;
4077+ } ) ;
4078+ dotsHtml += '</div>' ;
4079+
4080+ const leftBtn = step . buttons [ 0 ] ; // Skip
4081+ const rightBtn = step . buttons [ 1 ] ; // Start / Next / Got it!
4082+
4083+ card . innerHTML = html `
4084+ ${ dotsHtml }
4085+ < h2 > ${ escapeHtml ( step . title ) } </ h2 >
4086+ < p > ${ step . description } </ p >
4087+ < div class ="onboarding-actions ">
4088+ < button class ="btn btn-ghost " id ="onboarding-skip "> ${ leftBtn } </ button >
4089+ < button class ="btn btn-primary " id ="onboarding-next "> ${ rightBtn } </ button >
4090+ </ div >
4091+ ` ;
4092+
4093+ applyOnboardingHighlight ( step . highlight ) ;
4094+ initIcons ( ) ;
4095+ }
4096+
4097+ async function startOnboarding ( ) {
4098+ if ( onboardingActive ) return ;
4099+ onboardingActive = true ;
4100+ onboardingStep = 0 ;
4101+
4102+ const existing = $ ( '#onboarding-overlay' ) ;
4103+ if ( existing ) existing . remove ( ) ;
4104+
4105+ const overlay = document . createElement ( 'div' ) ;
4106+ overlay . id = 'onboarding-overlay' ;
4107+ overlay . className = 'onboarding-overlay' ;
4108+ overlay . innerHTML = html `
4109+ < div class ="onboarding-backdrop " id ="onboarding-backdrop "> </ div >
4110+ < div class ="onboarding-card " id ="onboarding-card "> </ div >
4111+ ` ;
4112+ document . body . appendChild ( overlay ) ;
4113+
4114+ renderOnboardingStep ( 0 ) ;
4115+
4116+ $ ( '#onboarding-skip' ) . addEventListener ( 'click' , stopOnboarding ) ;
4117+ $ ( '#onboarding-next' ) . addEventListener ( 'click' , ( ) => {
4118+ if ( onboardingStep === 0 ) {
4119+ onboardingStep = 1 ;
4120+ renderOnboardingStep ( onboardingStep ) ;
4121+ $ ( '#onboarding-skip' ) . addEventListener ( 'click' , stopOnboarding ) ;
4122+ $ ( '#onboarding-next' ) . addEventListener ( 'click' , handleOnboardingNext ) ;
4123+ } else {
4124+ handleOnboardingNext ( ) ;
4125+ }
4126+ } ) ;
4127+ }
4128+
4129+ function handleOnboardingNext ( ) {
4130+ if ( onboardingStep < ONBOARDING_STEPS . length - 1 ) {
4131+ onboardingStep ++ ;
4132+ renderOnboardingStep ( onboardingStep ) ;
4133+ $ ( '#onboarding-skip' ) . addEventListener ( 'click' , stopOnboarding ) ;
4134+ $ ( '#onboarding-next' ) . addEventListener ( 'click' , handleOnboardingNext ) ;
4135+ } else if ( onboardingStep === ONBOARDING_STEPS . length - 1 ) {
4136+ stopOnboarding ( true ) ;
4137+ }
4138+ }
4139+
4140+ async function stopOnboarding ( done ) {
4141+ if ( ! onboardingActive ) return ;
4142+ onboardingActive = false ;
4143+ clearOnboardingHighlight ( ) ;
4144+ const overlay = $ ( '#onboarding-overlay' ) ;
4145+ if ( overlay ) overlay . remove ( ) ;
4146+
4147+ if ( done && state . token ) {
4148+ try {
4149+ await api ( '/auth/complete-onboarding' , { method : 'POST' } ) ;
4150+ } catch { }
4151+ }
4152+ }
4153+
4154+ async function checkAndStartOnboarding ( ) {
4155+ if ( ! state . token ) return ;
4156+ try {
4157+ const data = await api ( '/auth/onboarding-status' ) ;
4158+ if ( ! data . done ) {
4159+ startOnboarding ( ) ;
4160+ }
4161+ } catch { }
4162+ }
4163+
40004164// ===== INIT =====
40014165function init ( ) {
40024166 const path = window . location . pathname ;
@@ -4033,6 +4197,7 @@ function init() {
40334197 if ( state . token ) {
40344198 history . replaceState ( { page : 'overview' } , '' , '/' ) ;
40354199 renderDashboard ( ) ;
4200+ if ( basePage !== 'signup' ) checkAndStartOnboarding ( ) ;
40364201 } else if ( basePage === 'login' ) {
40374202 renderLoginPage ( ) ;
40384203 } else {
@@ -4041,6 +4206,7 @@ function init() {
40414206 } else if ( state . token ) {
40424207 api ( '/servers/overview' ) . then ( ( ) => {
40434208 renderDashboard ( ) ;
4209+ checkAndStartOnboarding ( ) ;
40444210 fetchUnreadCount ( ) ;
40454211 setInterval ( fetchUnreadCount , 30000 ) ;
40464212 } ) . catch ( ( ) => {
0 commit comments