---
# System prepended metadata

title: AppWorks Personal Project

---

# AppWorks Personal Project
#### Brief: 復刻[Church Notes](https://www.churchnotesapp.com/),加上[Obsidian](https://help.obsidian.md/Plugins/Graph+view)的GraphView功能
## unit test
[TDD](https://medium.com/%E6%88%91%E6%83%B3%E8%A6%81%E8%AE%8A%E5%BC%B7/tdd-test-driven-development-%E6%B8%AC%E8%A9%A6%E9%A9%85%E5%8B%95%E9%96%8B%E7%99%BC-%E5%85%A5%E9%96%80%E7%AF%87-e3f6f15c6651)測試導向開發:先把測試＆預期結果寫好,再去開發function
用[jest](https://jestjs.io/docs/getting-started)框架,[test檔名命名方式](https://create-react-app.dev/docs/running-tests)
* describe:測試描述
* test:測試內容,可以改為語意化的寫法(describe it ...)
```
describe('add function', () => {
  it('should sum 1 + 2 to equal 3', () => {
    expect(sum(1, 2)).toBe(3);
  });
});
```
* 加x的可以跳過
```
describe('add function', () => {
  xit('should sum 1 + 2 to equal 3', () => {    //這邊
    expect(sum(1, 2)).toBe(3);
  });
});

xdescribe('add function', () => {    //這邊
  it('should sum 1 + 2 to equal 3', () => {
    expect(sum(1, 2)).toBe(3);
  });
});
```
![](https://hackmd.io/_uploads/S1sb87o42.png)
* 門檻：需熟悉測試的語法跟方法 [mock/stub/spy/fake](https://medium.com/@henry-chou/%E5%96%AE%E5%85%83%E6%B8%AC%E8%A9%A6%E4%B9%8B-mock-stub-spy-fake-%E5%82%BB%E5%82%BB%E6%90%9E%E4%B8%8D%E6%B8%85%E6%A5%9A-ba3dc4e86d86)
* `npm test -- --coverage`可以知道測試覆蓋率
* 比較常做的UI測試:[Snapshot Testing](https://jestjs.io/docs/tutorial-react#snapshot-testing):把當下的畫面快照下來,去對照
* [測試使用者行為](https://jestjs.io/docs/tutorial-react)
## 互測
[互測重點](https://www.notion.so/d1cd99dccca943c98442bbf0e826ef90)
## Custom Hook
其實就是單純的拆function,並且function裡有用到react的原生hook,就是Custom Hook
```
//很像useRef
function useHashtags = () {
    const [hashTags, setHashtags] = useState();
    useEffect(()=>{...},[]);
    ...

    return hashTags;
}

const Component = () => {
    const hashTags = useHashTags();
    ...
}
```
```
//很像useState
function useHashtags = () {
    const [hashTags, setHashtags] = useState();
    ...

    return [hashTags, setHashtags];
}

const Component = () => {
    const [hashTags, setHashtags] = useHashTags();
    ...
}
```
```
//可傳參數
function useHashtags = (arg) {
    const [hashTags, setHashtags] = useState();
    const [isLoading, setIsLoading] = useState();
    ...

    return {hashTags, setHashtags, isLoading};
}

const Component = () => {
    const {hashTags, setHashtags,isLoading} = useHashTags(arg);
    ...
}
```
* 官方建議hook名稱**用use開頭**,如果有用,react會幫你檢查有沒有符合hook規則
## [Presentational and Container Components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)
共用元件管理分類
UI越偏向Presentational越好
* [classnames](https://www.npmjs.com/package/classnames): className管理套件
* [tailwindcss-classnames](https://www.npmjs.com/package/tailwindcss-classnames): tailwind className管理套件

## 專案管理
[Trello](https://trello.com/b/h5AdKvKx/yuchien-%E5%97%8E%E5%93%AA%E7%BD%90%E5%AD%90)
[plany way](https://planyway.com/)
## 開發紀錄
### redux + TypeScript 開發
研究官方文件和counter範例花不少時間,而且看不到進度會很緊張。reducerSlice裡同步的有弄懂,但非同步(Thunk)有點卡關。
**卡關**：後來找業界的朋友幫忙教學,發現有useContext + useRerducer加上對redux有初步研究,被教的時候很快就能理解asyncThunk(redux的code style蠻抽象的)
### Auto Verse
手刻功能搭配open api,利用chatGPT生成regex語法開發很順利,並且很幸運能兼容react-Quill
### Backlinks
使用react-Quill/quill-mention套件搭配redux,專案中最困難的功能,原理如下:
1. 先從redux拿到使用者所有的筆記並生成quill-mention能使用的data,再將quill-mention模組匯入react-Quill實現＃轉換a連結功能
**卡關**：因為quill-mention模組是動態生成的,而react-Quill沒辦法接受變動的module參數(編輯器會跑不出來)而卡關,後來谷哥提議可使用useMemo來避免component re-render時重新生成quill-mention模組,再判斷如果redux資料尚未從firestore取得時,先避免渲染,待資料取得並生成模組後再渲染react-Quill編輯器。
**待解bug**:目前有使用者帳戶不能沒有筆記的bug,可能是模組不能沒有選項 || 我條件沒設好,暫時先用創建帳戶時就先塞一篇“**教學筆記**”給使用者解決
2. 生成link_notes資料給graphView使用
因Quill生成的資料為html格式,送出筆記時需透過regex先判斷有多少link,再把id存入link_notes array。
**待解bug**:link_notes資料結構現在是`{id:string,title:string}[]`,但其實用不到`title`,所以應該要改為`string[]`(為了抓title還跟chatGPT吵架,結果沒用到)
### GraphView
使用react-graph-vis套件搭配redux開發,分為full-graph跟note-graph(共用component)
1. full-graph:從redux先抓出所有的筆記title跟id生成nodes資料,再抓出所有筆記的link_notes生成edge資料,再根據node被連結的次數來計算node的大小(被連越多次越大顆)
2. note-graph:先從網址取得當前筆記的id,再過濾nodes資料,把跟此id沒任何關聯的筆記排除

react-graph-vis保留不少彈性,可以客製化style和針對不同事件的行為,而且edges資料與nodes資料不對應時也不會報錯,單純不顯示,非常好用!
### CSS [flex子元素宽度超出父元素]會造成推擠其他內容的問題(https://juejin.cn/post/6974356682574921765)
#### 解法
设置min-width：0可以解决当flex子元素的子元素大小为auto的情况；
设置overflow不为visible可以解决所有情况下的麻烦；
* grow的計算原理：
```
子元素宽度 = 子元素内容占据宽度(即content宽度) + flex: 1属性分配的宽度
```
![](https://i.imgur.com/IiilDV9.png)
### canva算graphView尺寸調整會拉伸的問題
可以用key={toggle}解: component偵測到key改變會重新un-mount,會重新做一次
## 好用的html tag
* progress 可以做進度條
* `<input required>` 可以指定必填
## Sue小波浪
[Creating an animated wave line with Tailwind CSS](https://daily-dev-tips.com/posts/creating-an-animated-wave-line-with-tailwind-css/)
[CSS Wavy Line Animation Effect For Website](https://www.youtube.com/watch?v=BaYd9Y_J71o)
## 功能spec:
### 輸入經節可自動插入經文:
1.[聖經api](https://bible.fhl.net/json/qb.php)
2.研究技術如何做到
### 分類整理筆記:
1.拖拉筆記位置
### 心智圖:
1.展示出有link的筆記
2.[Zoom-in Zoom-out](https://www.upbeatcode.com/react/how-to-implement-zoom-image-in-react/)
## 需研究套件:
[React-Graph-Vis](https://github.com/crubier/react-graph-vis/tree/master/example)
[React-Markdown](https://github.com/remarkjs/react-markdown)
[學長姐作品](https://bear-plus.yenchenkuo.com/home)
## API:
[信望愛站聖經 JSON API](https://bible.fhl.net/json/)
## [架構圖](https://www.figma.com/file/ztRMfXj4ADbe1Po9FWqVjQ/%E5%80%8B%E4%BA%BA%E5%B0%88%E6%A1%88%E6%9E%B6%E6%A7%8B?node-id=0-1&t=03uwxZJzWMBM6fnf-0)

## Chrome exetention
[校友作品](https://chrome.google.com/webstore/detail/catalyst/jhpdbbakbcmoaondmjjjbojlakgkeilo)
[校友作品](https://chrome.google.com/webstore/detail/tabshaker/eigbinbgomoampdljpjabchdempfileg?hl=zh-TW&authuser=0)

## 技術選型
#### firebase cloud function
1. next.js
2. firebase cloud function(可以很快地寫出api/不用管理server)
3. 幫你把cors的api導回自己的firebase的server
#### firebase cloud storage
1. 注意容量
2. demo帳號/限制檔案大小
#### build -> extention
1. fuzzy search ->cloud function會去監聽firestore的資料,放到 [algolia](https://www.algolia.com/) search(第三方)裡,帳號要先升級
#### authentication 第三方登入
* 可以在前端寫第一步的阻擋`if(!userid){ return }`
* 或是在firestore的rule裡寫阻擋規則
#### firestore
* 如果有兩個where,會是複合式索引,如果撈不到資料,錯誤訊息關鍵字有索引,有可能是這個原因
* 可以利用索引找分類(key的概念)
* 設計資料結構：
看wirefram,看畫面需要哪些資料,再建立資料庫,我們會知道怎樣的格式最容易做渲染
* 是否容易query?
* firebase的search只會query那一筆的流量,前端search就是所有的
* [搜尋的運算子](https://firebase.google.com/docs/firestore/query-data/queries#query_operators)
* 關鍵字where in可以去bars裡抓某個array裡所有的id
* array contains
* [onAuthStateChanged](https://firebase.google.com/docs/auth/web/manage-users): 監聽登入狀態保持登入功能
#### firebase config
```
const firebaseConfig = {
  apiKey: "AIzaSyAizA6LfYHyWib7-7cZb0LVZmpu0pa2qAo",
  authDomain: "manna-jar.firebaseapp.com",
  projectId: "manna-jar",
  storageBucket: "manna-jar.appspot.com",
  messagingSenderId: "806344207818",
  appId: "1:806344207818:web:e892e0e500e7748ed2f333",
  measurementId: "G-NB0SG7Q4HY"
};
```
## POC紀錄
### redux: reducer/dispatch
[RTK](https://redux-toolkit.js.org/rtk-query/overview)的API [Thunk](https://pjchender.dev/react/redux-thunk/):處理async事件 | React Redux:provider/Hook｜[複數reducer處理方法](https://ithelp.ithome.com.tw/articles/10275697) | [官網基礎](https://redux.js.org/tutorials/essentials/part-1-overview-concepts) | [Redux & RTK](https://www.freecodecamp.org/news/redux-and-redux-toolkit-for-beginners/)
### [Firebase Authentication](https://javascript.plainenglish.io/firebase-authentication-with-firestore-database-78e6e4f348c6) with Firestore
```
export const signupUser = (userDetails) => {
    //deconstruct the users details we will need these later
    const {firstName, lastName, birthday, email, password} = userDetails
    return () => {
        //user firebase using the appropriate firebase method
        firebase.auth().createUserWithEmailAndPassword(email, password)
        .then(() => {
            //Once the user creation has happened successfully, we can add the currentUser into firestore
            //with the appropriate details.
            firebase.firestore().collection('users').doc(firebase.auth().currentUser.uid)
            .set({
                firstName: firstName,
                lastName: lastName,
                birthday: birthday,
                email: email
            })
            //ensure we catch any errors at this stage to advise us if something does go wrong
            .catch(error => {
                console.log('Something went wrong with added user to firestore: ', error);
            })
        })
        //we need to catch the whole sign up process if it fails too.
        .catch(error => {
            console.log('Something went wrong with sign up: ', error);
        }
    },
};
```
```
import {GoogleSignin} from '@react-native-community/google-signin';
import auth, {firebase} from '@react-native-firebase/auth';
import firestore from '@react-native-firebase/firestore';

GoogleSignin.signIn()
.then(data => {
    // data provides us with an idToken and accessToke, we use these to set up a credential
    const googleCredential = auth.GoogleAuthProvider.credential(data.idToken,data.accessToken);
    try {
        firebase.auth().signInWithCredential(googleCredential)
        .then(user => {
            //after we have the credential - lets check if the user exists in firestore
            var docRef = firestore().collection('users').doc(auth().currentUser.uid);

            docRef.get()
            .then(doc => {
                if (doc.exists) {
                //user exists then just update the login time
                return user
                } else {
                //user doesn't exist - create a new user in firestore
                resolve(addNewUserToFirestore(user));
                }
            })
            .catch(error => {
                console.error('Checking if customer exists failed" ' + error);
            });
        })
        .catch(error => {
            console.error('GoogleSignIn to firebase Failed ' + error);
        })
    } catch (error) {
        console.log("Something generic went wrong, ", error )
    }
})
.catch(error => {
    console.error('GoogleSignIn to firebase Failed ' + error);
})

function addNewUserToFirestore(user) {
  const collection = firestore().collection('users');
  const {profile} = user.additionalUserInfo;
  const details = {
    firstName: profile.given_name,
    lastName: profile.family_name,
    fullName: profile.name,
    email: profile.email,
    picture: profile.picture,
    createdDtm: firestore.FieldValue.serverTimestamp(),
    lastLoginTime: firestore.FieldValue.serverTimestamp(),
  };
  collection.doc(auth().currentUser.uid).set(details);
  return {user, details};
}
```
### Auto Verse
[Auto Verse poc](https://codesandbox.io/s/autoverse-umzlk4)
```
import "./styles.css";
import React, { useState } from "react";

const BibleReference = async (book, chapter, verse) => {
  try {
    const response = await fetch(
      `https://bible.fhl.net/json/qb.php?chineses=${book}&chap=${chapter}&sec=${verse}`
    );
    const data = await response.json();
    let verses = `${book} ${chapter}:${verse}\n`;
    data.record.map((sec) => {
      verses += `[${sec.sec}]${sec.bible_text}`;
    });
    return verses;
  } catch (error) {
    console.error(error);
  }
};

function App() {
  const [noteContent, setNoteContent] = useState("");

  const handleKeyDown = async (event) => {
    // Check if the pressed key is the space bar
    if (event.key === " ") {
      // Get the current cursor position in the text area
      const cursorPosition = event.target.selectionStart;
      // Split the text content into two parts:
      // 1. text before the cursor position
      // 2. text after the cursor position
      const beforeCursor = noteContent.slice(0, cursorPosition);
      const afterCursor = noteContent.slice(cursorPosition);
      // Find the last Bible reference before the cursor position
      const lastReference = beforeCursor.match(
        /([\u4E00-\u9FFF]{1,2})(\d+):([\d,-]+)/
      );

      if (lastReference) {
        // If there is a Bible reference, insert the BibleReference component
        const referenceStart = beforeCursor.lastIndexOf(lastReference[0]);
        const referenceEnd = referenceStart + lastReference[0].length;

        const book = lastReference[1];
        const chapter = lastReference[2];
        const verse = lastReference[3];
        await BibleReference(book, chapter, verse) // Call BibleReference component with reference prop
          .then((text) => {
            setNoteContent(
              beforeCursor.slice(0, referenceStart) +
                text +
                afterCursor.slice(referenceEnd)
            );
          });
        // Prevent the space key from being inserted into the text area
        event.preventDefault();
      }
    }
  };

  return (
    <div>
      <textarea
        value={noteContent}
        onChange={(event) => setNoteContent(event.target.value)}
        onKeyDown={handleKeyDown}
      />
    </div>
  );
}

export default App;
```
### backlink實作
#### Quill-mention
[quill-mention](https://github.com/quill-mention/quill-mention)
[參考轉成link的方案](https://stackoverflow.com/questions/69704201/how-to-insert-link-for-hashtags-and-mentions-in-react-quill)
[poc轉成`<a href="">`](https://codesandbox.io/s/quill-mentions-bian-cheng-lian-jie-lftx1i)
[markdown parser poc](https://codesandbox.io/s/zi-ding-yi-react-markdown-parser-s1u4g5)
![](https://i.imgur.com/e5tAw1M.png)
* [Tailwind處理Quill h1大小方法](https://tools.wingzero.tw/article/sn/973)
#### 自定義markdown parser
客製化可以解讀`#title(url?id=note.id)`的parser
```
md = md.replace(/#(\w+)\(([^)]+)\)/g, (match, title, url) => {
    return `<a value=`${url}` class="text-red" href=${url}>${title}</a>`;
  });
```
* 一定要透過dangerouslySetInnerHTML,所以不能吃component, style也要用tailwind
* 可以透過bubbling事件來達到`<Link>`的功能
```
<div
    dangerouslySetInnerHTML={{ __html: note }}
    style={{ border: "1px solid #006888", padding: "0 10px" }}
    onClick={//TODO:bubbling的事件,用nevigate to來實現link的功能}
></div>
```
#### 編輯器
1.先把notes全部抓下來,title/id存成state
```
useEffect(() => {
  const unsubscribe = db.collection("notes").onSnapshot(snapshot => {
    const notesData = snapshot.docs.map(doc => {
      const { title } = doc.data();
      return 
      //TODO要製作graph{nodes:[...],edges:[...]}還有抓出跟當前筆記有關的Links
      {
        id: doc.id,
        title
      };
    });
    setNotes(notesData);
  });

  return () => unsubscribe();
}, []);
```
2.onKeyPress事件(e.target.value === #)跳出輸入框,根據輸入搜尋state裡的id,下方會顯示dropdown供點擊
3.點擊選項(id)後會自動置入#note.title(url?id=note.id)
4.點擊submit時會抓取符合#note.title(url?id=note.id)規則的資料,加入link-notes[]裡存到firestore
```
const regex = /#(\w+)\(([^)]+)\)/g;
const text = 'This is a #Note1(/note?id=1) and #Note2(/note?id=2).';
const matches = text.matchAll(regex);

for (const match of matches) {
  console.log(match);
}
```
在使用matchAll()方法後，matches會是一個Iterator物件，可以使用for...of迴圈或是將其轉為陣列後使用。
```
Array [ "#Note1(/note?id=1)", "Note1", "/note?id=1", index: 10, input: "This is a #Note1(/note?id=1) and #Note2(/note?id=2)." ]
Array [ "#Note2(/note?id=2)", "Note2", "/note?id=2", index: 26, input: "This is a #Note1(/note?id=1) and #Note2(/note?id=2)." ]
```
### [AI助理](https://platform.openai.com/docs/guides/chat/chat-vs-completions)
`openai.createChatCompletion()` 方法接受以下参数：
1. **model**：指定要使用的模型。目前支持的模型是 "gpt-3.5-turbo"，它是 OpenAI GPT-3.5 Turbo 模型，具有强大的生成能力。
2. **messages**：包含一个数组，其中包含对话中的用户和助手角色的消息。每个消息都有两个属性：**role** 表示角色 和 **content** 表示消息内容。
**role**包括:
-**system**：系统角色，用于传递系统级指令或提示，以控制生成模型的行为。
-**user**：用户角色，表示用户的消息或输入。
-**assistant**：助手角色，表示生成模型的回复或生成的文本。

其他系统级参数：您可以在 messages 中使用 system 角色来向模型传递系统级指令，例如更改温度或最大令牌限制，以影响生成的行为。

**Response format：**
```
{
 'id': 'chatcmpl-6p9XYPYSTTRi0xEviKjjilqrWU2Ve',
 'object': 'chat.completion',
 'created': 1677649420,
 'model': 'gpt-3.5-turbo',
 'usage': {'prompt_tokens': 56, 'completion_tokens': 31, 'total_tokens': 87},
 'choices': [
   {
    'message': {
      'role': 'assistant',
      'content': 'The 2020 World Series was played in Arlington, Texas at the Globe Life Field, which was the new home stadium for the Texas Rangers.'},
    'finish_reason': 'stop',
    'index': 0
   }
  ]
}
```
### react-graph-vis
[typeScript定義檔](https://gist.github.com/ChadJPetersen/2e2587bbd753c6a384c02519183e2031)
[graphview的codesandbox]()
### D3
了解如何使用D3.js和React来实现一个简单的图表：
```
import React, { useEffect, useRef } from 'react';
import * as d3 from 'd3';

function GraphView({ data }) {
  const svgRef = useRef();

  useEffect(() => {
    const svg = d3.select(svgRef.current);
    const nodes = data.nodes;
    const links = data.links;

    // 创建力导向图力学模拟器
    const simulation = d3.forceSimulation(nodes)
      .force('link', d3.forceLink(links).id(d => d.id))
      .force('charge', d3.forceManyBody().strength(-100))
      .force('center', d3.forceCenter(400, 400));

    // 绘制节点
    const node = svg.selectAll('.node')
      .data(nodes)
      .enter()
      .append('circle')
      .attr('r', 10)
      .attr('fill', '#2196f3')
      .call(d3.drag()
        .on('start', dragstart)
        .on('drag', drag)
        .on('end', dragend));

    // 绘制连线
    const link = svg.selectAll('.link')
      .data(links)
      .enter()
      .append('line')
      .attr('stroke', '#bdbdbd')
      .attr('stroke-width', 2);

    // 更新节点和连线位置
    function ticked() {
      link
        .attr('x1', d => d.source.x)
        .attr('y1', d => d.source.y)
        .attr('x2', d => d.target.x)
        .attr('y2', d => d.target.y);

      node
        .attr('cx', d => d.x)
        .attr('cy', d => d.y);
    }

    // 开始拖拽节点
    function dragstart(event, d) {
      if (!event.active) simulation.alphaTarget(0.3).restart();
      d.fx = d.x;
      d.fy = d.y;
    }

    // 拖拽节点过程中更新节点位置
    function drag(event, d) {
      d.fx = event.x;
      d.fy = event.y;
    }

    // 结束拖拽节点
    function dragend(event, d) {
      if (!event.active) simulation.alphaTarget(0);
      d.fx = null;
      d.fy = null;
    }

    // 每次模拟器更新时调用ticked函数更新节点和连线位置
    simulation.on('tick', ticked);

    // 在组件卸载时停止模拟器
    return () => {
      simulation.stop();
    };
  }, [data]);

  return (
    <svg ref={svgRef} width="800" height="800"></svg>
  );
}

export default GraphView;

```
Obsidian的Graph View使用了D3.js和webcola这两个开源库来实现。其中，D3.js用于创建可交互的力导向图，而webcola用于布局节点和连线。同时，Obsidian还使用了一些自定义的代码来增强Graph View的功能，例如支持节点标签、过滤器和笔记搜索等。

Logseq的Graph View同样使用了D3.js来创建力导向图，并使用了React和Redux来构建用户界面和管理应用程序的状态。Logseq的Graph View支持对笔记进行标记、搜索和筛选，并且可以根据笔记的重要性和相似性来调整节点的大小和颜色。此外，Logseq还提供了一个强大的插件API，使用户可以自由地扩展和定制Graph View的功能。
### Link的hover事件POC
1. `dangerouslySetInnerHTML={{ __html: currentNote.content }}`沒辦法做到分辨node,因為置入的html不會生成node
2. console.dir可以印出HTMLnode的對應屬性
3. React的[hover](https://upmostly.com/tutorials/react-onhover-event-handling-with-examples)事件為
* onMouseEnter={() => {}}
* onMouseLeave={() => {}}
* onMouseOver={() => {}}
```
onMouseEnter={(e) => {
  const nodeList = e.target.childNodes;
  nodeList.forEach((node) =>
    console.log(node.childNodes[0].dataset?.id)
  );
}}
```
目前可做到取得links的id值,但無法解決位置問題
### 分頁功能
```
// state
const [currentPage, setCurrentPage] = useState<number>(1);

// 變數
const PAGE_SIZE = 10;
const totalPages = Math.ceil(articles.length / PAGE_SIZE);
// currentPageArticle 這個變數放當前頁面要顯示的文章（displayedArticles 是可以使用搜尋功能的文章，預設會顯示所有文章）
const currentPageArticles = displayedArticles
     // 依時間排序
    .sort((a, b) => {
      if (a.date && b.date) {
        return new Date(b.date).getTime() - new Date(a.date).getTime();
      } else {
        return 0;
      }
    })
    // 用 slice 切出點到的分頁要顯示的文章
    .slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);

const handlePageChange = (pageNumber: number) => {
    setCurrentPage(pageNumber);
  };


// return 出 pagination 的元件
<Pagination>
  {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
    <button
      key={page}
      onClick={() => handlePageChange(page)}
      disabled={page === currentPage}
    >
      {page}
    </button>
  ))}
</Pagination>
```
