@@ -27,7 +27,7 @@ List<Item> generateItems(List<String> categories) {
2727 Item (
2828 headerValue: 'Post Categories' ,
2929 expandedValues: categories,
30- isExpanded: false ,
30+ isExpanded: true ,
3131 )
3232 ];
3333}
@@ -43,9 +43,11 @@ class _PostListPageState extends State<PostListPage> {
4343 late DateFormat df;
4444 bool isShowPostDetail = false ;
4545 late MdDoc currentPost;
46-
4746 List <Item > _data = [];
4847 List <String > categories = [];
48+ List <String > tags = [];
49+ String ? _selectedCategory = 'All' ;
50+ String ? _selectedTag;
4951
5052 @override
5153 void initState () {
@@ -54,11 +56,23 @@ class _PostListPageState extends State<PostListPage> {
5456 df = DateFormat ('yyyy-MM-dd' ); // 한국어 로케일이면 intl 초기화 추가 가능
5557
5658 future.then ((docs) {
57- final cats = docs.map ((d) => d.meta.category).toSet ().toList ();
59+ final cats = docs
60+ .map ((d) =>
61+ (d.meta.category).isNotEmpty ? d.meta.category : 'Uncategorized' )
62+ .toSet ()
63+ .toList ();
64+ final tagList = docs
65+ .map ((d) => (d.meta.tag).isNotEmpty ? d.meta.tag : '' )
66+ .where ((t) => t.isNotEmpty)
67+ .toSet ()
68+ .toList ();
5869 setState (() {
5970 categories = cats;
71+ tags = tagList;
6072 _data = generateItems (categories);
6173 });
74+ }).catchError ((e) {
75+ debugPrint ('load docs error: $e ' );
6276 });
6377 }
6478
@@ -67,14 +81,22 @@ class _PostListPageState extends State<PostListPage> {
6781 }
6882
6983 void _copyCurrentPostUrlToClipboard () {
70- final postUrl =
71- Uri .base .resolve ('/posts/${currentPost .meta .slug }' ).toString ();
72- print ('Copy URL: $postUrl ' );
73- Clipboard .setData (ClipboardData (text: postUrl));
84+ try {
85+ final postPath = '/posts/${currentPost .meta .slug }' ;
86+ web.window.history.pushState (null , '' , postPath);
7487
75- ScaffoldMessenger .of (context).showSnackBar (
76- const SnackBar (content: Text ('URL copied to clipboard!' )),
77- );
88+ final postUrl = web.window.location.href;
89+ Clipboard .setData (ClipboardData (text: postUrl));
90+
91+ ScaffoldMessenger .of (context).showSnackBar (
92+ const SnackBar (content: Text ('URL copied to clipboard!' )),
93+ );
94+ } catch (e, st) {
95+ debugPrint ('URL 복사 실패: $e \n $st ' );
96+ ScaffoldMessenger .of (context).showSnackBar (
97+ const SnackBar (content: Text ('URL 복사에 실패했습니다.' )),
98+ );
99+ }
78100 }
79101
80102 Widget _postDetail (MdDoc currentDoc) {
@@ -109,7 +131,8 @@ class _PostListPageState extends State<PostListPage> {
109131
110132 @override
111133 Widget build (BuildContext context) {
112- Widget _buildPanel () {
134+ // 카테고리 패널
135+ Widget buildPanel () {
113136 return ExpansionPanelList (
114137 expansionCallback: (int index, bool isExpanded) {
115138 setState (() {
@@ -125,13 +148,28 @@ class _PostListPageState extends State<PostListPage> {
125148 body: item.hasExpandedValues
126149 ? Column (
127150 children: item.expandedValues.map ((value) {
151+ final selected = value == _selectedCategory;
128152 return ListTile (
129153 title: Text (value),
154+ selected: selected,
130155 onTap: () {
131- // TODO: Implement category filtering
132- print ('Tapped on $value ' );
156+ setState (() {
157+ isShowPostDetail = false ;
158+ _selectedCategory = value;
159+ _selectedTag = null ;
160+ });
161+ final path = value == 'All'
162+ ? '/'
163+ : '/category/${Uri .encodeComponent (value )}' ;
164+ try {
165+ web.window.history.pushState (null , value, path);
166+ } catch (e) {
167+ debugPrint ('history pushState 실패: $e ' );
168+ }
133169 },
134- trailing: const Icon (Icons .chevron_right),
170+ trailing: selected
171+ ? const Icon (Icons .check)
172+ : const Icon (Icons .chevron_right),
135173 );
136174 }).toList (),
137175 )
@@ -142,62 +180,126 @@ class _PostListPageState extends State<PostListPage> {
142180 );
143181 }
144182
145- Widget _buildPostList (String ? category) {
146- // all 카테고리면 전체, 아니면 해당 카테고리만 필터링
147- // til 카테고리는 따로 구현?
183+ // 태그 칩
184+ Widget buildTagChips () {
185+ if (tags.isEmpty) return const SizedBox .shrink ();
186+ return Padding (
187+ padding: const EdgeInsets .symmetric (horizontal: 8.0 , vertical: 6.0 ),
188+ child: Wrap (
189+ spacing: 8 ,
190+ children: tags.map ((t) {
191+ final sel = t == _selectedTag;
192+ return ChoiceChip (
193+ label: Text (t),
194+ selected: sel,
195+ onSelected: (v) {
196+ setState (() {
197+ isShowPostDetail = false ;
198+ _selectedTag = v ? t : null ;
199+ if (v) _selectedCategory = null ;
200+ });
148201
149- return ! isShowPostDetail
150- ? FutureBuilder <List <MdDoc >>(
151- future: future,
152- builder: (context, snap) {
153- if (snap.connectionState != ConnectionState .done) {
154- return const Center (child: CircularProgressIndicator ());
202+ final path = v
203+ ? '/tag/${Uri .encodeComponent (t )}'
204+ : (_selectedCategory != null && _selectedCategory != 'All'
205+ ? '/category/${Uri .encodeComponent (_selectedCategory !)}'
206+ : '/' );
207+ try {
208+ web.window.history.pushState (null , _selectedTag! , path);
209+ } catch (e) {
210+ debugPrint ('history pushState 실패: $e ' );
155211 }
156- if (snap.hasError) {
157- return Center (child: Text ('로드 실패: ${snap .error }' ));
158- }
159- final docs = snap.data ?? [];
160- if (docs.isEmpty) {
161- return const Center (child: Text ('문서가 없습니다.' ));
162- }
163- return ListView .separated (
164- itemCount: docs.length,
165- separatorBuilder: (_, __) => const Divider (height: 1 ),
166- itemBuilder: (context, i) {
167- final d = docs[i];
168- final dateStr = d.meta.date != null
169- ? df.format (d.meta.date! )
170- : '날짜 정보 없음' ;
171- return ListTile (
172- title: Text (d.meta.title,
173- maxLines: 1 , overflow: TextOverflow .ellipsis),
174- subtitle: Text ('${d .meta .category } · $dateStr ' ),
175- trailing: const Icon (Icons .chevron_right),
176- onTap: () {
177- setState (() {
178- isShowPostDetail = true ;
179- currentPost = d;
180- });
181- },
182- );
183- },
184- );
185212 },
186- )
187- : _postDetail (currentPost);
213+ );
214+ }).toList (),
215+ ),
216+ );
217+ }
218+
219+ // 포스트 리스트
220+ Widget buildPostList (String ? category) {
221+ // category: All, Language, Platform, Dev, Life, Book_Review
222+ // tag: TIL, 등등...
223+ if (category != null && category == 'All' ) {
224+ return ! isShowPostDetail
225+ ? FutureBuilder <List <MdDoc >>(
226+ future: future,
227+ builder: (context, snap) {
228+ if (snap.connectionState != ConnectionState .done) {
229+ return const Center (child: CircularProgressIndicator ());
230+ }
231+ if (snap.hasError) {
232+ return Center (child: Text ('로드 실패: ${snap .error }' ));
233+ }
234+ var docs = snap.data ?? [];
235+
236+ if (_selectedTag != null && _selectedTag! .isNotEmpty) {
237+ docs =
238+ docs.where ((d) => d.meta.tag == _selectedTag).toList ();
239+ } else if (_selectedCategory != null &&
240+ _selectedCategory != 'All' ) {
241+ docs = docs
242+ .where ((d) => d.meta.category == _selectedCategory)
243+ .toList ();
244+ }
245+ if (docs.isEmpty) {
246+ return const Center (child: Text ('문서가 없습니다.' ));
247+ }
248+ return ListView .separated (
249+ itemCount: docs.length,
250+ separatorBuilder: (_, __) => const Divider (height: 1 ),
251+ itemBuilder: (context, i) {
252+ final d = docs[i];
253+ final dateStr = d.meta.date != null
254+ ? df.format (d.meta.date! )
255+ : '날짜 정보 없음' ;
256+ return ListTile (
257+ title: Text (d.meta.title,
258+ maxLines: 1 , overflow: TextOverflow .ellipsis),
259+ subtitle: Text ('${d .meta .category } · $dateStr ' ),
260+ trailing: const Icon (Icons .chevron_right),
261+ onTap: () {
262+ final postPath =
263+ '/posts/${Uri .encodeComponent (d .meta .slug )}' ;
264+ // 주소 표시줄을 포스트 경로로 즉시 갱신
265+ web.window.history
266+ .pushState (null , d.meta.title, postPath);
267+ setState (() {
268+ isShowPostDetail = true ;
269+ currentPost = d;
270+ });
271+ },
272+ );
273+ },
274+ );
275+ },
276+ )
277+ : _postDetail (currentPost);
278+ } else if (category != null && category == 'TIL' ) {
279+ return const Center (child: Text ('TIL 카테고리 페이지는 미구현 상태입니다.' ));
280+ } else {
281+ return const Center (child: Text ('카테고리를 선택해주세요.' ));
282+ }
188283 }
189284
190285 return Scaffold (
191286 appBar: ! isShowPostDetail
192287 ? AppBar (
288+ elevation: 0 ,
193289 title: const Text ('Blog Posts' ),
290+ shape: const Border (
291+ bottom: BorderSide (color: Colors .grey, width: 0.5 ),
292+ ),
194293 )
195294 : AppBar (
196295 elevation: 0 ,
197296 scrolledUnderElevation: 0 ,
198297 shadowColor: Colors .transparent,
199298 surfaceTintColor: Colors .transparent,
200299 forceMaterialTransparency: true ,
300+ shape: const Border (
301+ bottom: BorderSide (color: Colors .grey, width: 0.5 ),
302+ ),
201303 title: Text (currentPost.meta.title),
202304 leading: BackButton (
203305 onPressed: () => setState (() {
@@ -223,18 +325,18 @@ class _PostListPageState extends State<PostListPage> {
223325 width: width * 0.3 ,
224326 child: Column (
225327 children: [
226- _buildPanel (),
227- ListTile (
228- title: const Text ('TIL (Today I Learned)' ),
229- onTap: () {
230- // TODO: Implement TIL navigation
231- },
328+ buildPanel (),
329+ const ListTile (
330+ title: Center (child: Text ('Tags' )),
232331 ),
332+ Padding (
333+ padding: const EdgeInsets .all (8.0 ),
334+ child: buildTagChips ()),
233335 ],
234336 ),
235337 ),
236338 VerticalDivider (color: Colors .grey[400 ], width: 3 ),
237- Expanded (child: _buildPostList ('All' )),
339+ Expanded (child: buildPostList ('All' )),
238340 ],
239341 );
240342 } else {
@@ -244,24 +346,19 @@ class _PostListPageState extends State<PostListPage> {
244346 visible: ! isShowPostDetail,
245347 child: Column (
246348 children: [
247- _buildPanel (),
248- ListTile (
249- title: const Text ('TIL (Today I Learned)' ),
250- onTap: () {
251- // TODO: Implement TIL navigation
252- },
349+ buildPanel (),
350+ const ListTile (
351+ title: Center (child: Text ('Tags' )),
253352 ),
353+ Padding (
354+ padding: const EdgeInsets .all (8.0 ),
355+ child: buildTagChips ()),
254356 ],
255357 ),
256358 ),
257359 Divider (color: Colors .grey[400 ], height: 3 ),
258360 SizedBox (height: 16 ),
259- Text ('Recent Posts' ,
260- style: TextStyle (
261- fontWeight: FontWeight .bold,
262- fontSize: 20 ,
263- color: Colors .grey[700 ])),
264- Expanded (child: _buildPostList ('All' )),
361+ Expanded (child: buildPostList ('All' )),
265362 ],
266363 );
267364 }
0 commit comments