Add step library foundation and user preferences (#24)
## Summary Implements Phase 2.5 Step Library Foundation: ### Issues Completed - #3 User Preferences - export format default setting - #5 Step Categories - database table and seed data - #6 Step Library - database schema and migrations - #7 Step Library - CRUD API endpoints - #8 Step Library - rating and review system ### Changes **Backend:** - Migration 007: step_categories table with 10 seeded global categories - Migration 008: step_library, step_ratings, step_usage_log tables - Full CRUD API for step categories (/api/v1/step-categories) - Full CRUD API for step library (/api/v1/steps) with search, filters, ratings - CORS support for Railway PR environments (ALLOW_RAILWAY_ORIGINS) **Frontend:** - User preferences store (Zustand + localStorage) - Settings page at /settings with export format dropdown - Default export format applied in SessionDetailPage ### Testing - Tested in Railway PR environment - Database seeded with 7 MSP troubleshooting trees - All API endpoints verified working
This commit was merged in pull request #24.
This commit is contained in:
@@ -16,6 +16,7 @@ export function AppLayout() {
|
||||
const navItems = [
|
||||
{ path: '/trees', label: 'Trees' },
|
||||
{ path: '/sessions', label: 'Sessions' },
|
||||
{ path: '/settings', label: 'Settings' },
|
||||
]
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,17 +3,19 @@ import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Copy, Check, Eye } from 'lucide-react'
|
||||
import { sessionsApi } from '@/api'
|
||||
import { ExportPreviewModal } from '@/components/session/ExportPreviewModal'
|
||||
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
|
||||
import type { Session, SessionExport } from '@/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function SessionDetailPage() {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
const { defaultExportFormat } = useUserPreferencesStore()
|
||||
const [session, setSession] = useState<Session | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
const [exportFormat, setExportFormat] = useState<'markdown' | 'text' | 'html'>('markdown')
|
||||
const [exportFormat, setExportFormat] = useState<'markdown' | 'text' | 'html'>(defaultExportFormat)
|
||||
const [exportContent, setExportContent] = useState<string | null>(null)
|
||||
const [showPreview, setShowPreview] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
93
frontend/src/pages/SettingsPage.tsx
Normal file
93
frontend/src/pages/SettingsPage.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Settings } from 'lucide-react'
|
||||
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
|
||||
import { useThemeStore } from '@/store/themeStore'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ThemeToggle } from '@/components/common/ThemeToggle'
|
||||
|
||||
export function SettingsPage() {
|
||||
const { defaultExportFormat, setDefaultExportFormat } = useUserPreferencesStore()
|
||||
const { theme } = useThemeStore()
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<Settings className="h-8 w-8 text-primary" />
|
||||
<h1 className="text-3xl font-bold text-foreground">Settings</h1>
|
||||
</div>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Manage your application preferences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-2xl space-y-6">
|
||||
{/* Appearance Section */}
|
||||
<div className="rounded-lg border border-border bg-card p-6 shadow-sm">
|
||||
<h2 className="text-lg font-semibold text-card-foreground">Appearance</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Customize how Patherly looks on your device
|
||||
</p>
|
||||
|
||||
<div className="mt-4">
|
||||
<label className="block text-sm font-medium text-card-foreground">
|
||||
Theme
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Current: {theme.charAt(0).toUpperCase() + theme.slice(1)}
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Export Preferences Section */}
|
||||
<div className="rounded-lg border border-border bg-card p-6 shadow-sm">
|
||||
<h2 className="text-lg font-semibold text-card-foreground">Export Preferences</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Configure default settings for session exports
|
||||
</p>
|
||||
|
||||
<div className="mt-4">
|
||||
<label
|
||||
htmlFor="export-format"
|
||||
className="block text-sm font-medium text-card-foreground"
|
||||
>
|
||||
Default Export Format
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This format will be pre-selected when exporting sessions
|
||||
</p>
|
||||
<select
|
||||
id="export-format"
|
||||
value={defaultExportFormat}
|
||||
onChange={(e) => setDefaultExportFormat(e.target.value as 'markdown' | 'text' | 'html')}
|
||||
className={cn(
|
||||
'mt-2 block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-sm text-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
>
|
||||
<option value="markdown">Markdown (.md)</option>
|
||||
<option value="text">Plain Text (.txt)</option>
|
||||
<option value="html">HTML (.html)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* About Section */}
|
||||
<div className="rounded-lg border border-border bg-card p-6 shadow-sm">
|
||||
<h2 className="text-lg font-semibold text-card-foreground">About</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Patherly - Troubleshooting Decision Trees
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
"Take the path MOST traveled."
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsPage
|
||||
@@ -5,3 +5,4 @@ export { default as TreeNavigationPage } from './TreeNavigationPage'
|
||||
export { default as TreeEditorPage } from './TreeEditorPage'
|
||||
export { default as SessionHistoryPage } from './SessionHistoryPage'
|
||||
export { default as SessionDetailPage } from './SessionDetailPage'
|
||||
export { default as SettingsPage } from './SettingsPage'
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
TreeEditorPage,
|
||||
SessionHistoryPage,
|
||||
SessionDetailPage,
|
||||
SettingsPage,
|
||||
} from '@/pages'
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
@@ -59,6 +60,10 @@ export const router = createBrowserRouter([
|
||||
path: 'sessions/:id',
|
||||
element: <SessionDetailPage />,
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
element: <SettingsPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
23
frontend/src/store/userPreferencesStore.ts
Normal file
23
frontend/src/store/userPreferencesStore.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
|
||||
type ExportFormat = 'markdown' | 'text' | 'html'
|
||||
|
||||
interface UserPreferencesState {
|
||||
defaultExportFormat: ExportFormat
|
||||
setDefaultExportFormat: (format: ExportFormat) => void
|
||||
}
|
||||
|
||||
export const useUserPreferencesStore = create<UserPreferencesState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
defaultExportFormat: 'markdown',
|
||||
setDefaultExportFormat: (format) => set({ defaultExportFormat: format }),
|
||||
}),
|
||||
{
|
||||
name: 'user-preferences-storage',
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
export default useUserPreferencesStore
|
||||
Reference in New Issue
Block a user