Skip to content

Commit 5e5492e

Browse files
committed
chore: enhance UI/UX.
1 parent 7596ad3 commit 5e5492e

7 files changed

Lines changed: 136 additions & 36 deletions

File tree

frontend/src/components/Layout/Layout.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,12 @@ export default function Layout({ children }) {
7373

7474
{/* Main */}
7575
<div className="flex flex-1 overflow-hidden">
76-
{sidebarOpen && <Sidebar />}
76+
<div
77+
className="shrink-0 overflow-hidden transition-all duration-200 ease-in-out"
78+
style={{ width: sidebarOpen ? '240px' : '0px' }}
79+
>
80+
<Sidebar />
81+
</div>
7782
<main className="flex-1 overflow-auto bg-gray-50 p-6">
7883
{children}
7984
</main>

frontend/src/components/Layout/Sidebar.jsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from 'react'
1+
import { useEffect } from 'react'
22
import { Link, useLocation } from 'react-router-dom'
33
import usePages from '../../store/usePages'
44
import useBookmarks from '../../store/useBookmarks'
@@ -36,17 +36,13 @@ export default function Sidebar() {
3636
const { tree } = usePages()
3737
const { bookmarks } = useBookmarks()
3838
const { stats, fetchStats } = useActivity()
39-
const [loaded, setLoaded] = useState(false)
4039

4140
useEffect(() => {
42-
if (!loaded) {
43-
fetchStats()
44-
setLoaded(true)
45-
}
46-
}, [loaded])
41+
fetchStats()
42+
}, [])
4743

4844
return (
49-
<aside className="w-60 bg-white border-r border-gray-200 overflow-y-auto shrink-0">
45+
<aside className="w-60 min-w-60 bg-white border-r border-gray-200 overflow-y-auto">
5046
<div className="p-3">
5147
{/* Quick links */}
5248
<div className="mb-4">
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useEffect } from 'react'
2+
3+
export default function useUnsavedWarning(isDirty) {
4+
useEffect(() => {
5+
if (!isDirty) return
6+
const handler = (e) => {
7+
e.preventDefault()
8+
e.returnValue = ''
9+
}
10+
window.addEventListener('beforeunload', handler)
11+
return () => window.removeEventListener('beforeunload', handler)
12+
}, [isDirty])
13+
}

frontend/src/pages/Home.jsx

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@ import { Link } from 'react-router-dom'
33
import usePages from '../store/usePages'
44
import useTags from '../store/useTags'
55

6+
const PER_PAGE = 20
7+
68
export default function Home() {
79
const { pages, total, loading, fetchPages } = usePages()
810
const { allTags, fetchAllTags } = useTags()
9-
const [selectedTag, setSelectedTag] = useState('')
11+
const [page, setPage] = useState(1)
1012

1113
useEffect(() => {
12-
fetchPages()
14+
fetchPages(page, PER_PAGE)
1315
fetchAllTags()
14-
}, [])
16+
}, [page])
1517

16-
// Client-side tag filter (since tag filter on list isn't in the backend yet for list endpoint)
17-
const filteredPages = pages
18+
const totalPages = Math.ceil(total / PER_PAGE)
1819

1920
return (
2021
<div className="max-w-4xl mx-auto">
@@ -45,7 +46,7 @@ export default function Home() {
4546

4647
{loading ? (
4748
<p className="text-gray-500">Loading...</p>
48-
) : filteredPages.length === 0 ? (
49+
) : pages.length === 0 ? (
4950
<div className="text-center py-16">
5051
<p className="text-gray-400 text-lg mb-4">No pages yet</p>
5152
<Link
@@ -56,22 +57,59 @@ export default function Home() {
5657
</Link>
5758
</div>
5859
) : (
59-
<div className="bg-white rounded-xl shadow-sm border border-gray-200">
60-
{filteredPages.map((page, i) => (
61-
<Link
62-
key={page.id}
63-
to={`/page/${page.slug}`}
64-
className={`block px-5 py-4 hover:bg-gray-50 transition-colors ${
65-
i > 0 ? 'border-t border-gray-100' : ''
66-
}`}
67-
>
68-
<div className="font-medium text-gray-800">{page.title}</div>
69-
<div className="text-sm text-gray-400 mt-1">
70-
/{page.slug} &middot; {new Date(page.updated_at).toLocaleDateString()} &middot; {page.view_count} views
60+
<>
61+
<div className="bg-white rounded-xl shadow-sm border border-gray-200">
62+
{pages.map((p, i) => (
63+
<Link
64+
key={p.id}
65+
to={`/page/${p.slug}`}
66+
className={`block px-5 py-4 hover:bg-gray-50 transition-colors ${
67+
i > 0 ? 'border-t border-gray-100' : ''
68+
}`}
69+
>
70+
<div className="font-medium text-gray-800">{p.title}</div>
71+
<div className="text-sm text-gray-400 mt-1">
72+
/{p.slug} &middot; {new Date(p.updated_at).toLocaleDateString()} &middot; {p.view_count} views
73+
</div>
74+
</Link>
75+
))}
76+
</div>
77+
78+
{/* Pagination */}
79+
{totalPages > 1 && (
80+
<div className="flex items-center justify-center gap-2 mt-6">
81+
<button
82+
onClick={() => setPage((p) => Math.max(1, p - 1))}
83+
disabled={page === 1}
84+
className="px-3 py-1.5 text-sm rounded-lg border border-gray-200 hover:bg-gray-50 disabled:opacity-40 disabled:cursor-not-allowed"
85+
>
86+
Previous
87+
</button>
88+
<div className="flex gap-1">
89+
{Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => (
90+
<button
91+
key={p}
92+
onClick={() => setPage(p)}
93+
className={`w-8 h-8 text-sm rounded-lg ${
94+
p === page
95+
? 'bg-blue-600 text-white'
96+
: 'hover:bg-gray-100 text-gray-600'
97+
}`}
98+
>
99+
{p}
100+
</button>
101+
))}
71102
</div>
72-
</Link>
73-
))}
74-
</div>
103+
<button
104+
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
105+
disabled={page === totalPages}
106+
className="px-3 py-1.5 text-sm rounded-lg border border-gray-200 hover:bg-gray-50 disabled:opacity-40 disabled:cursor-not-allowed"
107+
>
108+
Next
109+
</button>
110+
</div>
111+
)}
112+
</>
75113
)}
76114
</div>
77115
)

frontend/src/pages/NewPage.jsx

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'
33
import usePages from '../store/usePages'
44
import api from '../api/client'
55
import Editor from '../components/Editor/Editor'
6+
import useUnsavedWarning from '../hooks/useUnsavedWarning'
67

78
export default function NewPage() {
89
const navigate = useNavigate()
@@ -13,7 +14,12 @@ export default function NewPage() {
1314
const [selectedTemplate, setSelectedTemplate] = useState(null)
1415
const [showTemplates, setShowTemplates] = useState(true)
1516
const [saving, setSaving] = useState(false)
17+
const [saved, setSaved] = useState(false)
1618
const [error, setError] = useState('')
19+
const [editorKey, setEditorKey] = useState(0)
20+
const dirty = !saved && !showTemplates && (title.trim() !== '' || content.trim() !== '')
21+
22+
useUnsavedWarning(dirty)
1723

1824
useEffect(() => {
1925
api.get('/templates').then((res) => setTemplates(res.data))
@@ -22,13 +28,21 @@ export default function NewPage() {
2228
const selectTemplate = (tmpl) => {
2329
setSelectedTemplate(tmpl)
2430
setContent(tmpl.content_md)
31+
setEditorKey((k) => k + 1)
2532
setShowTemplates(false)
2633
}
2734

2835
const skipTemplates = () => {
36+
setSelectedTemplate(null)
37+
setContent('')
38+
setEditorKey((k) => k + 1)
2939
setShowTemplates(false)
3040
}
3141

42+
const changeTemplate = () => {
43+
setShowTemplates(true)
44+
}
45+
3246
const handleSave = useCallback(async () => {
3347
if (!title.trim() || saving) return
3448
setSaving(true)
@@ -40,6 +54,7 @@ export default function NewPage() {
4054
template_id: selectedTemplate?.id,
4155
})
4256
await fetchTree()
57+
setSaved(true)
4358
navigate(`/page/${page.slug}`)
4459
} catch (err) {
4560
console.error('Create failed:', err)
@@ -121,12 +136,29 @@ export default function NewPage() {
121136
</div>
122137
)}
123138
{selectedTemplate && (
124-
<div className="text-xs text-gray-400 mb-3">
125-
Template: {selectedTemplate.name}
139+
<div className="flex items-center gap-2 text-xs text-gray-400 mb-3">
140+
<span>Template: {selectedTemplate.name}</span>
141+
<button
142+
onClick={changeTemplate}
143+
className="text-blue-500 hover:text-blue-700 underline"
144+
>
145+
Change
146+
</button>
147+
</div>
148+
)}
149+
{!selectedTemplate && (
150+
<div className="flex items-center gap-2 text-xs text-gray-400 mb-3">
151+
<span>Blank page</span>
152+
<button
153+
onClick={changeTemplate}
154+
className="text-blue-500 hover:text-blue-700 underline"
155+
>
156+
Use template
157+
</button>
126158
</div>
127159
)}
128160
<div className="bg-white rounded-xl shadow-sm border border-gray-200 min-h-[500px]">
129-
<Editor defaultValue={content} onChange={setContent} />
161+
<Editor key={editorKey} defaultValue={content} onChange={setContent} />
130162
</div>
131163
</div>
132164
)

frontend/src/pages/PageEdit.jsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { useEffect, useState, useCallback } from 'react'
1+
import { useEffect, useState, useCallback, useRef } from 'react'
22
import { useParams, useNavigate } from 'react-router-dom'
33
import usePages from '../store/usePages'
44
import Editor from '../components/Editor/Editor'
5+
import useUnsavedWarning from '../hooks/useUnsavedWarning'
56

67
export default function PageEdit() {
78
const { slug } = useParams()
@@ -12,22 +13,34 @@ export default function PageEdit() {
1213
const [content, setContent] = useState('')
1314
const [saving, setSaving] = useState(false)
1415
const [error, setError] = useState('')
16+
const [dirty, setDirty] = useState(false)
17+
const originalRef = useRef({ title: '', content: '' })
18+
19+
useUnsavedWarning(dirty)
1520

1621
useEffect(() => {
1722
getPage(slug).then((p) => {
1823
setPage(p)
1924
setTitle(p.title)
2025
setContent(p.content_md)
26+
originalRef.current = { title: p.title, content: p.content_md }
2127
})
2228
}, [slug])
2329

30+
useEffect(() => {
31+
if (!page) return
32+
const { title: origTitle, content: origContent } = originalRef.current
33+
setDirty(title !== origTitle || content !== origContent)
34+
}, [title, content, page])
35+
2436
const handleSave = useCallback(async () => {
2537
if (saving) return
2638
setSaving(true)
2739
setError('')
2840
try {
2941
await updatePage(slug, { title, content_md: content })
3042
await fetchTree()
43+
setDirty(false)
3144
navigate(`/page/${slug}`)
3245
} catch (err) {
3346
console.error('Save failed:', err)

frontend/src/store/useActivity.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import api from '../api/client'
44
const useActivity = create((set) => ({
55
activities: [],
66
stats: null,
7+
statsLoaded: false,
78
total: 0,
89
loading: false,
910

@@ -18,10 +19,12 @@ const useActivity = create((set) => ({
1819
}
1920
},
2021

21-
fetchStats: async () => {
22+
fetchStats: async (force = false) => {
23+
const { statsLoaded } = useActivity.getState()
24+
if (statsLoaded && !force) return useActivity.getState().stats
2225
try {
2326
const res = await api.get('/activity/stats')
24-
set({ stats: res.data })
27+
set({ stats: res.data, statsLoaded: true })
2528
return res.data
2629
} catch {
2730
return null

0 commit comments

Comments
 (0)