# 5/17 部課 ## TODO - [x] graphql - [x] 填表單 - [ ] 練習coursemap ## GraphQL 上次簡單介紹過GraphQL後端了 (想複習的請參考[這裡](https://hackmd.io/Bo7_enSbQPeFZEhcefLr3Q)) 不過只介紹了Query,假設我們需要新增(修改)資料,在GraphQL中是使用 Mutation 這個方法 可以做個簡單對照: GET <--> Query POST <--> Mutation 我們將實作三個功能: **Query** books: 把所有books找出來 **Mutation** CreateBook: 新增一本書 DeleteBook: 刪除一本書 以下為範例的程式碼: ```bash mkdir graphql cd graphql npm init -y mkdir backend cd backend npm init -y npm pkg set type="module" npm install @apollo/server graphql nodemon # 下列的程式碼都要放在 backend 資料夾裡 ``` 定義Types In `schema.gql`: ```graphql= type Book { id: ID! title: String! author: String! publishDate: String! } type Query { books: [Book!]! } type Mutation { createBook(title: String!, author: String!, publishDate: String!): Book! deleteBook(id: ID!): Book! } ``` 創建一些local資料 In `data/books.js`: ```javascript= export let books = [ { id: 1, title: "The Great Gatsby", author: "F. Scott Fitzgerald", publishDate: "1925-04-10" }, { id: 2, title: "To Kill a Mockingbird", author: "Harper Lee", publishDate: "1960-07-11" } ]; ``` 定義resolvers In `resolvers/Query.js`: ```javascript= import { books } from '../data/books.js'; export const Query = { books: () => books }; ``` In `resolvers/Mutation.js`: ```javascript= import { books } from '../data/books.js'; export const Mutation = { createBook: (_, { title, author, publishDate }) => { const newBook = { id: books.length + 1, title, author, publishDate }; books.push(newBook); return newBook; }, deleteBook: (_, { id }) => { const bookIndex = books.findIndex(book => book.id === parseInt(id)); if (bookIndex === -1) return null; const [deletedBook] = books.splice(bookIndex, 1); return deletedBook; } }; ``` 把上述function合併至index.js中 In `index.js`: ```javascript= import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { readFileSync } from "fs"; import { Query } from "./resolvers/Query.js"; import { Mutation } from "./resolvers/Mutation.js"; // Read type definitions from schema.gql const typeDefs = readFileSync("./src/schema.gql", "utf8"); // Resolvers const resolvers = { Query, Mutation, }; // Create Apollo Server const server = new ApolloServer({ typeDefs, resolvers }); // Start the server const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, }); console.log(`🚀 Server ready at: ${url}`); ``` 一樣,要能夠hot reload的話,請安裝nodemon,並且在指令的地方,把node 換成 nodemon。 ``` graphql ├── backend │   ├── node_modules/ │   ├── package-lock.json │   ├── package.json │   └── src │   ├── data │   │   └── books.js │   ├── index.js │   ├── resolvers │   │   ├── Mutation.js │   │   └── Query.js │   └── schema.gql └── package.json ``` 後端架設好之後,我們就來用前端操作吧! ```bash # 回到 graphql cd .. npm create vite@latest √ Project name: ... frontend √ Select a framework: » React √ Select a variant: » JavaScript cd frontend npm install npm install @apollo/client graphql ``` 創建Apollo Provider: In `src/ApolloProvider.jsx`: ```javascript= import React from "react"; import { ApolloClient, InMemoryCache, ApolloProvider as Provider } from "@apollo/client"; const client = new ApolloClient({ uri: "http://localhost:4000", // Replace with your GraphQL server URL cache: new InMemoryCache(), }); const ApolloProvider = ({ children }) => { return <Provider client={client}>{children}</Provider>; }; export default ApolloProvider; ``` 在App.jsx中定義剩餘介面 In `App.jsx`: ```javascript= import React, { useState } from "react"; import { gql, useQuery, useMutation } from "@apollo/client"; import ApolloProvider from "./ApolloProvider"; import "./App.css"; const GET_BOOKS = gql` query GetBooks { books { id title author publishDate } } `; const CREATE_BOOK = gql` mutation CreateBook($title: String!, $author: String!, $publishDate: String!) { createBook(title: $title, author: $author, publishDate: $publishDate) { id title author publishDate } } `; const DELETE_BOOK = gql` mutation DeleteBook($id: ID!) { deleteBook(id: $id) { id } } `; const App = () => { const { loading, error, data, refetch } = useQuery(GET_BOOKS); const [createBook] = useMutation(CREATE_BOOK); const [deleteBook] = useMutation(DELETE_BOOK); const [title, setTitle] = useState(""); const [author, setAuthor] = useState(""); const [publishDate, setPublishDate] = useState(""); const handleAddBook = async () => { await createBook({ variables: { title, author, publishDate }, }); refetch(); setTitle(""); setAuthor(""); setPublishDate(""); }; const handleDeleteBook = async (id) => { await deleteBook({ variables: { id } }); refetch(); }; if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <div className="container"> <h1>Books</h1> <ul> {data.books.map((book) => ( <li key={book.id}> {book.title} by {book.author} (Published: {book.publishDate}) <button onClick={() => handleDeleteBook(book.id)}>Delete</button> </li> ))} </ul> <h2>Add a New Book</h2> <div className="form"> <input type="text" placeholder="Title" value={title} onChange={(e) => setTitle(e.target.value)} /> <input type="text" placeholder="Author" value={author} onChange={(e) => setAuthor(e.target.value)} /> <input type="text" placeholder="Publish Date" value={publishDate} onChange={(e) => setPublishDate(e.target.value)} /> <button onClick={handleAddBook}>Add Book</button> </div> </div> ); }; const WrappedApp = () => ( <ApolloProvider> <App /> </ApolloProvider> ); export default WrappedApp; ``` 引入 App.css In `src/App.css`: ```css= body { font-family: Arial, sans-serif; background-color: #f0f0f0; margin: 0; padding: 0; } .container { max-width: 800px; margin: 0 auto; padding: 20px; background-color: #ffffff; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); } h1 { text-align: center; color: #333; } ul { list-style-type: none; padding: 0; } li { background-color: #fafafa; margin: 10px 0; padding: 10px; border: 1px solid #ddd; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; } button { background-color: #ff4d4d; border: none; color: white; padding: 5px 10px; border-radius: 4px; cursor: pointer; } button:hover { background-color: #ff3333; } .form { display: flex; flex-direction: column; gap: 10px; margin-top: 20px; } .form input { padding: 10px; border: 1px solid #ddd; border-radius: 4px; } .form button { background-color: #4caf50; border: none; color: white; padding: 10px; border-radius: 4px; cursor: pointer; } .form button:hover { background-color: #45a049; } ``` 最後,把index.css這個檔案刪掉,並且在`index.js`的地方也把 `import "./index.css"` 拿掉 再來就是跑`npm run dev` 就可以啟動前端了!(但是後端要跑起來,前端才會顯示資料) subscription: 訂閱,就像訂閱youtube頻道,當該youtuber發佈影片時,你會收到他發新片的「通知」 而一個block假如也有訂閱某些事件的時候(例如剛剛的createbook),books的陣列一更新,那個block也會馬上收到相關的通知。這裡是用`refetch()` 的方法來重新執行Query以獲取最新的資料,對於固定頻率更新資料或是經過mutation的操作較為適用。 ## 練習操作 coursemap ### 使用 coursemap 1. 把 [Coursemap](https://github.com/NTUEEInfoDep/NTUEECourseMap) 的 repo clone 下來 (HTTPS 也可以,用SSH需要設定key,請參照 [Generate a new SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent)) ![image](https://hackmd.io/_uploads/HyGguM9-C.png) ```bash cd your-target-directory # git clone [put the copied url here] git clone https://github.com/NTUEEInfoDep/NTUEECourseMap.git ``` 2. 進入coursemap 資料夾裡,安裝專案需要的套件 ```bash cd NTUEECourseMap # yarn 的速度較 npm 快不少,因此使用 yarn 安裝跟執行 # 使用 yarn yarn install # 啟動 coursemap (下列兩種都可以) yarn run dev # 或 yarn start ``` 接著,前往 http://localhost:8000/ ,看到畫面就成功囉! ### 使用系學會網站 目前還有一些設定沒有完成,之後有弄好再更新 (demo 連結在上方) [系學會網站](https://eesa.ntuee.org/) (目前本地跑不動) ### 實習地圖 未定 styled-component https://styled-components.com/ MUI tailwind