diff --git a/.github/workflows/linting.yaml b/.github/workflows/linting.yaml new file mode 100644 index 00000000..92ac41d5 --- /dev/null +++ b/.github/workflows/linting.yaml @@ -0,0 +1,76 @@ +name: Linting + +on: + push: + paths: + - "frontend/**" + - "server/**" + pull_request: + paths: + - "frontend/**" + - "server/**" + +jobs: + frontend-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Check for Frontend Changes + id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + frontend: + - 'frontend/**' + + - name: Setup Node + if: steps.filter.outputs.frontend == 'true' + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: "npm" + cache-dependency-path: frontend/package-lock.json + + - name: Install Frontend Deps + if: steps.filter.outputs.frontend == 'true' + run: npm ci + working-directory: ./frontend + + - name: Lint Frontend + if: steps.filter.outputs.frontend == 'true' + run: npm run lint + working-directory: ./frontend + + server-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Check for Server Changes + id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + server: + - 'server/**' + + - name: Setup Node + if: steps.filter.outputs.server == 'true' + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: "npm" + cache-dependency-path: server/package-lock.json + + - name: Install Server Deps + if: steps.filter.outputs.server == 'true' + run: npm ci --legacy-peer-deps + working-directory: ./server + + - name: Lint Server + if: steps.filter.outputs.server == 'true' + run: npm run lint + working-directory: ./server diff --git a/.gitignore b/.gitignore index 0317e003..0bcfb980 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ frontend/.pnp frontend/.pnp.js frontend/public/alertSounds/* +#root +node_modules + # testing /coverage diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..d0a77842 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged \ No newline at end of file diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 87e66431..e48ce4b9 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -13,6 +13,16 @@ export default defineConfig([ extends: ["js/recommended"], languageOptions: { globals: globals.browser }, }, + { + rules: { + "@typescript-eslint/no-empty-object-type": [ + "warn", + { + allowInterfaces: "with-single-extends", + }, + ], + }, + }, tseslint.configs.recommended, pluginReact.configs.flat["jsx-runtime"], { diff --git a/frontend/src/components/achievements/CustomAchievementModalInputs.tsx b/frontend/src/components/achievements/CustomAchievementModalInputs.tsx index 0cd405d7..54762fc3 100644 --- a/frontend/src/components/achievements/CustomAchievementModalInputs.tsx +++ b/frontend/src/components/achievements/CustomAchievementModalInputs.tsx @@ -44,7 +44,7 @@ export default function CustomAchievementModalInputs() { setCustom({ ...(achievement.custom || initialCustom), stringValues: e.target.value.split("\n"), - }) + }), ) } value={achievement.custom.stringValues?.join("\n")} @@ -61,7 +61,7 @@ export default function CustomAchievementModalInputs() { setCustom({ ...(achievement.custom || initialCustom), caseSensitive: !achievement.custom?.caseSensitive, - }) + }), ) } > @@ -85,7 +85,7 @@ export default function CustomAchievementModalInputs() { setCustom({ ...(achievement.custom || initialCustom), numberValue: value > 0 ? value : 5, - }) + }), ); }} value={achievement.custom.numberValue || 5} @@ -94,7 +94,6 @@ export default function CustomAchievementModalInputs() { ); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [achievement]); return ( @@ -117,7 +116,7 @@ export default function CustomAchievementModalInputs() { setCustom({ ...achievement.custom, action: e.target.value as CustomAchievementAction, - }) + }), ) } > diff --git a/frontend/src/components/achievements/badges/AvailableBadgeImages.tsx b/frontend/src/components/achievements/badges/AvailableBadgeImages.tsx index 8a6cdd1e..65d5b286 100644 --- a/frontend/src/components/achievements/badges/AvailableBadgeImages.tsx +++ b/frontend/src/components/achievements/badges/AvailableBadgeImages.tsx @@ -96,7 +96,7 @@ function UploadBadgeImageButtons({ const { addNotify } = useNotifications(); const { uploadProgress, handleFileUpload, error, success } = useFileUpload( - uploadBadgesData.badgesImages + uploadBadgesData.badgesImages, ); useEffect(() => { @@ -108,7 +108,6 @@ function UploadBadgeImageButtons({ }); onSuccessCallback(); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [success]); useEffect(() => { diff --git a/frontend/src/components/achievements/badges/EditCreateBadgeModal.tsx b/frontend/src/components/achievements/badges/EditCreateBadgeModal.tsx index 81a13915..d512c8cb 100644 --- a/frontend/src/components/achievements/badges/EditCreateBadgeModal.tsx +++ b/frontend/src/components/achievements/badges/EditCreateBadgeModal.tsx @@ -10,7 +10,7 @@ export default function EditCreateBadgeModal() { const dispatch = useDispatch(); const { addNotify } = useNotifications(); const { isModalOpen, badge, editingId } = useSelector( - (root: RootStore) => root.badges + (root: RootStore) => root.badges, ); const editBadgeMutation = useEditBadge(); const createBadgeMutation = useCreateBadge(); @@ -39,7 +39,8 @@ export default function EditCreateBadgeModal() { title={`${editingId ? "Edit" : "Create"} badge`} onClose={() => dispatch(closeModal())} onSubmit={() => { - editingId ? onSubmitModalEdit() : onSubmitModalCreate(); + if (editingId) onSubmitModalEdit(); + else onSubmitModalCreate(); }} show={isModalOpen} > diff --git a/frontend/src/components/achievements/oneStage/ActionButtons.tsx b/frontend/src/components/achievements/oneStage/ActionButtons.tsx index 98a0d3a0..9a6fe92b 100644 --- a/frontend/src/components/achievements/oneStage/ActionButtons.tsx +++ b/frontend/src/components/achievements/oneStage/ActionButtons.tsx @@ -58,14 +58,14 @@ export default function ActionButtons({ updatedAt: new Date(), }, showTimeMs: 2500, - } - ) + }, + ), ); }; const refetchAchievementStageById = useRefetchAchievementStageById(); const handleEditAchievementStage = () => { - const { createdAt, updatedAt, stageData, ...rest } = stage; + const { stageData, ...rest } = stage; editAchievementStageMutation.mutate({ id: stageId, updatedAchievementStage: { @@ -80,7 +80,7 @@ export default function ActionButtons({ const handleOnClickSave = () => { const isBadgeSet = stage.stageData.every( - (stageData) => stageData.badge._id + (stageData) => stageData.badge._id, ); if (!isBadgeSet) return addNotify({ diff --git a/frontend/src/components/achievements/stages/CreateStage.tsx b/frontend/src/components/achievements/stages/CreateStage.tsx index 5d815ca7..fff196c0 100644 --- a/frontend/src/components/achievements/stages/CreateStage.tsx +++ b/frontend/src/components/achievements/stages/CreateStage.tsx @@ -17,7 +17,7 @@ export default function CreateStages() { }; return ( <> - + { @@ -81,7 +81,7 @@ export default function UserAchievementProgress({ type: NOTIFICATION_TYPE.SUCCESS, }); } - } + }, ); }; const { currentPage, count, totalPages, data } = achievementsData; @@ -159,7 +159,7 @@ function UserAchievementProgressValue({
, - document.getElementById("root")! + document.getElementById("root")!, ); } diff --git a/frontend/src/components/overlay/Overlay.tsx b/frontend/src/components/overlay/Overlay.tsx index 7eaa6568..b259c980 100644 --- a/frontend/src/components/overlay/Overlay.tsx +++ b/frontend/src/components/overlay/Overlay.tsx @@ -70,7 +70,6 @@ export default function Overlay(params: { editor?: boolean }) { useEffect(() => { dispatch(setIsEditor(editor)); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [editor]); useEffect(() => { @@ -81,7 +80,6 @@ export default function Overlay(params: { editor?: boolean }) { return () => { refreshOverlayLayoutEvent.off(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [refreshOverlayLayoutEvent]); useEffect(() => { diff --git a/frontend/src/components/overlay/achievements/Achievements.tsx b/frontend/src/components/overlay/achievements/Achievements.tsx index 262124ad..49ede77e 100644 --- a/frontend/src/components/overlay/achievements/Achievements.tsx +++ b/frontend/src/components/overlay/achievements/Achievements.tsx @@ -54,10 +54,10 @@ export default function Achievements() { useEffect(() => { if (isEditor) setObtainedAchievements(getExampleAchievementsData()); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [isEditor]); //TODO: add progressBar via those. + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { start, end, isActive } = useAchievementQueue( socket, setObtainedAchievements, @@ -77,14 +77,14 @@ export default function Achievements() { }; } }, - } + }, ); useEffect(() => { if (obtainedAchievements.length > MAX_ACHIEVEMENTS_IN_CACHE) { obtainedAchievements.splice( MAX_ACHIEVEMENTS_IN_CACHE, - obtainedAchievements.length + obtainedAchievements.length, ); } }, [obtainedAchievements]); @@ -103,7 +103,7 @@ export default function Achievements() { styles.direction === "column" || styles.direction === "horizontal" ? "column" : "row", - [styles.direction] + [styles.direction], ); return ( diff --git a/frontend/src/components/overlay/achievements/use-achievements-queue.ts b/frontend/src/components/overlay/achievements/use-achievements-queue.ts index 0e592013..f51168e7 100644 --- a/frontend/src/components/overlay/achievements/use-achievements-queue.ts +++ b/frontend/src/components/overlay/achievements/use-achievements-queue.ts @@ -16,7 +16,7 @@ type Options = { getAudioAndDelay: ( data: | ObtainAchievementDataWithCollectedAchievement - | ObtainAchievementDataWithProgressOnly + | ObtainAchievementDataWithProgressOnly, ) => AchievementData; }; @@ -27,13 +27,13 @@ export const useAchievementQueue = ( prev: ( | ObtainAchievementDataWithCollectedAchievement | ObtainAchievementDataWithProgressOnly - )[] + )[], ) => ( | ObtainAchievementDataWithCollectedAchievement | ObtainAchievementDataWithProgressOnly - )[] + )[], ) => void, - options: Options + options: Options, ) => { const [start, setStart] = useState(new Date().getTime()); const [end, setEnd] = useState(new Date().getTime() + 2500); @@ -75,15 +75,13 @@ export const useAchievementQueue = ( isProcessing.current = false; processNext(); }, delay); - - // eslint-disable-next-line react-hooks/exhaustive-deps }, [options]); useEffect(() => { const handler = ( data: | ObtainAchievementDataWithCollectedAchievement - | ObtainAchievementDataWithProgressOnly + | ObtainAchievementDataWithProgressOnly, ) => { queue.current.push(data); diff --git a/frontend/src/components/overlay/chat/Chat.tsx b/frontend/src/components/overlay/chat/Chat.tsx index aeea0b36..cc37fa05 100644 --- a/frontend/src/components/overlay/chat/Chat.tsx +++ b/frontend/src/components/overlay/chat/Chat.tsx @@ -22,7 +22,7 @@ export default function Chat() { const [messagesData, setMessagesData] = useLocalStorage( "chatOverlayMessages", - [] + [], ); useEffect(() => { @@ -37,14 +37,13 @@ export default function Chat() { useEffect(() => { if (isEditor) setMessagesData(getExampleChatData()); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [isEditor]); useEffect(() => { messageServerDelete.on((data) => { setMessagesData((prevMessages) => { const messagesWithoutDeletedMsg = prevMessages.filter( - (val) => val.messageData.id !== data.userstate["target-msg-id"] + (val) => val.messageData.id !== data.userstate["target-msg-id"], ); return messagesWithoutDeletedMsg; @@ -108,7 +107,7 @@ export default function Chat() { }, }} /> - ) + ), )} diff --git a/frontend/src/components/overlay/musicPlayer/MusicPlayer.tsx b/frontend/src/components/overlay/musicPlayer/MusicPlayer.tsx index 1299a288..73f5d725 100644 --- a/frontend/src/components/overlay/musicPlayer/MusicPlayer.tsx +++ b/frontend/src/components/overlay/musicPlayer/MusicPlayer.tsx @@ -35,13 +35,12 @@ export default function MusicPlayer() { if (!isEditor || audioData.id) return; setAudioData(editorTestData); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [audioData.id, isEditor]); const updateProgress = useCallback( (value: number) => { setProgress((value / audioData.duration) * 100); }, - [audioData.duration] + [audioData.duration], ); if (!isPlaying) return null; return ( diff --git a/frontend/src/components/overlay/redemptions/Redemptions.tsx b/frontend/src/components/overlay/redemptions/Redemptions.tsx index 23f9bd9b..ecfaea91 100644 --- a/frontend/src/components/overlay/redemptions/Redemptions.tsx +++ b/frontend/src/components/overlay/redemptions/Redemptions.tsx @@ -29,7 +29,7 @@ export default function Redemptions() { setRedemptionInfo( `${ commonData.nicknames[randomWithMax(commonData.nicknames.length - 1)] - } has redeemed - Reward` + } has redeemed - Reward`, ); } }, [isEditor]); @@ -45,6 +45,7 @@ export default function Redemptions() { const audioCtx = new AudioContext(); //TODO: replace any later. + // eslint-disable-next-line @typescript-eslint/no-explicit-any audioCtx.decodeAudioData(audioBuffer as any, (buffer) => { if (source) { source.stop(); diff --git a/frontend/src/components/pagination/Pagination.tsx b/frontend/src/components/pagination/Pagination.tsx index a7245267..72309896 100644 --- a/frontend/src/components/pagination/Pagination.tsx +++ b/frontend/src/components/pagination/Pagination.tsx @@ -23,7 +23,7 @@ export default function Pagination({ const [pageSizeT, setPageSize] = useLocalStorage( localStorageName || DEFUALT_LOCAL_STORAGE_NAME, - 15 + 15, ); const [currentPageLoc, setCurrentPageLoc] = useState(currentPage); @@ -40,7 +40,6 @@ export default function Pagination({ return prevState; }); // Unnecessary rendering - // eslint-disable-next-line react-hooks/exhaustive-deps }, [pageSizeT]); useEffect(() => { @@ -58,14 +57,13 @@ export default function Pagination({ return prevState; }); // Unnecessary rendering - // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentPageLoc]); const paginationRange = usePagination( totalCount, pageSizeT, siblingCount, - currentPageLoc + currentPageLoc, ); const PageSizeSelect = () => { diff --git a/frontend/src/components/selectWithData/SelectWithData.tsx b/frontend/src/components/selectWithData/SelectWithData.tsx index dbaec4c2..71683ce7 100644 --- a/frontend/src/components/selectWithData/SelectWithData.tsx +++ b/frontend/src/components/selectWithData/SelectWithData.tsx @@ -75,7 +75,7 @@ interface SelectPaginationProps { function SelectPagination({ totalCount, currentPage, - totalPages, + // totalPages, pageSize, onChangePagination, }: SelectPaginationProps) { diff --git a/frontend/src/components/songsList/SongsData.tsx b/frontend/src/components/songsList/SongsData.tsx index f4e818ab..9b81ef4a 100644 --- a/frontend/src/components/songsList/SongsData.tsx +++ b/frontend/src/components/songsList/SongsData.tsx @@ -27,7 +27,7 @@ export default function SongsData({ data }: SongsDataProps) { const handleDeleteSong = (id: string) => { if ( !window.confirm( - `Are you sure you want to delete the song with ID: ${id}?` + `Are you sure you want to delete the song with ID: ${id}?`, ) ) return; @@ -94,7 +94,7 @@ export default function SongsData({ data }: SongsDataProps) { { + onClick={() => { setPrevievedSong(song); }} > diff --git a/frontend/src/components/streamEvents/musicPlayer/AudioFoldersList.tsx b/frontend/src/components/streamEvents/musicPlayer/AudioFoldersList.tsx index 4941d024..58f850c7 100644 --- a/frontend/src/components/streamEvents/musicPlayer/AudioFoldersList.tsx +++ b/frontend/src/components/streamEvents/musicPlayer/AudioFoldersList.tsx @@ -22,7 +22,7 @@ export default function AudioFoldersList() { const handleDeleteMp3File = (data: DeleteMp3FileParams) => { if ( !window.confirm( - `Are you sure you want to delete the mp3 file in folder: ${data.folderName} | named: ${data.fileName}?` + `Are you sure you want to delete the mp3 file in folder: ${data.folderName} | named: ${data.fileName}?`, ) ) return; @@ -40,7 +40,6 @@ export default function AudioFoldersList() { useEffect(() => { if (!folderName) return; refetchFolderFilesAudioData(folderName); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [folderName]); useEffect(() => { diff --git a/frontend/src/components/streamEvents/musicPlayer/local/AudioFoldersList.tsx b/frontend/src/components/streamEvents/musicPlayer/local/AudioFoldersList.tsx index c08f8ddc..8cc853a3 100644 --- a/frontend/src/components/streamEvents/musicPlayer/local/AudioFoldersList.tsx +++ b/frontend/src/components/streamEvents/musicPlayer/local/AudioFoldersList.tsx @@ -23,7 +23,7 @@ export default function AudioFoldersList() { const handleDeleteMp3File = (data: DeleteMp3FileParams) => { if ( !window.confirm( - `Are you sure you want to delete the mp3 file in folder: ${data.folderName} | named: ${data.fileName}?` + `Are you sure you want to delete the mp3 file in folder: ${data.folderName} | named: ${data.fileName}?`, ) ) return; @@ -42,7 +42,6 @@ export default function AudioFoldersList() { if (!folderName) return; refetchFolderFilesAudioData(folderName); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [folderName]); useEffect(() => { diff --git a/frontend/src/components/streamEvents/rewardsWindow/RewardsModal.tsx b/frontend/src/components/streamEvents/rewardsWindow/RewardsModal.tsx index 13c34ae2..c802038b 100644 --- a/frontend/src/components/streamEvents/rewardsWindow/RewardsModal.tsx +++ b/frontend/src/components/streamEvents/rewardsWindow/RewardsModal.tsx @@ -49,7 +49,7 @@ export function RewardsModal() { fileList: fileList, bodySingleFileName: { bodyName: "title", value: title }, }, - "alertSound" + "alertSound", ); setFileList(null); @@ -93,7 +93,7 @@ export function RewardsModal() { fileList: fileList, bodySingleFileName: { bodyName: "title", value: title }, }, - "alertSound" + "alertSound", ); setFileList(null); addNotify({ @@ -113,7 +113,8 @@ export function RewardsModal() { dispatch(setEditingAlertSound("")); }} onSubmit={() => { - editingId ? emitEditAlertSoundReward() : emitCreateAlertSoundReward(); + if (editingId) emitEditAlertSoundReward(); + else emitCreateAlertSoundReward(); }} show={isModalOpen} > diff --git a/frontend/src/components/streamEvents/streamChatters/StreamChatters.tsx b/frontend/src/components/streamEvents/streamChatters/StreamChatters.tsx index ac116a93..93984dfd 100644 --- a/frontend/src/components/streamEvents/streamChatters/StreamChatters.tsx +++ b/frontend/src/components/streamEvents/streamChatters/StreamChatters.tsx @@ -19,10 +19,10 @@ export default function StreamChatters() { setLastChatters((prevState) => { prevState.set(data.user.username, new Date(data.messageData.timestamp)); - let newState = new Map( + const newState = new Map( [...prevState.entries()].sort( - (a, b) => new Date(b[1]).getTime() - new Date(a[1]).getTime() - ) + (a, b) => new Date(b[1]).getTime() - new Date(a[1]).getTime(), + ), ); if (newState.size > LIMIT_LAST_CHATTERS) { diff --git a/frontend/src/components/streamEvents/streamModes/StreamModes.tsx b/frontend/src/components/streamEvents/streamModes/StreamModes.tsx index 1806966f..921d120d 100644 --- a/frontend/src/components/streamEvents/streamModes/StreamModes.tsx +++ b/frontend/src/components/streamEvents/streamModes/StreamModes.tsx @@ -49,9 +49,9 @@ export default function StreamModes() { {/* TODO: REFACTOR UNDER */}