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
13 changes: 8 additions & 5 deletions client/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Navigate,
} from 'react-router-dom';
import { AuthProvider, useAuth } from './context/AuthContext';
import { TourProvider } from './context/TourContext';
import { routes } from './routes/RoutesConfig';

import { OrganizationInsights, ContributorInsights } from './pages';
Expand Down Expand Up @@ -54,11 +55,13 @@ const AppRoutes = () => {
const App = () => {
return (
<AuthProvider>
<Router>
<Suspense fallback={null}>
<AppRoutes />
</Suspense>
</Router>
<TourProvider>
<Router>
<Suspense fallback={null}>
<AppRoutes />
</Suspense>
</Router>
</TourProvider>
</AuthProvider>
);
};
Expand Down
205 changes: 205 additions & 0 deletions client/src/components/GuidedTour.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { Button, Space } from 'antd';

const GuidedTour = ({ run, stepIndex, role, next, prev, exit }) => {
const navigate = useNavigate();
const location = useLocation();
const [rect, setRect] = useState(null);

const orgSteps = [
{
target: '[data-tour="create-bounty"]',
content: 'Create a new bounty here.',
path: '/dashboard',
},
{
target: '[data-tour="bounty-list"]',
content: 'View and manage your bounties.',
path: '/dashboard',
},
{
target: '[data-tour="profile-menu"]',
content: 'Access your profile settings.',
path: '/dashboard',
},
{
target: '[data-tour="github-integration"]',
content: 'Connect your GitHub repo.',
path: '/profile/organization',
},
{
target: '[data-tour="discord-integration"]',
content: 'Enable Discord notifications.',
path: '/profile/organization',
},
{
target: '[data-tour="wallet-send"]',
content: 'Send payments to contributors.',
path: '/wallet',
},
{
target: '[data-tour="insights-nav"]',
content: 'View analytics for your organization.',
path: '/dashboard',
},
{
target: '[data-tour="analytics-page"]',
content: 'Track bounty stats and payments here.',
path: '/insights',
},
];

const contribSteps = [
{
target: '[data-tour="bounty-list"]',
content: 'Browse bounties to work on.',
path: '/dashboard',
},
{
target: '[data-tour="profile-menu"]',
content: 'Update your contributor profile.',
path: '/dashboard',
},
{
target: '[data-tour="wallet-send"]',
content: 'View payouts and manage your wallet.',
path: '/wallet',
},
{
target: '[data-tour="insights-nav"]',
content: 'Check your analytics.',
path: '/dashboard',
},
{
target: '[data-tour="analytics-page"]',
content: 'Analyze your contributions and payments.',
path: '/insights',
},
];

const steps = role === 'organization' ? orgSteps : contribSteps;
const step = steps[stepIndex];

useEffect(() => {
if (!run || !step) return;

if (step.path && step.path !== location.pathname) {
navigate(step.path);
}

let el = document.querySelector(step.target);

const update = () => {
if (!el) return;
const r = el.getBoundingClientRect();
const scrollY = window.scrollY;
const scrollX = window.scrollX;
const viewportH = window.innerHeight;
const viewportW = window.innerWidth;
const popoverW = 280;
const popoverH = 130;

let popTop = r.bottom + scrollY + 8;
let placement = 'bottom';
if (popTop + popoverH > scrollY + viewportH) {
popTop = r.top + scrollY - popoverH - 8;
placement = 'top';
}

let popLeft = r.left + scrollX + r.width / 2 - popoverW / 2;
if (popLeft < scrollX + 8) popLeft = scrollX + 8;
if (popLeft + popoverW > scrollX + viewportW) {
popLeft = scrollX + viewportW - popoverW - 8;
}

setRect({
top: r.top + scrollY,
left: r.left + scrollX,
width: r.width,
height: r.height,
popTop,
popLeft,
placement,
});
};

const check = () => {
el = document.querySelector(step.target);
if (el) {
update();
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
};

check();
const observer = new MutationObserver(check);
observer.observe(document.body, { childList: true, subtree: true });

window.addEventListener('resize', update);
window.addEventListener('scroll', update);

return () => {
observer.disconnect();
window.removeEventListener('resize', update);
window.removeEventListener('scroll', update);
};
}, [run, stepIndex, step, navigate, location.pathname]);

if (!run || !step || !rect) return null;

const highlightStyle = {
top: rect.top - 4,
left: rect.left - 4,
width: rect.width + 8,
height: rect.height + 8,
};

const popoverStyle = {
top: rect.popTop,
left: rect.popLeft,
};

const isLast = stepIndex === steps.length - 1;

return createPortal(
<div className="tour-overlay" onClick={exit}>
<div className="tour-highlight" style={highlightStyle} />
<div
className={`tour-popover ${rect.placement}`}
style={popoverStyle}
onClick={(e) => e.stopPropagation()}
>
<div>{step.content}</div>
<Space className="tour-controls" style={{ marginTop: '0.5rem' }}>
<Button size="small" onClick={exit} aria-label="Exit tour">
Exit
</Button>
<Button
size="small"
onClick={prev}
disabled={stepIndex === 0}
aria-label="Previous step"
>
Previous
</Button>
<span
style={{ flex: 1, textAlign: 'center' }}
>{`Step ${stepIndex + 1} of ${steps.length}`}</span>
<Button
size="small"
type="primary"
onClick={isLast ? exit : next}
aria-label={isLast ? 'Finish tour' : 'Next step'}
>
{isLast ? 'Finish' : 'Next'}
</Button>
</Space>
</div>
</div>,
document.body
);
};

export default GuidedTour;
44 changes: 41 additions & 3 deletions client/src/components/Navbar.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import React, { useState } from 'react';
import { Layout, Button, Space, Drawer, Grid, Switch } from 'antd';
import { MenuOutlined, BulbOutlined } from '@ant-design/icons';
import {
MenuOutlined,
BulbOutlined,
QuestionCircleOutlined,
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { useTheme } from '../context/ThemeContext';
import { useTour } from '../context/TourContext';

const { Header } = Layout;

const Navbar = () => {
const navigate = useNavigate();
const { user, setUser } = useAuth();
const { dark, toggleTheme } = useTheme();
const { startTour, completed } = useTour();
const role = user?.role;

const [drawerOpen, setDrawerOpen] = useState(false);
Expand Down Expand Up @@ -49,13 +55,21 @@ const Navbar = () => {
</div>
{screens.md ? (
<Space>
<Button type="default" onClick={() => navigate('/insights')}>
<Button
type="default"
data-tour="insights-nav"
onClick={() => navigate('/insights')}
>
Insights
</Button>
<Button type="default" onClick={() => navigate('/wallet')}>
Wallet
</Button>
<Button type="default" onClick={handleProfile}>
<Button
data-tour="profile-menu"
type="default"
onClick={handleProfile}
>
Profile
</Button>
<Switch
Expand All @@ -65,6 +79,14 @@ const Navbar = () => {
checkedChildren={<BulbOutlined aria-hidden="true" />}
unCheckedChildren={<BulbOutlined aria-hidden="true" />}
/>
{!completed && (
<Button
type="text"
aria-label="Start tour"
icon={<QuestionCircleOutlined aria-hidden="true" />}
onClick={startTour}
/>
)}
<Button type="primary" onClick={handleLogout}>
Logout
</Button>
Expand Down Expand Up @@ -94,6 +116,7 @@ const Navbar = () => {
<Button
type="default"
block
data-tour="insights-nav"
style={{ marginBottom: 8 }}
onClick={() => {
navigate('/insights');
Expand All @@ -114,6 +137,7 @@ const Navbar = () => {
Wallet
</Button>
<Button
data-tour="profile-menu"
type="default"
block
style={{ marginBottom: 8 }}
Expand All @@ -133,6 +157,20 @@ const Navbar = () => {
unCheckedChildren={<BulbOutlined aria-hidden="true" />}
/>
</div>
{!completed && (
<Button
block
type="text"
style={{ marginBottom: 8 }}
icon={<QuestionCircleOutlined aria-hidden="true" />}
onClick={() => {
startTour();
setDrawerOpen(false);
}}
>
Start Tour
</Button>
)}
<Button type="primary" block onClick={handleLogout}>
Logout
</Button>
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/organization/DiscordIntegration.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const DiscordIntegration = ({ uid, profile, setProfile }) => {
};

return (
<>
<div data-tour="discord-integration">
<Paragraph style={{ marginTop: '1rem' }}>
Connect Discord to post new bounties automatically to your channel.
</Paragraph>
Expand Down Expand Up @@ -138,7 +138,7 @@ const DiscordIntegration = ({ uid, profile, setProfile }) => {
Add to Discord
</Button>
)}
</>
</div>
);
};

Expand Down
4 changes: 2 additions & 2 deletions client/src/components/organization/GitHubIntegration.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const GitHubIntegration = ({ uid, profile, setProfile }) => {
};

return (
<>
<div data-tour="github-integration">
<Paragraph style={{ marginTop: '1rem' }}>
Link a GitHub repo to sync issues labeled <code>dao</code> as bounties.
</Paragraph>
Expand Down Expand Up @@ -87,7 +87,7 @@ const GitHubIntegration = ({ uid, profile, setProfile }) => {
Authorize GitHub
</Button>
)}
</>
</div>
);
};

Expand Down
Loading