Files
tuxmate-abusoww/src/components/header/HowItWorks.tsx

218 lines
11 KiB
TypeScript

'use client';
import { useState, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { HelpCircle, X } from 'lucide-react';
import { analytics } from '@/lib/analytics';
// The "?" help modal - shows keyboard shortcuts and how to use the app
export function HowItWorks() {
const [isOpen, setIsOpen] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const [mounted, setMounted] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
const handleOpen = () => {
setIsClosing(false);
setIsOpen(true);
analytics.helpOpened();
};
const handleClose = () => {
setIsClosing(true);
analytics.helpClosed();
// Wait for exit animation to finish
setTimeout(() => {
setIsOpen(false);
setIsClosing(false);
}, 200);
};
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setMounted(true);
}, []);
// Lock body scroll when modal is open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
// Global keyboard shortcut: ? to toggle modal
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Ignore if typing in input
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
// Skip if Ctrl/Alt/Meta are pressed (Shift is allowed for ?)
if (e.ctrlKey || e.altKey || e.metaKey) return;
if (e.key === '?' || (e.shiftKey && e.key === '/')) {
e.preventDefault();
if (isOpen) {
handleClose();
} else {
handleOpen();
}
}
// Close on Escape
if (e.key === 'Escape' && isOpen) {
handleClose();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isOpen]);
const modal = (
<>
{/* Backdrop with blur */}
<div
className="fixed inset-0 bg-black/30 backdrop-blur-sm z-[99998]"
onClick={handleClose}
style={{
animation: isClosing
? 'fadeOut 0.2s ease-out forwards'
: 'fadeIn 0.25s ease-out'
}}
/>
{/* Modal - AccessGuide style: rectangular with left border accent */}
<div
role="dialog"
aria-modal="true"
aria-labelledby="how-it-works-title"
className="fixed bg-[var(--bg-primary)] border-l-4 z-[99999]"
style={{
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
borderRadius: '0 4px 4px 0',
borderLeftColor: 'var(--accent)',
width: '620px',
maxWidth: 'calc(100vw - 32px)',
maxHeight: 'min(85vh, 720px)',
display: 'flex',
flexDirection: 'column',
animation: isClosing
? 'modalSlideOut 0.2s ease-out forwards'
: 'modalSlideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1)',
overflow: 'hidden',
boxShadow: '0 16px 48px -8px rgba(0, 0, 0, 0.25)',
}}
>
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--border-secondary)]">
<h3 id="how-it-works-title" className="text-base font-semibold text-[var(--text-primary)]">
Help
</h3>
<button
onClick={handleClose}
className="p-1.5 -mr-1 hover:bg-[var(--bg-hover)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-6" style={{ scrollbarGutter: 'stable' }}>
{/* Shortcuts - AccessGuide style */}
<section>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-4 pl-3 border-l-2 border-[var(--accent)]">Keyboard Shortcuts</h4>
<div className="grid grid-cols-2 gap-x-8 gap-y-2">
{[
['↑↓←→', 'Navigate through apps'],
['hjkl', 'Vim-style navigation'],
['Space', 'Select or deselect app'],
['/', 'Focus search box'],
['y', 'Copy install command'],
['d', 'Download install script'],
['c', 'Clear all selections'],
['t', 'Toggle light/dark theme'],
['Tab', 'Preview current selection'],
['Esc', 'Close this modal'],
['?', 'Show this help'],
['1 / 2', 'Switch AUR helper (yay/paru)'],
].map(([key, desc]) => (
<div key={key} className="flex items-center gap-3 text-sm">
<kbd className="inline-flex items-center justify-center min-w-[52px] px-2 py-1 bg-[var(--bg-secondary)] border-l-2 border-[var(--accent)] text-xs font-mono text-[var(--text-secondary)]">
{key}
</kbd>
<span className="text-[var(--text-muted)]">{desc}</span>
</div>
))}
</div>
</section>
{/* Getting Started - AccessGuide style */}
<section>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3 pl-3 border-l-2 border-[var(--accent)]">Getting Started</h4>
<ol className="space-y-2 text-sm text-[var(--text-muted)] leading-relaxed">
<li>
<strong className="text-[var(--text-secondary)]">1. Pick your distro</strong> Select your Linux distribution from the dropdown at the top. This determines which package manager commands TuxMate generates for you.
</li>
<li>
<strong className="text-[var(--text-secondary)]">2. Select apps</strong> Browse the categories and click on apps to add them to your selection. Selected apps are highlighted. Use keyboard shortcuts to navigate faster.
</li>
<li>
<strong className="text-[var(--text-secondary)]">3. Copy or download</strong> Copy the generated install command to your clipboard, or download a complete shell script. Downloaded scripts include error handling and can install multiple apps at once.
</li>
<li>
<strong className="text-[var(--text-secondary)]">4. Run in terminal</strong> Open your terminal, paste the command (Ctrl+Shift+V), and press Enter. The script will handle the rest.
</li>
</ol>
</section>
{/* Notes - AccessGuide style */}
<section>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3 pl-3 border-l-2 border-[var(--accent)]">Good to Know</h4>
<ul className="space-y-2 text-sm text-[var(--text-muted)] leading-relaxed">
<li>
<strong className="text-[var(--text-secondary)]">Greyed out apps</strong> aren&apos;t available in your distro&apos;s official repositories. Try switching to Flatpak or Snap in the dropdown, or hover the info icon next to the app for alternative installation methods.
</li>
<li>
<strong className="text-[var(--text-secondary)]">Arch Linux users</strong> Some packages come from the AUR. TuxMate uses yay or paru as the AUR helper. Press 1 or 2 anytime to switch between them.
</li>
<li>
<strong className="text-[var(--text-secondary)]">Homebrew users</strong> Works on both macOS and Linux. GUI apps (Casks) are macOS-only and will be automatically skipped on Linux. The script handles this check for you.
</li>
<li>
<strong className="text-[var(--text-secondary)]">Auto-save</strong> Your app selections are saved automatically in your browser. Come back anytime and your selections will still be there.
</li>
<li>
<strong className="text-[var(--text-secondary)]">Script Safety</strong> Downloaded scripts are robust and idempotent. They include error handling, network retries, and system checks. Run them with <code className="px-1 py-0.5 bg-[var(--bg-secondary)] border-l-2 border-[var(--accent)] text-xs font-mono">bash tuxmate-*.sh</code> to safely install your selection.
</li>
</ul>
</section>
</div>
</div>
</>
);
return (
<>
<button
ref={triggerRef}
onClick={handleOpen}
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs sm:text-sm transition-all duration-200 hover:scale-105 ${isOpen
? 'bg-[var(--accent)]/20 text-[var(--text-primary)]'
: 'bg-[var(--bg-tertiary)] text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]'}`}
>
<HelpCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span className="whitespace-nowrap">Help</span>
</button>
{isOpen && mounted && typeof document !== 'undefined' && createPortal(modal, document.body)}
</>
);
}