# Next.js建立作品集網站
* 網址: https://archielai.github.io/portfolio/
* github repo: https://github.com/ArchieLai/portfolio
## 1.啟動專案
* User story:「身為對建築設計有興趣的使用者,需要瀏覽設計作品圖片,因為想獲得靈感啟發、或進一步瞭解作者。」
* UI 設計:

## 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'取代