# Next.js建立作品集網站 * 網址: https://archielai.github.io/portfolio/ * github repo: https://github.com/ArchieLai/portfolio ## 1.啟動專案 * User story:「身為對建築設計有興趣的使用者,需要瀏覽設計作品圖片,因為想獲得靈感啟發、或進一步瞭解作者。」 * UI 設計: ![截圖 2024-01-27 上午9.54.31](https://hackmd.io/_uploads/Hy-Ozyf5a.png) ## 2.建立Next.js app ``` npx create-next-app@latest ``` ### App router與Page router Next.js在v13以後增加了App router,和過去的Page router一樣以檔案系統(file-system based)作為路由。例如Page router建立新的`about`頁面時,直接在`pages/`資料夾下建立`about.js`;使用App router則是在`app/`下建立`about/`資料夾,如下圖目錄結構。這個專案採用App router。 ``` app/ ∟about/ ⊢layout.js ⊢page.js ⊢loading.js ∟error.js ``` ### 動態路由 要用id為每個作品建立url:`/posts/[id]`,只需要建立`app/posts/[id]`資料夾以及底下的`page.js`,可以避免重複為每一個id建立頁面。 * generateStaticParams() * 回傳包含`params`(id)的陣列,value必須是字串型態。 * 類似Page router中的`getStaticPaths` * dynamicParams: 設為false,在url輸入不存在的id時會回傳404。 ```javascript // app/posts/[id]/page.js export function generateStaticParams() { return list.map((post) => ({ id: String(post.id) })); } export const dynamicParams = false; export default function Page({ params }) { return ( <p>{params.id}</p> ); } ``` ### 靜態檔案 將同一個作品id的圖片放在`public/images/posts/[id]`下,需要用一個utililty software取得檔案名稱。 ```javascript export function getImagePath(id) { //取得current working directory下存放圖片位置 const postsDirectory = path.join(process.cwd(), `public/images/posts/${id}`); //取得dir內檔案名稱 const fileNames = fs.readdirSync(postsDirectory); return fileNames; } ``` 將圖片加入作品頁面。 ```javascript export default function Page({ params }){ const imgNames = getImagePath(params.id); return ( {imgNames.map((img) => { const imgPath = `/images/posts/${id}/${img}`; return ( <Image key={id} src={imgPath} width={1070} height={580} />); })} ); } ``` ### CSS 使用css module,每個路徑的資料夾下建立`page.module.css`。會自動產生唯一的 class 名稱,避免不同route之間 class 名稱衝突的問題。`globals.css`可以套用到每一個路由。 ## 3.Material UI (MUI) MUI是一個React元件函式庫,預設的CSS樣式引擎是使用emotion,需要安裝以下幾個套件。 ``` npm install @mui/material-nextjs @emotion/cache @emotion/react @emotion/styled ``` 修改`app/layout.js`,匯入`AppRouterCacheProvider`並加入`RootLayout`元件。因為app開發不是使用emotion,需要調整選項。 ```javascript import { AppRouterCacheProvider } from '@mui/material-nextjs/v13-appRouter'; export default function RootLayout(props) { return ( <html lang="en"> <body> - {props.children} + <AppRouterCacheProvider options={{ enableCssLayer: true }}>{props.children}</AppRouterCacheProvider> </body> </html> ); } ``` ### BottomNavigation 在網頁上方nav bar加上一個導航選單。 官網範例:https://mui.com/material-ui/react-bottom-navigation/ * value是用來辨識第幾個BottomNavigationAction,預設是位置的index * 嘗試把BottomNavigationAction包在Link元件會出現error,根據[issues的回答](https://github.com/mui/material-ui/issues/12206),BottomNavigationAction必須是BottomNavigation的直接子元件,所以改用useRouter設定連結。 ```javascript= export default function BottomNav() { const [value, setValue] = React.useState(3); const router = useRouter(); const handleClick = (e, route) => { e.preventDefault(); router.push(route); }; return ( <div> <BottomNavigation value={value} onChange={(event, newValue) => { setValue(newValue); }} sx={{background: "rgba(0,0,0,0)"}} > <BottomNavigationAction icon={<HomeIcon />} onClick={(e) => handleClick(e, "/")} /> // add more BottomNavigation </BottomNavigation> </div> ); } ``` ### Card元件的Hover效果 當滑鼠停在圖片上(Card)時,產生一個半透明Box、並且顯示該作品的標題。 ```javascript= export default function Post(props) { const [isHovered, setHovered] = useState(false); const handleMouseEnter = () => { setHovered(true); }; const handleMouseLeave = () => { setHovered(false); }; return ( <Card className={styles.card} sx={{ width: w, height: h}} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} > <Link href={`/posts/${props.index}`}> <Box sx={{position: "relative"}}> <CardMedia component={() => ( <Image src={props.cover} width={w} height={h} /> )} /> {isHovered && ( <Box className={styles.box}>{props.title}</Box> )} </Box> </Link> </Card> ); } ``` ### Carousel (SwiperJS) MUI沒有類似carousel的元件,官網上[類似的元件](https://mui.com/material-ui/react-button/#complex-button)比較複雜,使用另一個套件SwiperJS。 ``` npm i swiper ``` ```javascript= export default function Carousel(props){ const carousel = props.pathList; return ( <> <Swiper spaceBetween={30} centeredSlides={true} autoplay={{ delay: 3000, disableOnInteraction: false, }} pagination={{ clickable: true, }} navigation={true} modules={[Autoplay, Pagination, Navigation]} className={styles.mySwiper} > {carousel.map((imgPath, index) => { return( <SwiperSlide key={index}> <Image src={imgPath} width={1000} height={600} className={styles.carousel} alt='carousel' /> </SwiperSlide> ); })} </Swiper> </> ); } ``` ## 4.RWD ### 圖片大小調整 因為圖片長寬比例與Image元件設定比例不同,所以加上aspect-ratio。 ```css .image { max-width: 100%; height: auto; aspect-ratio: 640/450; object-fit: cover; object-position: 50% 50%; } ``` ### 用media query修改grid gap, padding, margin可以使用相對尺寸(rem, vw) ```css .container { display: grid; grid-template-columns: repeat(2, minmax(100px, 1fr)); padding: 50px 5vw 50px 5vw; column-gap: 5vw; row-gap: 5vw; justify-items: center; @media (max-width: 768px) { column-gap: 0px; padding: 30px 20px 30px 20px; grid-template-columns: repeat(1, minmax(100px, 1fr)); } } ``` ### 使用MUI useMediaQuery 用className套用css在MUI元件上失敗時,可以直接傳參數到MUI元件,以Swiper為例。 ```javascript export default function Carousel(props){ const matches = useMediaQuery('(min-width:768px)'); return ( <> <Swiper //... style={{aspectRatio: matches ? 1000/600 : 640/450}} > {carousel.map((imgPath, index) => { return( <SwiperSlide key={index}> <Image src={imgPath} width={1000} height={600} className={styles.carousel} alt='carousel' /> </SwiperSlide> ); })} </Swiper> </> ); } ``` ### 文字斷行 ```css .intro { @media (max-width: 768px) { word-wrap: break-word; hyphens: auto; } } ``` ## 5.建置及部署 流程參考[How to Deploy Next.js Apps to Github Pages ](https://www.freecodecamp.org/news/how-to-deploy-next-js-app-to-github-pages/) ### 修改next.config.mjs 使用Github Actions部署於Github Pages。修改`next.confing.mjs`,basePath為`/REPO_NAME`,`output: export`會在build之後產出一個out資料夾。 ```javascript /** @type {import('next').NextConfig} */ const nextConfig = { basePath: "/portfolio", output: "export", reactStrictMode: true, }; export default nextConfig; ``` 根據[官方文件](https://nextjs.org/docs/app/api-reference/next-config-js/basePath),next/link不會受到basePath影響,但是next/image必須要修改src路徑。 ```javascript= //page.js export default function About(){ return( <Image priority src="/portfolio/images/about/person.jpg" width={300} height={300} alt="photo" className={styles.image} /> ); } ``` ### 本地端測試build build完後註解掉output: 'export',才能執行npm run start。 ``` npx next build cd out npm run start ``` ### 修改YAML檔 使用github預設產生的YAML檔會有error,參考[gregrickaby/nextjs-github-pages](https://github.com/gregrickaby/nextjs-github-pages),移除(註解)以下兩個部分。 ```yaml - name: Setup Pages uses: actions/configure-pages@v4 with: # static_site_generator: next # - name: Static HTML export with Next.js # run: ${{ steps.detect-package-manager.outputs.runner }} next export ``` * 根據[actions/configure-pages的source code](https://github.com/actions/configure-pages/blob/main/src/set-pages-config.js),static_site_generator: next會設定basePath以及關掉server side image optimization,但是在next.config.mjs已經設定過的話可能會衝突,[starter-workflows](https://github.com/actions/starter-workflows/blob/main/pages/nextjs.yml)的註解中建議有手動設定config就可以移除這一行。 * 根據[官方文件](https://nextjs.org/docs/app/building-your-application/upgrading/version-14),next export在nextjs v14中被output: 'export'取代