Astro 專題 - DevPulse:極簡開發者技術雷達 (Tech Radar)-4

更新 發佈閱讀 12 分鐘

(進階) 搭配 Nano Stores 完成「過濾器按鈕」

  1. index.astro 的 ALL 按鈕,改為 FilterButton.tsx
  2. 套用先前建立好的 techStore.js

1. 建立 FilterButton 元件

新增 src\components\FilterButton.tsx

import { useState } from 'react'
type Props = {
categories: string[]
}

const FilterButton = function ({ categories }: Props) {
const [isOpen, setIsOpen] = useState(false)
const [active, setActive] = useState('All')
const allCategories = ['All', ...categories]

const handleSelect = (filter: string) => {
setActive(filter)
setIsOpen(false)
}

return (
<div className="flex items-center gap-2 p-4 bg-white">
<div
className={`flex items-center rounded-2xl overflow-hidden transition-all duration-500 ease-in-out
${isOpen ? 'max-w-[600px] shadow-md' : 'max-w-[120px]'}
`}
>
<button
onClick={() => setIsOpen(!isOpen)}
className={`px-4 py-2 bg-indigo-600 text-white rounded-2xl
${
isOpen
? 'text-blue-600 bg-blue-50'
: 'text-gray-700 hover:bg-blue-100 hover:text-blue-600'
}
`}
>
<span className="flex items-center gap-1">{active}</span>
</button>

<div
className={`flex items-center pr-2 transition-opacity duration-300
${isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}
`}
>
{allCategories.map((c) => (
<button
key={c}
onClick={() => handleSelect(c)}
className="px-3 py-2 text-sm text-gray-500 whitespace-nowrap hover:bg-blue-50 rounded-full transition-all"
>
{c}
</button>
))}
</div>
</div>
</div>
)
}

export default FilterButton

2. 使用 FilterButton 設定 techStore 的 categoryFilter

// FilterButton.tsx
import { useState } from 'react'
import { setCategory } from '../store/techStore'

type Props = {
categories: string[]
}

const FilterButton = function ({ categories }: Props) {
const [isOpen, setIsOpen] = useState(false)
const [active, setActive] = useState('All')
const allCategories = ['All', ...categories]

const handleSelect = (filter: string) => {
setActive(filter)
setIsOpen(false)
setCategory(filter) // 使用 setCategory
}

return (
....
)
}

export default FilterButton

3. TechList 取得及時 techStore 的 categoryFilter

// SearchBar.tsx
import type { CategoryMap, Tech, TechStatus } from '../type/tech'
import { useState } from 'react'
import { useStore } from '@nanostores/react'
import { searchQuery } from '../store/searchStore'
import { categoryFilter } from '../store/techStore'

type TechListProps = {
category: CategoryMap
}

const CategorySection = ({
status,
items,
}: {
status: string
items: Tech[]
}) => {
const [isOpen, setIsOpen] = useState(true)
return <section>...</section>
}

const TechList = function ({ category }: TechListProps) {
const $searchQuery = useStore(searchQuery)
const $categoryFilter = useStore(categoryFilter)

const allCategory = {
mastered: category.mastered.filter((item) =>
item.body?.includes($searchQuery)
),
learning: category.learning.filter((item) =>
item.body?.includes($searchQuery)
),
wishlist: category.wishlist.filter((item) =>
item.body?.includes($searchQuery)
),
}

const filterCategory: CategoryMap =
$categoryFilter == 'All'
? allCategory
: ({
[$categoryFilter]: allCategory[$categoryFilter as TechStatus],
} as CategoryMap)

return (
<div className="grid gap-8">
{Object.entries(filterCategory).map(([status, items]) => (
<CategorySection status={status} items={items} key={status} />
))}
</div>
)
}

export default TechList

4. index 渲染 FilterButton

src\pages\index.astro

---
import { getCollection } from 'astro:content'
import type { CategoryMap } from '../type/tech'
import Layout from '../layouts/Layout.astro'
import SearchBar from '../components/SearchBar'
import TechList from '../components/TechList'
import FilterButton from '../components/FilterButton'
const allTechs = await getCollection('techs')

const category: CategoryMap = {
mastered: allTechs.filter((t) => t.data.status === 'mastered'),
learning: allTechs.filter((t) => t.data.status === 'learning'),
wishlist: allTechs.filter((t) => t.data.status === 'wishlist'),
}

const categoryKeys = Object.keys(category)
---

<Layout title="DevPulse">
<main class="max-w-4xl mx-auto p-8">
<h1 class="text-4xl font-bold tracking-tight text-gray-900 mb-8">
My Tech Radar
</h1>

<FilterButton categories={categoryKeys} client:load />
<SearchBar client:load />
<TechList category={category} client:load />
</main>
</Layout>
留言
avatar-img
李昀瑾的沙龍
0會員
37內容數
李昀瑾的沙龍的其他內容
2026/03/12
(進階) 搭配 Nano Stores 完成「即時搜尋」 靜態 HTML Client Component Vanilla JS / Framework JS
2026/03/12
(進階) 搭配 Nano Stores 完成「即時搜尋」 靜態 HTML Client Component Vanilla JS / Framework JS
2026/03/05
(進階) 在 Astro 5 中讀取並顯示 我們可以直接在頁面中把這些資料抓出來,並根據 status 分類。
2026/03/05
(進階) 在 Astro 5 中讀取並顯示 我們可以直接在頁面中把這些資料抓出來,並根據 status 分類。
2026/02/26
這是一個讓開發者管理自己「想學、正在學、已精通」技術清單的工具,強調極速的頁面切換與流暢的 UI。 環境架構設計 astro v5 tailwind v4 Nano Stores vercel 佈署
2026/02/26
這是一個讓開發者管理自己「想學、正在學、已精通」技術清單的工具,強調極速的頁面切換與流暢的 UI。 環境架構設計 astro v5 tailwind v4 Nano Stores vercel 佈署
看更多
你可能也想看
Thumbnail
網站開發專案成功的關鍵在於與客戶的有效溝通。本文分享一個成功案例,說明如何透過明確掌握專案需求、主動提供技術方案、定期回報進度、完善技術協助及建立良好客戶關係,順利完成一個中文影片學習分享網站的建置,並獲得客戶高度滿意與後續合作機會。
Thumbnail
網站開發專案成功的關鍵在於與客戶的有效溝通。本文分享一個成功案例,說明如何透過明確掌握專案需求、主動提供技術方案、定期回報進度、完善技術協助及建立良好客戶關係,順利完成一個中文影片學習分享網站的建置,並獲得客戶高度滿意與後續合作機會。
Thumbnail
《轉轉生》(Re:INCARNATION)為奈及利亞編舞家庫德斯.奧尼奎庫與 Q 舞團創作的當代舞蹈作品,結合拉各斯街頭節奏、Afrobeat/Afrobeats、以及約魯巴宇宙觀的非線性時間,建構出關於輪迴的「誕生—死亡—重生」儀式結構。本文將從約魯巴哲學概念出發,解析其去殖民的身體政治。
Thumbnail
《轉轉生》(Re:INCARNATION)為奈及利亞編舞家庫德斯.奧尼奎庫與 Q 舞團創作的當代舞蹈作品,結合拉各斯街頭節奏、Afrobeat/Afrobeats、以及約魯巴宇宙觀的非線性時間,建構出關於輪迴的「誕生—死亡—重生」儀式結構。本文將從約魯巴哲學概念出發,解析其去殖民的身體政治。
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
Thumbnail
這是一場從「網路連結 → 線下見面」的活動,我一開始其實有些猶豫,畢竟地點對我來說不近,加上平常在社群裡其實不太主動互動。 但因為主辦人西打誠意滿滿地邀請,甚至還提出補貼車資,最後我決定自費參加。現在回頭看,真的很值得! 場地很有感,氛圍超溫暖 一踏進場地就被暖黃的燈光包圍,小閣樓超舒適,還
Thumbnail
這是一場從「網路連結 → 線下見面」的活動,我一開始其實有些猶豫,畢竟地點對我來說不近,加上平常在社群裡其實不太主動互動。 但因為主辦人西打誠意滿滿地邀請,甚至還提出補貼車資,最後我決定自費參加。現在回頭看,真的很值得! 場地很有感,氛圍超溫暖 一踏進場地就被暖黃的燈光包圍,小閣樓超舒適,還
Thumbnail
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
這份是我在使用Lovable做網站時,邊做邊紀錄的完整攻略,分享給需要的人。 給開發者/接案者的前言: Lovable 是一個強大的 AI 全端開發工具,要用它來賺錢,您必須清楚它的邊界在哪裡。這份指南將協助您完全掌控這個工具,從而自信地向客戶報價。 第一章:深度認識 Lovable 1.1
Thumbnail
這份是我在使用Lovable做網站時,邊做邊紀錄的完整攻略,分享給需要的人。 給開發者/接案者的前言: Lovable 是一個強大的 AI 全端開發工具,要用它來賺錢,您必須清楚它的邊界在哪裡。這份指南將協助您完全掌控這個工具,從而自信地向客戶報價。 第一章:深度認識 Lovable 1.1
Thumbnail
從實際應用中學習 Python 程式設計,提升技能並建立作品集。文章提供八個循序漸進的 Python 專案範例,涵蓋檔案操作、網路爬蟲、Web 應用、自動化腳本、數據分析、遊戲開發、API 互動及應用程式部署,並附上實戰建議及學習資源。
Thumbnail
從實際應用中學習 Python 程式設計,提升技能並建立作品集。文章提供八個循序漸進的 Python 專案範例,涵蓋檔案操作、網路爬蟲、Web 應用、自動化腳本、數據分析、遊戲開發、API 互動及應用程式部署,並附上實戰建議及學習資源。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News