Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Routes, Route, Navigate } from 'react-router-dom';
import { useEffect } from 'react';
import { Routes, Route, Navigate, useNavigate } from 'react-router-dom';
import { useEffect, useState } from 'react';
import Home from './pages/Home';
import Debugger from './pages/Debugger';
import PayrollScheduler from './pages/PayrollScheduler';
Expand All @@ -24,9 +24,12 @@ import Login from './pages/Login';
import AuthCallback from './pages/AuthCallback';
import { useTranslation } from 'react-i18next';
import { contractService } from './services/contracts';
import { OnboardingTour, ONBOARDING_KEY } from './components/OnboardingTour';

function App() {
const { t } = useTranslation();
const navigate = useNavigate();
const [showTour, setShowTour] = useState(false);

// Initialize contract service on app startup
useEffect(() => {
Expand All @@ -35,8 +38,25 @@ function App() {
});
}, []);

// Auto-start onboarding tour on first authenticated visit
useEffect(() => {
const tourCompleted = localStorage.getItem(ONBOARDING_KEY) === 'true';
const hasAuth = localStorage.getItem('payd_auth_token') !== null;
if (!tourCompleted && hasAuth) {
setShowTour(true);
}
}, []);

// Listen for "restart tour" requests from e.g. Help Center
useEffect(() => {
const handleRestartTour = () => setShowTour(true);
window.addEventListener('payd:restart-tour', handleRestartTour);
return () => window.removeEventListener('payd:restart-tour', handleRestartTour);
}, []);

return (
<Routes>
<>
<Routes>
<Route element={<EmployerLayout />}>
<Route
path="/"
Expand Down Expand Up @@ -221,6 +241,12 @@ function App() {
<Route path="/auth-callback" element={<AuthCallback />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
<OnboardingTour
run={showTour}
navigateTo={navigate}
onComplete={() => setShowTour(false)}
/>
</>
);
}

Expand Down
6 changes: 4 additions & 2 deletions frontend/src/components/DashboardSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ interface NavItem {
icon: React.ComponentType<{ className?: string }>;
label: string;
path: string;
tourId?: string;
}

const navItems: NavItem[] = [
{ icon: LayoutDashboard, label: 'Dashboard', path: '/' },
{ icon: Wallet, label: 'Payroll', path: '/payroll' },
{ icon: Users, label: 'Employees', path: '/employee' },
{ icon: Wallet, label: 'Payroll', path: '/payroll', tourId: 'tour-payroll' },
{ icon: Users, label: 'Employees', path: '/employee', tourId: 'tour-employees' },
{ icon: TrendingUp, label: 'Forecast', path: '/forecast' },
{ icon: FileText, label: 'Reports', path: '/reports' },
{ icon: Receipt, label: 'Tax Compliance', path: '/tax-compliance' },
Expand Down Expand Up @@ -58,6 +59,7 @@ export const DashboardSidebar: React.FC<DashboardSidebarProps> = ({
<NavLink
key={item.path}
to={item.path}
id={item.tourId && !onClose ? item.tourId : undefined}
onClick={onClose}
className={({ isActive }) =>
`flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200 group ${
Expand Down
75 changes: 56 additions & 19 deletions frontend/src/components/OnboardingTour.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,97 @@
import React from 'react';
import React, { useState, useCallback, useEffect } from 'react';
import Joyride, { Step, CallBackProps, STATUS } from 'react-joyride';

const ONBOARDING_KEY = 'payd_onboarding_complete';

const TOUR_STEPS: Step[] = [
{
target: '#tour-welcome',
content: "Welcome to PayD! Let's take a quick tour to get you started.",
content: 'Welcome to PayD! This quick tour will show you how to manage your workforce and payroll — all in just a few clicks.',
placement: 'bottom',
disableBeacon: true,
},
{
target: '#tour-connect',
content: 'First, connect your Stellar wallet to securely manage your organization.',
content: 'Connect your Stellar wallet to securely manage your organization and process payments.',
placement: 'bottom',
},
{
target: '#tour-employees',
content: 'Navigate here to set up your organization and manage your workforce.',
placement: 'bottom',
content: 'Manage your team here. Navigate to the Employees page to add and manage your workforce.',
placement: 'right',
},
{
target: '#tour-add-employee',
content: 'Add your employees here to include them in the payroll.',
content: 'Click here to add your first employee. Fill in their details and save them to the payroll roster.',
placement: 'right',
},
{
target: '#tour-payroll',
content: 'Once your team is set up, head over to the Payroll Scheduler.',
placement: 'bottom',
content: 'Set up automated payroll streams here. Configure recurring payments for your team in real-time.',
placement: 'right',
},
{
target: '#tour-init-payroll',
content:
'Set up automated streams and fund your distribution account to pay your team in real-time.',
placement: 'top',
content: 'Open the scheduling wizard to configure recurring payments, set frequency, and fund your distribution account.',
placement: 'bottom',
},
];

export const OnboardingTour: React.FC<{
run: boolean;
navigateTo?: (path: string) => void;
onComplete: () => void;
}> = ({ run, onComplete }) => {
const handleJoyrideCallback = (data: CallBackProps) => {
const { status } = data;
const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED];
}> = ({ run, navigateTo, onComplete }) => {
const [stepIndex, setStepIndex] = useState(0);
const [internalRun, setInternalRun] = useState(false);

if (finishedStatuses.includes(status)) {
onComplete();
useEffect(() => {
if (run) {
setStepIndex(0);
setInternalRun(true);
} else {
setInternalRun(false);
}
};
}, [run]);

const handleJoyrideCallback = useCallback(
(data: CallBackProps) => {
const { index, type, status } = data;

const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED];
if (finishedStatuses.includes(status)) {
localStorage.setItem(ONBOARDING_KEY, 'true');
setInternalRun(false);
onComplete();
return;
}

// Auto-navigate to correct page when transitioning between steps
if (type === 'step:after') {
if (index === 2 && navigateTo) {
// After step 3 (Employees), navigate to employee page for step 4 (Add Employee)
navigateTo('/employee');
}
if (index === 4 && navigateTo) {
// After step 5 (Payroll sidebar), navigate to payroll page for step 6 (Init Payroll)
navigateTo('/payroll');
}
setStepIndex(index + 1);
}
},
[navigateTo, onComplete]
);

return (
<Joyride
steps={TOUR_STEPS}
run={run}
run={internalRun}
stepIndex={stepIndex}
continuous
showProgress
showSkipButton
callback={handleJoyrideCallback}
disableScrolling
styles={{
options: {
primaryColor: '#4AF0B8',
Expand Down Expand Up @@ -100,3 +135,5 @@ export const OnboardingTour: React.FC<{
/>
);
};

export { ONBOARDING_KEY };
21 changes: 21 additions & 0 deletions frontend/src/pages/HelpCenter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,27 @@ export default function HelpCenter() {
</div>
))}
</div>

{/* Restart Onboarding Tour */}
<div className="mt-12 p-6 rounded-2xl border border-white/10 bg-white/5 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div>
<h2 className="text-lg font-bold mb-1 text-(--accent)">
Get a Guided Tour
</h2>
<p className="text-sm text-(--muted)">
Replay the interactive onboarding tour to learn how to connect a wallet, add your employees, and set up your first payroll.
</p>
</div>
<button
onClick={() => {
localStorage.removeItem('payd_onboarding_complete');
window.dispatchEvent(new Event('payd:restart-tour'));
}}
className="px-6 py-3 bg-(--accent) text-black font-bold rounded-xl hover:brightness-110 transition text-sm whitespace-nowrap"
>
Restart Tour
</button>
</div>
</div>
</div>
);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/PayrollScheduler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ export default function PayrollScheduler() {
<div className="flex flex-row sm:flex-col items-center sm:items-end gap-3 sm:gap-2">
<AutosaveIndicator saving={saving} lastSaved={lastSaved} />
<button
id="tour-init-payroll"
onClick={() => setIsWizardOpen(true)}
className="p-2.5 rounded-lg hover:bg-white/5 transition-colors touch-manipulation"
style={{ minHeight: '44px', minWidth: '44px' }}
Expand Down