Skip to content

Commit 8e5335f

Browse files
Merge pull request #340 from CivicDataLab/walkthrough
Add basic touring feature
2 parents 6948872 + a7808a4 commit 8e5335f

File tree

17 files changed

+1829
-17
lines changed

17 files changed

+1829
-17
lines changed

app/[locale]/(user)/components/Content.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { SearchInput, Spinner, Tag, Text } from 'opub-ui';
1010
import { GraphQL } from '@/lib/api';
1111
import { cn } from '@/lib/utils';
1212
import Styles from '../page.module.scss';
13+
import { useTourTrigger } from '@/hooks/use-tour-trigger';
1314

1415
const statsInfo: any = graphql(`
1516
query StatsList {
@@ -34,6 +35,10 @@ const statsInfo: any = graphql(`
3435

3536
export const Content = () => {
3637
const router = useRouter();
38+
39+
// Enable tour for first-time users
40+
useTourTrigger(true, 1500);
41+
3742
const Stats: { data: any; isLoading: any } = useQuery([`statsDetails`], () =>
3843
GraphQL(statsInfo, {}, [])
3944
);
@@ -104,7 +109,11 @@ export const Content = () => {
104109
) : (
105110
<div className="flex flex-wrap items-center gap-4 md:gap-0 lg:gap-0 ">
106111
{Metrics.map((item, index) => (
107-
<Link key={`${item.label}_${index}`} href={item.link}>
112+
<Link
113+
key={`${item.label}_${index}`}
114+
href={item.link}
115+
data-tour={index === 0 ? 'datasets-link' : index === 1 ? 'usecases-link' : index === 2 ? 'publishers-link' : undefined}
116+
>
108117
<div
109118
key={index}
110119
className="flex flex-col border-x-[1px] border-solid border-tertiaryAccent px-4"
@@ -123,7 +132,7 @@ export const Content = () => {
123132
))}
124133
</div>
125134
)}
126-
<div className="w-full">
135+
<div className="w-full" data-tour="search-bar">
127136
<SearchInput
128137
className={cn(Styles.Search)}
129138
onSubmit={handleSearch}

app/[locale]/(user)/components/ListingComponent.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { fetchData } from '@/fetch';
2323
import { cn, formatDate } from '@/lib/utils';
2424
import Filter from '../datasets/components/FIlter/Filter';
2525
import Styles from '../datasets/dataset.module.scss';
26+
import { useTourTrigger } from '@/hooks/use-tour-trigger';
2627

2728
// Helper function to strip markdown and HTML tags for card preview
2829
const stripMarkdown = (markdown: string): string => {
@@ -256,6 +257,8 @@ const ListingComponent: React.FC<ListingProps> = ({
256257
redirectionURL,
257258
lockedFilters = {},
258259
}) => {
260+
useTourTrigger(true, 1500);
261+
259262
const [facets, setFacets] = useState<{
260263
results: any[];
261264
total: number;
@@ -397,7 +400,7 @@ const ListingComponent: React.FC<ListingProps> = ({
397400

398401
<div className="mt-5 lg:mt-10">
399402
<div className="row mb-16 flex gap-5 ">
400-
<div className="hidden min-w-64 max-w-64 lg:block">
403+
<div className="hidden min-w-64 max-w-64 lg:block" data-tour="filters">
401404
<Filter
402405
options={filterOptions}
403406
setSelectedOptions={handleFilterChange}
@@ -408,7 +411,7 @@ const ListingComponent: React.FC<ListingProps> = ({
408411

409412
<div className="flex w-full flex-col gap-4 px-2">
410413
<div className="flex flex-wrap items-center justify-between gap-5 rounded-2 py-2 lg:flex-nowrap">
411-
<div className="w-full md:block">
414+
<div className="w-full md:block" data-tour="search">
412415
<SearchInput
413416
key={queryParams.query}
414417
label="Search"
@@ -468,7 +471,7 @@ const ListingComponent: React.FC<ListingProps> = ({
468471
/>
469472
</Button>
470473
</div>
471-
<div className="flex items-center gap-2">
474+
<div className="flex items-center gap-2" data-tour="sort">
472475
<Select
473476
label=""
474477
labelInline
@@ -541,7 +544,7 @@ const ListingComponent: React.FC<ListingProps> = ({
541544
onPageSizeChange={handlePageSizeChange}
542545
view={view}
543546
>
544-
{datasetDetails.map((item: any) => {
547+
{datasetDetails.map((item: any, index: number) => {
545548
const image = item.is_individual_dataset
546549
? item?.user?.profile_picture
547550
? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${item.user.profile_picture}`
@@ -654,6 +657,7 @@ const ListingComponent: React.FC<ListingProps> = ({
654657
}
655658
iconColor="warning"
656659
href={`${redirectionURL}/${item.id}`}
660+
data-tour={index === 0 && type === 'dataset' ? 'dataset-card' : index === 0 && type === 'usecase' ? 'usecase-card' : undefined}
657661
/>
658662
);
659663
})}

app/[locale]/(user)/datasets/[datasetIdentifier]/DatasetDetailsPage.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Metadata from './components/Metadata';
1313
import PrimaryData from './components/PrimaryData';
1414
import Resources from './components/Resources';
1515
import SimilarDatasets from './components/SimilarDatasets';
16+
import { useTourTrigger } from '@/hooks/use-tour-trigger';
1617

1718
const datasetQuery: any = graphql(`
1819
query getDataset($datasetId: UUID!) {
@@ -84,6 +85,9 @@ export default function DatasetDetailsPage({
8485
}: {
8586
datasetId: string;
8687
}) {
88+
// Enable tour for first-time users
89+
useTourTrigger(true, 1500);
90+
8791
const Datasetdetails: { data: any; isLoading: any } = useQuery(
8892
[`details_${datasetId}`],
8993
() => GraphQL(datasetQuery, {}, { datasetId: datasetId })
@@ -114,7 +118,7 @@ export default function DatasetDetailsPage({
114118
]}
115119
/>
116120
<div className="flex">
117-
<div className="w-full gap-10 border-r-2 border-solid border-greyExtralight p-6 lg:w-3/4 lg:p-10">
121+
<div className="w-full gap-10 border-r-2 border-solid border-greyExtralight p-6 lg:w-3/4 lg:p-10" data-tour="dataset-info">
118122
{Datasetdetails.isLoading ? (
119123
<div className=" mt-8 flex justify-center">
120124
<Spinner />
@@ -125,9 +129,9 @@ export default function DatasetDetailsPage({
125129
isLoading={Datasetdetails.isLoading}
126130
/>
127131
)}
128-
<Details />
129-
<Resources />
130-
<SimilarDatasets />
132+
<Details data-tour="preview" />
133+
<Resources data-tour="download-button" />
134+
<SimilarDatasets data-tour="related-datasets" />
131135
</div>
132136
<div className=" hidden w-1/4 gap-10 px-7 py-10 lg:block">
133137
{Datasetdetails.isLoading ? (

app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ActionBar } from './components/action-bar';
1515
import { Content } from './components/content';
1616
import { Navigation } from './components/navigate-org-datasets';
1717
import { formatDate } from '@/lib/utils';
18+
import { useTourTrigger } from '@/hooks/use-tour-trigger';
1819

1920
const allDatasetsQueryDoc: any = graphql(`
2021
query allDatasetsQuery($filters: DatasetFilter, $order: DatasetOrder) {
@@ -69,6 +70,8 @@ export default function DatasetPage({
6970
}: {
7071
params: { entityType: string; entitySlug: string };
7172
}) {
73+
useTourTrigger(true, 1500);
74+
7275
const router = useRouter();
7376

7477
const [navigationTab, setNavigationTab] = useQueryState('tab', parseAsString);
@@ -252,6 +255,7 @@ export default function DatasetPage({
252255
<Navigation
253256
setNavigationTab={setNavigationTab}
254257
options={navigationOptions}
258+
data-tour="sidebar"
255259
/>
256260

257261
{AllDatasetsQuery.data?.datasets.length > 0 ? (
@@ -264,13 +268,15 @@ export default function DatasetPage({
264268
content: 'Add New Dataset',
265269
onAction: () => CreateDatasetMutation.mutate(),
266270
}}
271+
data-tour="create-dataset"
267272
/>
268273

269274
<DataTable
270275
columns={datasetsListColumns}
271276
rows={generateTableData(AllDatasetsQuery.data.datasets)}
272277
hideSelection
273278
hideViewSelector
279+
data-tour="my-datasets"
274280
/>
275281
</div>
276282
) : AllDatasetsQuery.isLoading ? (

app/[locale]/dashboard/page.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ import BreadCrumbs from '@/components/BreadCrumbs';
77
import { Icons } from '@/components/icons';
88
import { Loading } from '@/components/loading';
99
import { useDashboardStore } from '@/config/store';
10+
import { useTourTrigger } from '@/hooks/use-tour-trigger';
1011

1112
const UserDashboard = () => {
13+
// Enable tour for first-time users
14+
useTourTrigger(true, 1500);
15+
1216
const { userDetails } = useDashboardStore();
1317
const list = [
1418
{
@@ -43,12 +47,13 @@ const UserDashboard = () => {
4347
<Text variant="headingXl"> User Dashboard</Text>
4448
</div>
4549
<div className="flex-1 ">
46-
<div className="flex flex-wrap items-center gap-6 md:flex-nowrap lg:flex-nowrap">
50+
<div className="flex flex-wrap items-center gap-6 md:flex-nowrap lg:flex-nowrap" data-tour="sidebar">
4751
{list.map((item, index) => (
4852
<Link
4953
key={index}
5054
href={item.path}
5155
className=" flex max-h-56 min-h-56 w-full flex-col items-center justify-center gap-3 rounded-4 bg-greyExtralight p-4 "
56+
data-tour={index === 0 ? 'my-datasets' : undefined}
5257
>
5358
<Icon source={item.icon} size={60} color="highlight" />
5459
<Text variant="headingLg">{item.label}</Text>

components/Tour/TourButton.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { useManualTour } from '@/hooks/use-tour-trigger';
5+
import { HelpCircle } from 'lucide-react';
6+
7+
interface TourButtonProps {
8+
variant?: 'icon' | 'text' | 'both';
9+
className?: string;
10+
label?: string;
11+
}
12+
13+
/**
14+
* Button component to manually trigger the tour for current page
15+
* Can be placed in header, footer, or anywhere on the page
16+
*/
17+
export function TourButton({
18+
variant = 'both',
19+
className = '',
20+
label = 'Take a tour'
21+
}: TourButtonProps) {
22+
const { startTour, hasTour, isTourRunning } = useManualTour();
23+
24+
// Don't render if no tour available for current page
25+
if (!hasTour) {
26+
return null;
27+
}
28+
29+
// Don't render if tour is already running
30+
if (isTourRunning) {
31+
return null;
32+
}
33+
34+
const baseClasses = 'inline-flex items-center gap-2 rounded-md transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-actionPrimaryBasicDefault';
35+
36+
const variantClasses = {
37+
icon: 'p-2 hover:bg-actionSecondaryBasicHovered',
38+
text: 'px-4 py-2 text-sm font-medium text-textMedium hover:text-textDefault hover:bg-actionSecondaryBasicHovered',
39+
both: 'px-4 py-2 text-sm font-medium bg-actionSecondaryBasicDefault hover:bg-actionSecondaryBasicHovered text-textDefault',
40+
};
41+
42+
return (
43+
<button
44+
onClick={startTour}
45+
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
46+
aria-label={label}
47+
title={label}
48+
>
49+
<HelpCircle className="w-4 h-4" />
50+
{(variant === 'text' || variant === 'both') && <span>{label}</span>}
51+
</button>
52+
);
53+
}
54+
55+
/**
56+
* Floating action button for tour - positioned fixed on screen
57+
*/
58+
export function FloatingTourButton() {
59+
const { startTour, hasTour, isTourRunning } = useManualTour();
60+
61+
if (!hasTour || isTourRunning) {
62+
return null;
63+
}
64+
65+
return (
66+
<button
67+
onClick={startTour}
68+
className="fixed bottom-6 right-6 z-50 flex items-center justify-center w-14 h-14 rounded-full bg-actionPrimaryBasicDefault text-textOnBGDefault shadow-lg hover:bg-actionPrimaryBasicHovered transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-actionPrimaryBasicDefault"
69+
aria-label="Take a tour"
70+
title="Take a tour"
71+
>
72+
<HelpCircle className="w-6 h-6" />
73+
</button>
74+
);
75+
}

0 commit comments

Comments
 (0)