BLOG-8 Set up gitea action #22
							
								
								
									
										35
									
								
								.gitea/workflows/frontend-ci.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								.gitea/workflows/frontend-ci.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| name: Frontend CI | ||||
| on: [push] | ||||
| 
 | ||||
| defaults: | ||||
|   run: | ||||
|     working-directory: ./frontend | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v4 | ||||
| 
 | ||||
|       - name: Install pnpm | ||||
|         uses: pnpm/action-setup@v4 | ||||
|         with: | ||||
|           version: 10 | ||||
|           run_install: false | ||||
| 
 | ||||
|       - name: Install node.js | ||||
|         uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: 20 | ||||
|           cache: "pnpm" | ||||
|           cache-dependency-path: frontend/pnpm-lock.yaml | ||||
| 
 | ||||
|       - name: Install dependencies | ||||
|         run: pnpm install | ||||
| 
 | ||||
|       - name: ESLint | ||||
|         run: pnpm run lint | ||||
| 
 | ||||
|       - name: Build | ||||
|         run: pnpm run build | ||||
| @ -9,6 +9,7 @@ | ||||
|     "start": "next start", | ||||
|     "lint": "next lint" | ||||
|   }, | ||||
|   "packageManager": "pnpm@10.0.0", | ||||
|   "dependencies": { | ||||
|     "@fortawesome/fontawesome-svg-core": "^6.7.2", | ||||
|     "@fortawesome/free-brands-svg-icons": "^6.7.2", | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| "use client"; | ||||
| 
 | ||||
| import { useEffect, useState } from "react"; | ||||
| import { useEffect, useRef, useState } from "react"; | ||||
| import { Provider } from "react-redux"; | ||||
| 
 | ||||
| import { useTagDispatch, useTagSelector } from "@/lib/home/presenter/tagHooks"; | ||||
| import { tagStartedAction, tagStoppedAction } from "@/lib/home/presenter/tagSlice"; | ||||
| import { useTagDispatch, useTagSelector } from "@/lib/home/presenter/tagHook"; | ||||
| import { tagShuffledAction } from "@/lib/home/presenter/tagReducer"; | ||||
| import tagStore from "@/lib/home/presenter/tagStore"; | ||||
| 
 | ||||
| export default function SelfTags() { | ||||
| @ -16,25 +16,35 @@ export default function SelfTags() { | ||||
| } | ||||
| 
 | ||||
| function SelfTagsProvided() { | ||||
|   const tags = useTagSelector((state) => state.tag.tags); | ||||
|   const tags = useTagSelector((state) => state.tags); | ||||
|   const dispatch = useTagDispatch(); | ||||
| 
 | ||||
|   // Initialize with placeholder to prevent flickering
 | ||||
|   const [tagsToShow, setTagsToShow] = useState<string[]>([""]); | ||||
|   const [isTagsVisible, setIsTagsVisible] = useState(false); | ||||
| 
 | ||||
|   const timer = useRef<NodeJS.Timeout | undefined>(undefined); | ||||
| 
 | ||||
|   // On mount
 | ||||
|   useEffect(() => { | ||||
|     dispatch(tagStartedAction({ interval: 4000 })); | ||||
|     timer.current = setInterval(() => { | ||||
|       dispatch(tagShuffledAction()); | ||||
|     }, 4000); | ||||
| 
 | ||||
|     return () => { | ||||
|       dispatch(tagStoppedAction()); | ||||
|       setIsTagsVisible(false); | ||||
|       clearInterval(timer.current); | ||||
|       timer.current = undefined; | ||||
|     }; | ||||
|   }, [dispatch]); | ||||
| 
 | ||||
|   // On tags changed
 | ||||
|   useEffect(() => { | ||||
|     if (tags.length === 0) return; | ||||
|     setIsTagsVisible(true); | ||||
|     setIsTagsVisible(false); | ||||
| 
 | ||||
|     setTimeout(() => { | ||||
|       setIsTagsVisible(false); | ||||
|     }, 3500); | ||||
|       setTagsToShow(tags); | ||||
|       setIsTagsVisible(true); | ||||
|     }, 500); | ||||
|   }, [tags]); | ||||
| 
 | ||||
|   return ( | ||||
| @ -43,7 +53,7 @@ function SelfTagsProvided() { | ||||
|     > | ||||
|       <div className="absolute inset-0 bg-gradient-to-r from-transparent via-transparent via-60% to-white" /> | ||||
|       <div className="flex flex-row items-center gap-x-2 overflow-hidden"> | ||||
|         {tags.map((tag) => ( | ||||
|         {tagsToShow.map((tag) => ( | ||||
|           <Hashtag key={tag} tag={tag} /> | ||||
|         ))} | ||||
|       </div> | ||||
|  | ||||
							
								
								
									
										55
									
								
								frontend/src/lib/home/presenter/tagReducer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								frontend/src/lib/home/presenter/tagReducer.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | ||||
| import { createAction, createReducer } from "@reduxjs/toolkit"; | ||||
| 
 | ||||
| import shuffleArray from "@/lib/util/shuffleArray"; | ||||
| 
 | ||||
| export const tagShuffledAction = createAction("tag/shuffled"); | ||||
| 
 | ||||
| const tagReducer = createReducer<TagState>( | ||||
|   () => ({ | ||||
|     tags: shuffleArray(tagsCollection), | ||||
|   }), | ||||
|   (builder) => { | ||||
|     builder.addCase(tagShuffledAction, (state) => { | ||||
|       state.tags = shuffleArray(tagsCollection); | ||||
|     }); | ||||
|   }, | ||||
| ); | ||||
| 
 | ||||
| export default tagReducer; | ||||
| export type TagAction = ReturnType<typeof tagShuffledAction>; | ||||
| 
 | ||||
| export interface TagState { | ||||
|   tags: string[]; | ||||
|   timer?: NodeJS.Timeout; | ||||
| } | ||||
| 
 | ||||
| const tagsCollection = [ | ||||
|   "APP", | ||||
|   "C++", | ||||
|   "Design Pattern", | ||||
|   "Docker", | ||||
|   "Flutter", | ||||
|   "Go", | ||||
|   "Java", | ||||
|   "LINER", | ||||
|   "Linux", | ||||
|   "Python", | ||||
|   "Squid", | ||||
|   "TypeScript", | ||||
|   "中央大學", | ||||
|   "全端", | ||||
|   "分享", | ||||
|   "前端", | ||||
|   "後端", | ||||
|   "教學", | ||||
|   "暴肝", | ||||
|   "知識", | ||||
|   "碼農", | ||||
|   "科技", | ||||
|   "科普", | ||||
|   "程式設計", | ||||
|   "資工系", | ||||
|   "軟體工程", | ||||
|   "遊戲", | ||||
|   "魷魚", | ||||
| ]; | ||||
| @ -1,73 +0,0 @@ | ||||
| import { PayloadAction, createSlice } from "@reduxjs/toolkit"; | ||||
| 
 | ||||
| import tagStore from "@/lib/home/presenter/tagStore"; | ||||
| import shuffleArray from "@/lib/util/shuffleArray"; | ||||
| 
 | ||||
| export interface TagState { | ||||
|   tags: string[]; | ||||
|   timer?: NodeJS.Timeout; | ||||
| } | ||||
| 
 | ||||
| export interface TagStartedActionPayload { | ||||
|   interval: number; | ||||
| } | ||||
| 
 | ||||
| export const tagSlice = createSlice({ | ||||
|   name: "tag", | ||||
|   initialState: { | ||||
|     tags: [], | ||||
|     timer: undefined, | ||||
|   } as TagState, | ||||
|   reducers: { | ||||
|     started: (state, action: PayloadAction<TagStartedActionPayload>) => { | ||||
|       state.tags = shuffleArray(tagsCollection); | ||||
|       state.timer = setInterval(() => { | ||||
|         tagStore.dispatch(tagSlice.actions.shuffled()); | ||||
|       }, action.payload.interval); | ||||
|     }, | ||||
|     shuffled: (state) => { | ||||
|       state.tags = shuffleArray(tagsCollection); | ||||
|     }, | ||||
|     stopped: (state) => { | ||||
|       clearInterval(state.timer); | ||||
|       state.timer = undefined; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| export const tagStartedAction = tagSlice.actions.started; | ||||
| export const tagStoppedAction = tagSlice.actions.stopped; | ||||
| 
 | ||||
| const tagReducer = tagSlice.reducer; | ||||
| export default tagReducer; | ||||
| 
 | ||||
| const tagsCollection = [ | ||||
|   "APP", | ||||
|   "C++", | ||||
|   "Design Pattern", | ||||
|   "Docker", | ||||
|   "Flutter", | ||||
|   "Go", | ||||
|   "Java", | ||||
|   "LINER", | ||||
|   "Linux", | ||||
|   "Python", | ||||
|   "Squid", | ||||
|   "TypeScript", | ||||
|   "中央大學", | ||||
|   "全端", | ||||
|   "分享", | ||||
|   "前端", | ||||
|   "後端", | ||||
|   "教學", | ||||
|   "暴肝", | ||||
|   "知識", | ||||
|   "碼農", | ||||
|   "科技", | ||||
|   "科普", | ||||
|   "程式設計", | ||||
|   "資工系", | ||||
|   "軟體工程", | ||||
|   "遊戲", | ||||
|   "魷魚", | ||||
| ]; | ||||
| @ -1,11 +1,7 @@ | ||||
| import { configureStore } from "@reduxjs/toolkit"; | ||||
| 
 | ||||
| import tagReducer from "@/lib/home/presenter/tagSlice"; | ||||
| import tagReducer, { TagAction, TagState } from "@/lib/home/presenter/tagReducer"; | ||||
| 
 | ||||
| const tagStore = configureStore({ | ||||
|   reducer: { | ||||
|     tag: tagReducer, | ||||
|   }, | ||||
| export default configureStore<TagState, TagAction>({ | ||||
|   reducer: tagReducer, | ||||
| }); | ||||
| 
 | ||||
| export default tagStore; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user