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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"prettier": "^3.4.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-joyride": "^2.8.2",
"react-markdown": "^9.0.3",
"react-router-dom": "^7.5.2",
"sharp": "^0.33.2",
Expand All @@ -64,6 +65,7 @@
"@types/papaparse": "^5.3.14",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@types/react-joyride": "^2.0.5",
"@types/sharp": "^0.32.0",
"@types/uuid": "^9.0.8",
"@vitejs/plugin-react": "^4.3.1",
Expand Down
23 changes: 20 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import HtmlMinifyBeautify from "./components/HtmlMinifyBeautify";
import SpotlightSearch from "./components/SpotlightSearch";
import KeyboardShortcuts from "./components/KeyboardShortcuts";
import Credits from "./components/Credits";
import GuidedTour, { useShouldShowTour } from "./components/GuidedTour";
import {
DndContext,
closestCenter,
Expand Down Expand Up @@ -197,6 +198,7 @@ function Layout({ tools: defaultTools }: { tools: Tool[] }) {
const [isSpotlightOpen, setIsSpotlightOpen] = useState(false);
const [isShortcutsOpen, setIsShortcutsOpen] = useState(false);
const [isSidebarExpanded, setIsSidebarExpanded] = useState(true);
const { shouldShow: shouldShowTour, setShouldShow: setShowTour } = useShouldShowTour();
const [tools, setTools] = useState<Tool[]>(() => {
const savedOrder = localStorage.getItem("toolsOrder");
if (savedOrder) {
Expand Down Expand Up @@ -236,6 +238,11 @@ function Layout({ tools: defaultTools }: { tools: Tool[] }) {
e.preventDefault();
setIsShortcutsOpen(true);
}
// Command/Control + Shift + ? for tour
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === "?") {
e.preventDefault();
setShowTour(true);
}
};

document.addEventListener("keydown", handleKeyDown);
Expand Down Expand Up @@ -289,6 +296,7 @@ function Layout({ tools: defaultTools }: { tools: Tool[] }) {
<RouterLink
to="/"
className="text-xl font-bold text-gray-800 no-underline"
data-tour="sidebar-brand"
>
DevUtils
</RouterLink>
Expand All @@ -308,20 +316,23 @@ function Layout({ tools: defaultTools }: { tools: Tool[] }) {
onClick={() => setIsSpotlightOpen(true)}
className="p-1.5 hover:bg-gray-100 rounded"
title="Search (⌘K)"
data-tour="search-button"
>
<Search size={18} />
</button>
<button
onClick={() => setIsShortcutsOpen(true)}
className="p-1.5 hover:bg-gray-100 rounded"
title="Keyboard Shortcuts (⌘?)"
data-tour="shortcuts-button"
>
<Keyboard size={18} />
</button>
<button
onClick={() => setIsSidebarExpanded(false)}
className="p-1.5 hover:bg-gray-100 rounded"
title="Collapse Sidebar"
data-tour="sidebar-toggle"
>
<PanelLeftClose size={18} />
</button>
Expand All @@ -330,7 +341,7 @@ function Layout({ tools: defaultTools }: { tools: Tool[] }) {
</div>
</div>
{isSidebarExpanded && (
<div className="flex space-x-2">
<div className="flex space-x-2" data-tour="github-links">
<a
href="https://github.com/nadimtuhin/devutils"
target="_blank"
Expand All @@ -352,7 +363,7 @@ function Layout({ tools: defaultTools }: { tools: Tool[] }) {
</div>
)}
</div>
<nav className="p-2 flex-1">
<nav className="p-2 flex-1" data-tour="tool-list">
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
Expand Down Expand Up @@ -380,7 +391,7 @@ function Layout({ tools: defaultTools }: { tools: Tool[] }) {
</div>

{/* Main Content */}
<div className="flex-1 overflow-auto">
<div className="flex-1 overflow-auto" data-tour="main-content">
<div className="p-8">
<Routes>
<Route path="/" element={<Navigate to="/unix-time" replace />} />
Expand Down Expand Up @@ -415,6 +426,12 @@ function Layout({ tools: defaultTools }: { tools: Tool[] }) {
isOpen={isShortcutsOpen}
onClose={() => setIsShortcutsOpen(false)}
/>

{/* Guided Tour */}
<GuidedTour
isOpen={shouldShowTour}
onClose={() => setShowTour(false)}
/>
</div>
);
}
Expand Down
147 changes: 147 additions & 0 deletions src/components/GuidedTour.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React, { useState, useEffect } from 'react';
import Joyride, { Step, CallBackProps, STATUS, EVENTS } from 'react-joyride';

interface GuidedTourProps {
isOpen: boolean;
onClose: () => void;
}

const TOUR_STORAGE_KEY = 'devutils-tour-completed';

const steps: Step[] = [
{
target: '[data-tour="sidebar-brand"]',
content: 'Welcome to DevUtils! This is your collection of handy development tools.',
placement: 'right',
},
{
target: '[data-tour="search-button"]',
content: 'Use the search button (⌘K) to quickly find any tool you need.',
placement: 'bottom',
},
{
target: '[data-tour="shortcuts-button"]',
content: 'View all keyboard shortcuts to speed up your workflow.',
placement: 'bottom',
},
{
target: '[data-tour="sidebar-toggle"]',
content: 'Toggle the sidebar to maximize your workspace when needed.',
placement: 'bottom',
},
{
target: '[data-tour="tool-list"]',
content: 'Browse all available tools here. You can drag to reorder them based on your preference!',
placement: 'right',
},
{
target: '[data-tour="main-content"]',
content: 'This is where the magic happens - each tool provides instant conversion and formatting capabilities.',
placement: 'left',
},
{
target: '[data-tour="github-links"]',
content: 'Don\'t forget to star the repository if you find DevUtils helpful!',
placement: 'right',
},
];

export default function GuidedTour({ isOpen, onClose }: GuidedTourProps) {
const [run, setRun] = useState(false);

useEffect(() => {
if (isOpen) {
setRun(true);
}
}, [isOpen]);

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

if ([EVENTS.STEP_AFTER, EVENTS.TARGET_NOT_FOUND].includes(type)) {
// Update state to advance the tour
}

if ([STATUS.FINISHED, STATUS.SKIPPED].includes(status)) {
// Mark tour as completed
localStorage.setItem(TOUR_STORAGE_KEY, 'true');
setRun(false);
onClose();
}
};

return (
<Joyride
callback={handleJoyrideCallback}
continuous={true}
run={run}
scrollToFirstStep={true}
showProgress={true}
showSkipButton={true}
steps={steps}
styles={{
options: {
primaryColor: '#3b82f6',
width: 320,
zIndex: 1000,
},
spotlight: {
borderRadius: 8,
},
tooltip: {
borderRadius: 8,
fontSize: 14,
},
tooltipContainer: {
textAlign: 'left',
},
tooltipTitle: {
fontSize: 16,
fontWeight: 600,
marginBottom: 8,
},
}}
locale={{
back: 'Back',
close: 'Close',
last: 'Finish',
next: 'Next',
open: 'Open the dialog',
skip: 'Skip tour',
}}
/>
);
}

// Hook to check if tour should be shown
export function useShouldShowTour() {
const [shouldShow, setShouldShow] = useState(false);

useEffect(() => {
const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY);
if (!hasCompletedTour) {
// Show tour after a short delay to ensure UI is rendered
const timer = setTimeout(() => {
setShouldShow(true);
}, 1000);
return () => clearTimeout(timer);
}
}, []);

const markTourCompleted = () => {
localStorage.setItem(TOUR_STORAGE_KEY, 'true');
setShouldShow(false);
};

const resetTour = () => {
localStorage.removeItem(TOUR_STORAGE_KEY);
setShouldShow(true);
};

return {
shouldShow,
markTourCompleted,
resetTour,
setShouldShow,
};
}
4 changes: 4 additions & 0 deletions src/components/KeyboardShortcuts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ const KeyboardShortcuts: React.FC<Props> = ({ isOpen, onClose }) => {
<span>Show Keyboard Shortcuts</span>
<kbd className="px-2 py-1 bg-gray-100 rounded">⌘ ?</kbd>
</div>
<div className="flex justify-between items-center">
<span>Show Guided Tour</span>
<kbd className="px-2 py-1 bg-gray-100 rounded">⌘ ⇧ ?</kbd>
</div>
<div className="flex justify-between items-center">
<span>Navigate Tools</span>
<kbd className="px-2 py-1 bg-gray-100 rounded">↑/↓</kbd>
Expand Down