Wiki source code of KBCategoryLanding

Last modified by Isaac Mejia on 2026/06/08 20:12

Show last authors
1 {{velocity}}
2 ## ------------------------------------------------------------
3 ## Category landing page for eFit subwiki
4 ## URL pattern: /Main/<Category>/WebHome
5 ## e.g. /Main/Payment_Management/WebHome
6 ## ------------------------------------------------------------
7
8 ## Extract category from URL - grab segment after "Main"
9 #set ($uri = $request.getRequestURI())
10 #set ($baseSpace = '')
11 #set ($catDisplayName = '')
12
13 #set ($viewMarker = '/view/')
14 #if ($uri.contains($viewMarker))
15 #set ($afterView = $uri.substring($uri.indexOf($viewMarker) + $viewMarker.length()))
16 #set ($uriParts = $afterView.split('/'))
17 ## uriParts: [0]=Main, [1]=Payment_Management, [2]=WebHome
18 #if ($uriParts.size() > 1)
19 #set ($baseSpace = $uriParts.get(1))
20 #end
21 #end
22
23 ## Fallback
24 #if ("$!baseSpace" == "" || $baseSpace == 'WebHome' || $baseSpace == 'Main')
25 #set ($baseSpace = $doc.space.replace('Main.', ''))
26 #end
27
28 ## Full XWiki space reference e.g. "Main.Payment_Management"
29 #set ($fullSpace = "Main." + $baseSpace)
30
31 ## Display name - replace underscores and hyphens with spaces
32 #set ($catDisplayName = $baseSpace.replace('_', ' ').replace('-', '-'))
33
34 ## -----------------------------
35 ## Read query params safely
36 ## -----------------------------
37 #set ($rawFilter = $request.getParameter('filter'))
38 #set ($filter = "$!rawFilter")
39 #set ($filter = $stringtool.trim($filter))
40 #set ($hasFilter = $filter != "")
41
42 #set ($rawP = $request.getParameter('p'))
43 #set ($pStr = "$!rawP")
44 #set ($pStr = $stringtool.trim($pStr))
45
46 #set ($pageSize = 20)
47 #set ($pageNum = 1)
48
49 #if ($pStr != "" && $pStr.matches("^[0-9]+$"))
50 #set ($pageNum = $numbertool.toNumber($pStr).intValue())
51 #end
52 #if ($pageNum < 1) #set ($pageNum = 1) #end
53
54 #set ($offset = ($pageNum - 1) * $pageSize)
55
56 #set ($baseUrl = $doc.getURL('view'))
57 #set ($encodedFilter = $escapetool.url($filter))
58 #set ($filterPrefix = "")
59 #if ($hasFilter)
60 #set ($filterPrefix = "filter=$encodedFilter&")
61 #end
62
63 ## -----------------------------
64 ## XWQL WHERE
65 ## -----------------------------
66 #set ($where =
67 "where doc.space = :fullSpace " +
68 "and doc.name <> 'WebHome' " +
69 "and doc.hidden <> true "
70 )
71 #if ($hasFilter)
72 #set ($where = $where +
73 "and (lower(doc.title) like :q or lower(doc.content) like :q) "
74 )
75 #end
76
77 ## -----------------------------
78 ## Count total
79 ## -----------------------------
80 #set ($countXwql =
81 "select count(doc.fullName) " +
82 "from XWikiDocument doc " +
83 $where
84 )
85
86 #set ($countQuery = $services.query.xwql($countXwql))
87 #set ($discard = $countQuery.bindValue('fullSpace', $fullSpace))
88 #if ($hasFilter)
89 #set ($discard = $countQuery.bindValue('q', "%" + $filter.toLowerCase() + "%"))
90 #end
91
92 #set ($countRows = $countQuery.execute())
93 #set ($totalCount = 0)
94 #if ($countRows && $countRows.size() > 0)
95 #set ($totalCount = $countRows.get(0))
96 #end
97
98 ## Pagination math
99 #set ($totalPages = 1)
100 #if ($totalCount > 0)
101 #set ($totalPages = (($totalCount + $pageSize - 1) / $pageSize))
102 #end
103 #if ($pageNum > $totalPages)
104 #set ($pageNum = $totalPages)
105 #set ($offset = ($pageNum - 1) * $pageSize)
106 #end
107
108 #set ($prev = $pageNum - 1)
109 #set ($next = $pageNum + 1)
110
111 ## -----------------------------
112 ## Fetch paged results
113 ## -----------------------------
114 #set ($listXwql =
115 "select doc.fullName " +
116 "from XWikiDocument doc " +
117 $where +
118 "order by lower(doc.title)"
119 )
120
121 #set ($listQuery = $services.query.xwql($listXwql))
122 #set ($discard = $listQuery.bindValue('fullSpace', $fullSpace))
123 #if ($hasFilter)
124 #set ($discard = $listQuery.bindValue('q', "%" + $filter.toLowerCase() + "%"))
125 #end
126
127 #set ($discard = $listQuery.setLimit($pageSize))
128 #set ($discard = $listQuery.setOffset($offset))
129 #set ($rows = $listQuery.execute())
130
131 #set ($articles = [])
132 #foreach ($name in $rows)
133 #set ($articleDoc = $xwiki.getDocument($name))
134 #set ($discard = $articles.add($articleDoc))
135 #end
136
137 #set ($articleCountLabel = "${totalCount} article")
138 #if ($totalCount != 1)
139 #set ($articleCountLabel = "${totalCount} articles")
140 #end
141
142 ## Pager window
143 #set ($start = $pageNum - 2)
144 #set ($end = $pageNum + 2)
145 #if ($start < 1) #set ($start = 1) #end
146 #if ($end > $totalPages) #set ($end = $totalPages) #end
147
148 {{html clean="false"}}
149 <div class="kb-category-page">
150
151 <div class="kb-hero">
152 <h1 class="kb-hero-title">$escapetool.xml($catDisplayName)</h1>
153 <p class="kb-hero-subtitle">Browse all articles in $escapetool.xml($catDisplayName).</p>
154
155 <form class="kb-category-search" action="$baseUrl" method="get">
156 <input
157 type="text"
158 name="filter"
159 value="$escapetool.html($filter)"
160 placeholder="Search within this category…"
161 class="kb-category-search-input"
162 />
163 <button type="submit" class="kb-category-search-button">Search</button>
164 #if ($hasFilter)
165 <a class="kb-category-clear" href="$baseUrl">Clear</a>
166 #end
167 </form>
168 </div>
169
170 <div class="kb-category-columns">
171
172 <!-- Popular -->
173 <aside class="kb-category-column kb-sidebar">
174 <h2 class="kb-section-title">Popular articles</h2>
175
176 <ul class="kb-category-list">
177 #if (!$hasFilter)
178 #set ($popXwql =
179 "select doc.fullName from XWikiDocument doc " +
180 "where doc.space = :fullSpace and doc.name <> 'WebHome' and doc.hidden <> true " +
181 "order by lower(doc.title)"
182 )
183 #set ($popQuery = $services.query.xwql($popXwql))
184 #set ($discard = $popQuery.bindValue('fullSpace', $fullSpace))
185 #set ($discard = $popQuery.setLimit(4))
186 #set ($popRows = $popQuery.execute())
187
188 #if ($popRows && $popRows.size() > 0)
189 #foreach ($n in $popRows)
190 #set ($pdoc = $xwiki.getDocument($n))
191 <li><a href="$pdoc.getURL('view')" class="kb-footer-link">$escapetool.xml($pdoc.displayTitle)</a></li>
192 #end
193 #else
194 <li><span>No articles yet.</span></li>
195 #end
196 #else
197 <li><span class="kb-muted">Popular list hidden while searching.</span></li>
198 #end
199 </ul>
200 </aside>
201
202 <!-- All articles -->
203 <main class="kb-category-column kb-main">
204 <div class="kb-category-header-row">
205 <h2 class="kb-section-title">All articles in $escapetool.xml($catDisplayName)</h2>
206 <span class="kb-article-count-badge">$escapetool.xml($articleCountLabel)</span>
207 </div>
208
209 <!-- Pager (top) -->
210 #if ($totalPages > 1)
211 <div class="kb-pager">
212 #if ($pageNum > 1)
213 <a class="kb-pager-btn" href="$baseUrl?$filterPrefix"
214 onclick="this.href='$baseUrl?$filterPrefix' + 'p=$prev';">← Prev</a>
215 #else
216 <span class="kb-pager-btn is-disabled">← Prev</span>
217 #end
218
219 <div class="kb-pager-pages">
220 #set ($p1 = $start)
221 #set ($p2 = $start + 1)
222 #set ($p3 = $start + 2)
223 #set ($p4 = $start + 3)
224 #set ($p5 = $start + 4)
225
226 #macro(renderPage $pn)
227 #if ($pn >= $start && $pn <= $end)
228 #if ($pn == $pageNum)
229 <span class="kb-pager-page is-current">$pn</span>
230 #else
231 <a class="kb-pager-page"
232 href="$baseUrl?$filterPrefix"
233 onclick="this.href='$baseUrl?$filterPrefix' + 'p=$pn';">$pn</a>
234 #end
235 #end
236 #end
237
238 #renderPage($p1)
239 #renderPage($p2)
240 #renderPage($p3)
241 #renderPage($p4)
242 #renderPage($p5)
243 </div>
244
245 #if ($pageNum < $totalPages)
246 <a class="kb-pager-btn" href="$baseUrl?$filterPrefix"
247 onclick="this.href='$baseUrl?$filterPrefix' + 'p=$next';">Next →</a>
248 #else
249 <span class="kb-pager-btn is-disabled">Next →</span>
250 #end
251 </div>
252 #end
253
254 <ul class="kb-category-card-grid" id="kbCategoryGrid">
255 #foreach ($article in $articles)
256 #set ($lastUpdatedLabel = "")
257 #if ($article.date)
258 #set ($lastUpdatedLabel = $xwiki.formatDate($article.date, "MMM d, yyyy"))
259 #end
260
261 <li class="kb-article-card" data-title="$escapetool.xml($article.displayTitle.toLowerCase())">
262 <a href="$article.getURL('view')" class="kb-article-card-title">
263 $escapetool.xml($article.displayTitle)
264 </a>
265
266 #if ($lastUpdatedLabel != "")
267 <div class="kb-article-card-meta">
268 Updated $escapetool.xml($lastUpdatedLabel)
269 </div>
270 #end
271 </li>
272 #end
273
274 #if ($articles.isEmpty())
275 <li class="kb-article-card kb-article-card-empty">
276 <div class="kb-article-card-title">
277 #if ($hasFilter)
278 No results for "$escapetool.xml($filter)".
279 #else
280 No articles yet.
281 #end
282 </div>
283 </li>
284 #end
285 </ul>
286
287 <!-- Pager (bottom) -->
288 #if ($totalPages > 1)
289 <div class="kb-pager kb-pager-bottom">
290 <span class="kb-pager-status">
291 Page <strong>$pageNum</strong> of <strong>$totalPages</strong>
292 </span>
293 </div>
294 #end
295 </main>
296
297 </div>
298 </div>
299
300 <style>
301 .kb-category-page { max-width: 1200px; margin: 0 auto; padding: 0 1rem 3rem; }
302 .kb-hero { text-align: center; padding: 2.2rem 0 1rem; }
303 .kb-hero-title { font-size: 2.2rem; font-weight: 800; margin: 0; }
304 .kb-hero-subtitle { color: #6b7280; margin: 0.6rem 0 1.2rem; }
305 .kb-category-search { display: flex; gap: 0.6rem; justify-content: center; align-items: center; flex-wrap: wrap; }
306 .kb-category-search-input { width: min(680px, 92vw); padding: 0.9rem 1.1rem; border: 1px solid #d1d5db; border-radius: 999px; font-size: 1.05rem; }
307 .kb-category-search-button { padding: 0.9rem 1.1rem; border-radius: 999px; border: none; background: #2563eb; color: #fff; font-weight: 700; }
308 .kb-category-clear { color: #6b7280; text-decoration: none; font-weight: 700; }
309 .kb-category-clear:hover { text-decoration: underline; }
310 .kb-muted { color: #9ca3af; }
311 .kb-category-columns { display: grid; grid-template-columns: 1fr; gap: 2rem; align-items: start; }
312 @media (min-width: 1100px) { .kb-category-columns { grid-template-columns: 340px minmax(0, 1fr); gap: 2.5rem; } }
313 @media (max-width: 1099px) { .kb-sidebar { order: 2; } .kb-main { order: 1; } }
314 .kb-sidebar { border: 1px solid #e5e7eb; border-radius: 1rem; padding: 1rem 1.1rem; background: #fff; box-shadow: 0 4px 12px rgba(15,23,42,0.06); }
315 .kb-category-list { list-style: none; padding: 0; margin: 0.8rem 0 0; }
316 .kb-category-list li { margin: 0.5rem 0; }
317 .kb-category-header-row { display: flex; gap: 0.75rem; justify-content: space-between; align-items: baseline; flex-wrap: wrap; }
318 .kb-article-count-badge { font-size: 0.85rem; padding: 0.15rem 0.55rem; border-radius: 999px; border: 1px solid #d4d4d8; background: #f4f4f5; color: #4b5563; }
319 .kb-pager { display: flex; gap: 0.75rem; align-items: center; justify-content: space-between; margin: 1rem 0 1.25rem; flex-wrap: wrap; }
320 .kb-pager-pages { display: flex; gap: 0.35rem; align-items: center; flex-wrap: wrap; justify-content: center; }
321 .kb-pager-btn { padding: 0.55rem 0.9rem; border-radius: 999px; border: 1px solid #e5e7eb; background: #fff; text-decoration: none; font-weight: 800; color: #111827; }
322 .kb-pager-btn.is-disabled { opacity: 0.45; cursor: not-allowed; }
323 .kb-pager-page { padding: 0.4rem 0.7rem; border-radius: 999px; border: 1px solid #e5e7eb; text-decoration: none; color: #111827; font-weight: 800; }
324 .kb-pager-page.is-current { background: #111827; color: #fff; border-color: #111827; }
325 .kb-pager-status { color: #6b7280; font-weight: 800; }
326 .kb-pager-bottom { justify-content: center; margin-top: 1.25rem; }
327 .kb-category-card-grid { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.9rem; grid-template-columns: 1fr; }
328 @media (min-width: 860px) { .kb-category-card-grid { grid-template-columns: 1fr 1fr; } }
329 .kb-article-card { border: 1px solid #e5e7eb; border-radius: 0.95rem; padding: 0.9rem 1rem; background: #fff; box-shadow: 0 4px 12px rgba(15,23,42,0.06); }
330 .kb-article-card-title { display: inline-block; font-size: 1.05rem; font-weight: 900; color: #2563eb; text-decoration: none; line-height: 1.25; }
331 .kb-article-card-title:hover { text-decoration: underline; }
332 .kb-article-card-meta { margin-top: 0.35rem; color: #6b7280; font-size: 0.86rem; }
333 .kb-article-card-empty { text-align: center; padding: 1.4rem; }
334 </style>
335
336 <script>
337 (function () {
338 const input = document.querySelector('.kb-category-search-input');
339 const grid = document.getElementById('kbCategoryGrid');
340 if (!input || !grid) return;
341 input.addEventListener('input', function () {
342 const q = (input.value || '').trim().toLowerCase();
343 const cards = grid.querySelectorAll('.kb-article-card[data-title]');
344 cards.forEach(card => {
345 const t = (card.getAttribute('data-title') || '');
346 card.style.display = (!q || t.includes(q)) ? '' : 'none';
347 });
348 });
349 })();
350 </script>
351 {{/html}}
352 {{/velocity}}