feat: Implement comprehensive help system with contextual icons (#122)
* feat: implement comprehensive help system with contextual icons - Add HelpModal component with navigation sidebar and 7 help sections - Add HelpButton component for main header controls - Add ContextualHelpIcon component for contextual help throughout UI - Add help icons to all major UI sections: - Settings modals (Server Settings, General Settings) - Sync button with update system help - Tab headers (Available, Downloaded, Installed Scripts) - FilterBar and CategorySidebar - Add comprehensive help content covering: - Server Settings: PVE server management, auth types, color coding - General Settings: Save filters, GitHub integration, authentication - Sync Button: Script metadata syncing explanation - Available Scripts: Browsing, filtering, downloading - Downloaded Scripts: Local script management and updates - Installed Scripts: Auto-detection feature (primary focus), manual management - Update System: Automatic/manual update process, release notes - Improve VersionDisplay: remove 'Update Available' text, add 'Release Notes:' label - Make help icons more noticeable with increased size - Fix dark theme compatibility issues in help modal * fix: resolve linting errors in HelpModal component - Remove unused Filter import - Fix unescaped entities by replacing apostrophes and quotes with HTML entities - All linting errors resolved * feat: implement release notes modal system - Add getAllReleases API endpoint to fetch GitHub releases with notes - Create ReleaseNotesModal component with localStorage version tracking - Add sticky Footer component with release notes link - Make version badge clickable to open release notes - Auto-show modal after updates when version changes - Track last seen version in localStorage to prevent repeated shows - Highlight new version in modal when opened after update - Add manual access via footer and version badge clicks * fix: use nullish coalescing operator in ReleaseNotesModal - Replace logical OR (||) with nullish coalescing (??) operator - Fixes ESLint prefer-nullish-coalescing rule violation - Ensures build passes successfully
This commit is contained in:
committed by
GitHub
parent
24afce49a3
commit
aaa09b4745
202
src/app/_components/ReleaseNotesModal.tsx
Normal file
202
src/app/_components/ReleaseNotesModal.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { api } from '~/trpc/react';
|
||||
import { Button } from './ui/button';
|
||||
import { Badge } from './ui/badge';
|
||||
import { X, ExternalLink, Calendar, Tag, Loader2 } from 'lucide-react';
|
||||
|
||||
interface ReleaseNotesModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
highlightVersion?: string;
|
||||
}
|
||||
|
||||
interface Release {
|
||||
tagName: string;
|
||||
name: string;
|
||||
publishedAt: string;
|
||||
htmlUrl: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
// Helper functions for localStorage
|
||||
const getLastSeenVersion = (): string | null => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem('LAST_SEEN_RELEASE_VERSION');
|
||||
};
|
||||
|
||||
const markVersionAsSeen = (version: string): void => {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.setItem('LAST_SEEN_RELEASE_VERSION', version);
|
||||
};
|
||||
|
||||
export function ReleaseNotesModal({ isOpen, onClose, highlightVersion }: ReleaseNotesModalProps) {
|
||||
const [currentVersion, setCurrentVersion] = useState<string | null>(null);
|
||||
const { data: releasesData, isLoading, error } = api.version.getAllReleases.useQuery(undefined, {
|
||||
enabled: isOpen
|
||||
});
|
||||
const { data: versionData } = api.version.getCurrentVersion.useQuery(undefined, {
|
||||
enabled: isOpen
|
||||
});
|
||||
|
||||
// Get current version when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen && versionData?.success && versionData.version) {
|
||||
setCurrentVersion(versionData.version);
|
||||
}
|
||||
}, [isOpen, versionData]);
|
||||
|
||||
// Mark version as seen when modal closes
|
||||
const handleClose = () => {
|
||||
if (currentVersion) {
|
||||
markVersionAsSeen(currentVersion);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const releases: Release[] = releasesData?.success ? releasesData.releases ?? [] : [];
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-card rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] flex flex-col border border-border">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<Tag className="h-6 w-6 text-blue-600" />
|
||||
<h2 className="text-2xl font-bold text-card-foreground">Release Notes</h2>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleClose}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-hidden flex flex-col">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
||||
<span className="text-muted-foreground">Loading release notes...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : error || !releasesData?.success ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-center">
|
||||
<p className="text-destructive mb-2">Failed to load release notes</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{releasesData?.error ?? 'Please try again later'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : releases.length === 0 ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<p className="text-muted-foreground">No releases found</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
{releases.map((release, index) => {
|
||||
const isHighlighted = highlightVersion && release.tagName.replace('v', '') === highlightVersion;
|
||||
const isLatest = index === 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={release.tagName}
|
||||
className={`border rounded-lg p-6 ${
|
||||
isHighlighted
|
||||
? 'border-blue-500 bg-blue-50/10 dark:bg-blue-950/10'
|
||||
: 'border-border bg-card'
|
||||
} ${isLatest ? 'ring-2 ring-primary/20' : ''}`}
|
||||
>
|
||||
{/* Release Header */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-xl font-semibold text-card-foreground">
|
||||
{release.name || release.tagName}
|
||||
</h3>
|
||||
{isLatest && (
|
||||
<Badge variant="default" className="text-xs">
|
||||
Latest
|
||||
</Badge>
|
||||
)}
|
||||
{isHighlighted && (
|
||||
<Badge variant="secondary" className="text-xs bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||
New
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<Tag className="h-4 w-4" />
|
||||
<span>{release.tagName}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>
|
||||
{new Date(release.publishedAt).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
asChild
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<a
|
||||
href={release.htmlUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="View on GitHub"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Release Body */}
|
||||
{release.body && (
|
||||
<div className="prose prose-sm max-w-none dark:prose-invert">
|
||||
<div className="whitespace-pre-wrap text-sm text-card-foreground leading-relaxed">
|
||||
{release.body}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between p-6 border-t border-border bg-muted/30">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{currentVersion && (
|
||||
<span>Current version: <span className="font-medium text-card-foreground">v{currentVersion}</span></span>
|
||||
)}
|
||||
</div>
|
||||
<Button onClick={handleClose} variant="default">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Export helper functions for use in other components
|
||||
export { getLastSeenVersion, markVersionAsSeen };
|
||||
Reference in New Issue
Block a user