From 0244708f6ba8a158970ed5543629a29b5be32482 Mon Sep 17 00:00:00 2001 From: Ezhil Shanmugham Date: Mon, 1 Jul 2024 01:12:17 +0530 Subject: [PATCH] feat: added ci and docs --- .github/workflows/lint.yml | 29 + CONTRIBUTING.md | 39 ++ README.md | 75 +- apps/frontend/src/App.tsx | 37 +- apps/frontend/src/components/Card.tsx | 2 +- apps/frontend/src/components/ChessBoard.tsx | 662 +++++++++--------- .../frontend/src/components/ExitGameModel.tsx | 21 +- apps/frontend/src/components/Footer.tsx | 7 +- apps/frontend/src/components/GameEndModal.tsx | 22 +- apps/frontend/src/components/Loader.tsx | 2 +- apps/frontend/src/components/ShareGame.tsx | 96 +-- apps/frontend/src/components/side-nav.tsx | 3 +- apps/frontend/src/components/themes.tsx | 110 ++- apps/frontend/src/components/ui/button.tsx | 53 +- apps/frontend/src/components/ui/card.tsx | 53 +- apps/frontend/src/constants/themes.ts | 60 +- apps/frontend/src/context/themeContext.tsx | 83 +-- apps/frontend/src/hooks/useSidebar.ts | 2 + apps/frontend/src/hooks/useThemes.ts | 14 +- apps/frontend/src/lib/utils.ts | 6 +- apps/frontend/src/screens/Game.tsx | 15 +- apps/frontend/src/screens/Landing.tsx | 73 +- apps/frontend/src/screens/Settings.tsx | 42 +- apps/frontend/src/utils/canvas.ts | 24 +- apps/ws/src/Game.ts | 125 ++-- apps/ws/src/GameManager.ts | 48 +- apps/ws/src/SocketManager.ts | 13 +- packages/store/src/atoms/chessBoard.ts | 20 +- yarn.lock | 148 +++- 29 files changed, 1125 insertions(+), 759 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 CONTRIBUTING.md diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..55a63df7 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,29 @@ +name: Lint check +on: + pull_request: + branches: + - '**' + +jobs: + lint-check: + name: Lint check + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@v4 + + - name: Cache dependencies + uses: actions/cache@v2 + + with: + path: ~/.cache/yarn + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Lint check + run: yarn lint diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..7e37309d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,39 @@ +# Contributing + +Thank you for your interest in contributing to this repository. To ensure a smooth and collaborative environment, please follow these guidelines. Before contributing, set up the project locally using the steps outlined in [README.md](./README.md). + +## Why these guidelines ? + +Our goal is to create a healthy and inclusive space for contributions. Remember that open-source contribution is a collaborative effort, not a competition. + +## General guidelines + +- Work only on one issue at a time since it will provide an opportunity for others to contribute as well. + +- Note that each open-source repository generally has its own guidelines, similar to these. Always read them before starting your contributions. + +## How to get an issue assigned + +- To get an issue assigned, provide a small description as to how you are planning to tackle this issue. + +> Ex - If the issue is about UI changes, you should create a design showing how you want it to look on the UI (make it using figma, paint, etc) + +- This will allow multiple contributors to discuss their approach to tackle the issue. The maintainer will then assign the issue. + +## After getting the issue assigned + +- Create your own branch instead of working directly on the main branch. + +- Provide feedback every 24-48 hours if an issue is assigned to you. Otherwise, it may be reassigned. + +- When submitting a pull request, please provide a screenshot or a screen-recording showcasing your work. + +## Don't while contributing + +- Avoid comments like "Please assign this issue to me" or "can i work on this issue ?" + +- Refrain from tagging the maintainer to assign issues or review pull requests. + +- Don't make any pull request for issues you are not assigned to. It will be closed without merging. + +Happy Contributing! diff --git a/README.md b/README.md index 26288a74..811ba46e 100644 --- a/README.md +++ b/README.md @@ -19,19 +19,64 @@ Let's keep it simple ## Setting it up locally - - Clone the repo - - Copy over .env.example over to .env everywhere - - Update .env - - Postgres DB Credentials - - Github/Google Auth credentials - - npm install - - Start ws server - - cd apps/ws - - npm run dev - - Start Backend - - cd apps/backend - - npm run dev - - Start frontend - - cd apps/frontend - - npm run dev +1. Fork and clone the repo +```bash +git clone https://github.com/your-username/chess.git +``` + +2. Copy over .env.example over to .env everywhere + +```bash +cp apps/backend/.env.example apps/backend/.env && cp apps/frontend/.env.example apps/frontend/.env +``` + +3. Update .env + +- Postgres DB Credentials +- Github/Google Auth credentials + +4. Install dependencies + +```bash +npm install +``` + +5. Start ws server + +```bash +cd apps/ws +npm run dev +``` + +6. Start Backend + +```bash +cd apps/backend +npm run dev +``` + +7. Start frontend + +```bash +cd apps/frontend +npm run dev +``` + +## Contributing + +We welcome contributions from the community! To contribute to chess, follow these steps: + +1. Fork the repository. +2. Create a new branch (`git checkout -b feature/fooBar`). +3. Make your changes and commit them (`git commit -am 'Add some fooBar'`). + > Make sure to lint and format your code before commiting + > + > - `npm run lint` to check for lint errors + > - `npm run format` to fix lint errors +4. Push to the branch (`git push origin feature/fooBar`). +5. Create a new Pull Request. + +For major changes, please open an issue first to discuss what you would like to change. + +Read our [contribution guidelines](./CONTRIBUTING.md) for more details. diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index ddcae763..c2a94c04 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -1,5 +1,5 @@ -import "./App.css"; -import "./themes.css"; +import './App.css'; +import './themes.css'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { Landing } from './screens/Landing'; import { Game } from './screens/Game'; @@ -9,8 +9,8 @@ import { RecoilRoot } from 'recoil'; import { Loader } from './components/Loader'; import { Layout } from './layout'; import { Settings } from './screens/Settings'; -import { Themes } from "./components/themes"; -import { ThemesProvider } from "./context/themeContext"; +import { Themes } from './components/themes'; +import { ThemesProvider } from './context/themeContext'; function App() { return ( @@ -30,21 +30,30 @@ function AuthApp() { return ( - } - /> } + path="/" + element={ + + + + } /> + } /> } + element={ + + + + } /> - } + + + + } > } /> diff --git a/apps/frontend/src/components/Card.tsx b/apps/frontend/src/components/Card.tsx index caeebe03..7343f4f3 100644 --- a/apps/frontend/src/components/Card.tsx +++ b/apps/frontend/src/components/Card.tsx @@ -86,7 +86,7 @@ export function PlayCard() { -

+

Play Chess

chess diff --git a/apps/frontend/src/components/ChessBoard.tsx b/apps/frontend/src/components/ChessBoard.tsx index fa9c6643..4097ac8b 100644 --- a/apps/frontend/src/components/ChessBoard.tsx +++ b/apps/frontend/src/components/ChessBoard.tsx @@ -6,7 +6,6 @@ import LegalMoveIndicator from './chess-board/LegalMoveIndicator'; import ChessSquare from './chess-board/ChessSquare'; import NumberNotation from './chess-board/NumberNotation'; import { drawArrow } from '../utils/canvas'; -import useWindowSize from '../hooks/useWindowSize'; import Confetti from 'react-confetti'; import MoveSound from '/move.wav'; import CaptureSound from '/capture.wav'; @@ -19,6 +18,7 @@ import { userSelectedMoveIndexAtom, } from '@repo/store/chessBoard'; +// eslint-disable-next-line react-refresh/only-export-components export function isPromoting(chess: Chess, from: Square, to: Square) { if (!from) { return false; @@ -44,353 +44,369 @@ export function isPromoting(chess: Chess, from: Square, to: Square) { .includes(to); } -export const ChessBoard = memo(({ - gameId, - started, - myColor, - chess, - board, - socket, - setBoard, -}: { - myColor: Color; - gameId: string; - started: boolean; - chess: Chess; - setBoard: React.Dispatch< - React.SetStateAction< - ({ - square: Square; - type: PieceSymbol; - color: Color; - } | null)[][] - > - >; - board: ({ - square: Square; - type: PieceSymbol; - color: Color; - } | null)[][]; - socket: WebSocket; -}) => { - console.log("chessboard reloaded") +export const ChessBoard = memo( + ({ + gameId, + started, + myColor, + chess, + board, + socket, + setBoard, + }: { + myColor: Color; + gameId: string; + started: boolean; + chess: Chess; + setBoard: React.Dispatch< + React.SetStateAction< + ({ + square: Square; + type: PieceSymbol; + color: Color; + } | null)[][] + > + >; + board: ({ + square: Square; + type: PieceSymbol; + color: Color; + } | null)[][]; + socket: WebSocket; + }) => { + console.log('chessboard reloaded'); - const [isFlipped, setIsFlipped] = useRecoilState(isBoardFlippedAtom); - const [userSelectedMoveIndex, setUserSelectedMoveIndex] = useRecoilState( - userSelectedMoveIndexAtom, - ); - const [moves, setMoves] = useRecoilState(movesAtom); - const [lastMove, setLastMove] = useState<{ from: string; to: string } | null>( - null, - ); - const [rightClickedSquares, setRightClickedSquares] = useState([]); - const [arrowStart, setArrowStart] = useState(null); + const [isFlipped, setIsFlipped] = useRecoilState(isBoardFlippedAtom); + const [userSelectedMoveIndex, setUserSelectedMoveIndex] = useRecoilState( + userSelectedMoveIndexAtom, + ); + const [moves, setMoves] = useRecoilState(movesAtom); + const [lastMove, setLastMove] = useState<{ + from: string; + to: string; + } | null>(null); + const [rightClickedSquares, setRightClickedSquares] = useState( + [], + ); + const [arrowStart, setArrowStart] = useState(null); - const [from, setFrom] = useState(null); - const isMyTurn = myColor === chess.turn(); - const [legalMoves, setLegalMoves] = useState([]); + const [from, setFrom] = useState(null); + const isMyTurn = myColor === chess.turn(); + const [legalMoves, setLegalMoves] = useState([]); - const labels = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; - const [canvas, setCanvas] = useState(null); - const boxSize = 80; - const [gameOver, setGameOver] = useState(false); - const moveAudio = new Audio(MoveSound); - const captureAudio = new Audio(CaptureSound); + const labels = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; + const [canvas, setCanvas] = useState(null); + const boxSize = 80; + const [gameOver, setGameOver] = useState(false); + const moveAudio = new Audio(MoveSound); + const captureAudio = new Audio(CaptureSound); - const handleMouseDown = ( - e: MouseEvent, - squareRep: string, - ) => { - e.preventDefault(); - if (e.button === 2) { - setArrowStart(squareRep); - } - }; - - useEffect(() => { - if (myColor === 'b') { - setIsFlipped(true); - } - }, [myColor]); - - const clearCanvas = () => { - setRightClickedSquares([]); - if (canvas) { - const ctx = canvas.getContext('2d'); - ctx?.clearRect(0, 0, canvas.width, canvas.height); - } - }; + const handleMouseDown = ( + e: MouseEvent, + squareRep: string, + ) => { + e.preventDefault(); + if (e.button === 2) { + setArrowStart(squareRep); + } + }; - const handleRightClick = (squareRep: string) => { - if (rightClickedSquares.includes(squareRep)) { - setRightClickedSquares((prev) => prev.filter((sq) => sq !== squareRep)); - } else { - setRightClickedSquares((prev) => [...prev, squareRep]); - } - }; + useEffect(() => { + if (myColor === 'b') { + setIsFlipped(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [myColor]); - const handleDrawArrow = (squareRep: string) => { - if (arrowStart) { - const stoppedAtSquare = squareRep; + const clearCanvas = () => { + setRightClickedSquares([]); if (canvas) { const ctx = canvas.getContext('2d'); - if (ctx) { - drawArrow({ - ctx, - start: arrowStart, - end: stoppedAtSquare, - isFlipped, - squareSize: boxSize, - }); - } + ctx?.clearRect(0, 0, canvas.width, canvas.height); } - setArrowStart(null); - } - }; + }; - const handleMouseUp = (e: MouseEvent, squareRep: string) => { - e.preventDefault(); - if (!started) { - return; - } - if (e.button === 2) { - if (arrowStart === squareRep) { - handleRightClick(squareRep); + const handleRightClick = (squareRep: string) => { + if (rightClickedSquares.includes(squareRep)) { + setRightClickedSquares((prev) => prev.filter((sq) => sq !== squareRep)); } else { - handleDrawArrow(squareRep); + setRightClickedSquares((prev) => [...prev, squareRep]); } - } else { - clearCanvas(); - } - }; + }; - useEffect(() => { - clearCanvas(); - const lMove = moves.at(-1); - if (lMove) { - setLastMove({ - from: lMove.from, - to: lMove.to, - }); - } else { - setLastMove(null); - } - }, [moves]); + const handleDrawArrow = (squareRep: string) => { + if (arrowStart) { + const stoppedAtSquare = squareRep; + if (canvas) { + const ctx = canvas.getContext('2d'); + if (ctx) { + drawArrow({ + ctx, + start: arrowStart, + end: stoppedAtSquare, + isFlipped, + squareSize: boxSize, + }); + } + } + setArrowStart(null); + } + }; - useEffect(() => { - if (userSelectedMoveIndex !== null) { - const move = moves[userSelectedMoveIndex]; - setLastMove({ - from: move.from, - to: move.to, - }); - chess.load(move.after); - setBoard(chess.board()); - return; - } - }, [userSelectedMoveIndex]); + const handleMouseUp = ( + e: MouseEvent, + squareRep: string, + ) => { + e.preventDefault(); + if (!started) { + return; + } + if (e.button === 2) { + if (arrowStart === squareRep) { + handleRightClick(squareRep); + } else { + handleDrawArrow(squareRep); + } + } else { + clearCanvas(); + } + }; - useEffect(() => { - if (userSelectedMoveIndex !== null) { - chess.reset(); - moves.forEach((move) => { - chess.move({ from: move.from, to: move.to }); - }); - setBoard(chess.board()); - setUserSelectedMoveIndex(null); - } else { - setBoard(chess.board()); - } - }, [moves]); + useEffect(() => { + clearCanvas(); + const lMove = moves.at(-1); + if (lMove) { + setLastMove({ + from: lMove.from, + to: lMove.to, + }); + } else { + setLastMove(null); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [moves]); - return ( - <> - {gameOver && } -
-
- {(isFlipped ? board.slice().reverse() : board).map((row, i) => { - i = isFlipped ? i + 1 : 8 - i; - return ( -
- - {(isFlipped ? row.slice().reverse() : row).map((square, j) => { - j = isFlipped ? 7 - (j % 8) : j % 8; + useEffect(() => { + if (userSelectedMoveIndex !== null) { + const move = moves[userSelectedMoveIndex]; + setLastMove({ + from: move.from, + to: move.to, + }); + chess.load(move.after); + setBoard(chess.board()); + return; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [userSelectedMoveIndex]); - const isMainBoxColor = (i + j) % 2 !== 0; - const isPiece: boolean = !!square; - const squareRepresentation = (String.fromCharCode(97 + j) + - '' + - i) as Square; - const isHighlightedSquare = - from === squareRepresentation || - squareRepresentation === lastMove?.from || - squareRepresentation === lastMove?.to; - const isRightClickedSquare = - rightClickedSquares.includes(squareRepresentation); + useEffect(() => { + if (userSelectedMoveIndex !== null) { + chess.reset(); + moves.forEach((move) => { + chess.move({ from: move.from, to: move.to }); + }); + setBoard(chess.board()); + setUserSelectedMoveIndex(null); + } else { + setBoard(chess.board()); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [moves]); - const piece = square && square.type; - const isKingInCheckSquare = - piece === 'k' && - square?.color === chess.turn() && - chess.inCheck(); + return ( + <> + {gameOver && } +
+
+ {(isFlipped ? board.slice().reverse() : board).map((row, i) => { + i = isFlipped ? i + 1 : 8 - i; + return ( +
+ + {(isFlipped ? row.slice().reverse() : row).map( + (square, j) => { + j = isFlipped ? 7 - (j % 8) : j % 8; - return ( -
{ - if (!started) { - return; - } - if (userSelectedMoveIndex !== null) { - chess.reset(); - moves.forEach((move) => { - chess.move({ from: move.from, to: move.to }); - }); - setBoard(chess.board()); - setUserSelectedMoveIndex(null); - return; - } - if (!from && square?.color !== chess.turn()) return; - if (!isMyTurn) return; - if (from != squareRepresentation) { - setFrom(squareRepresentation); - if (isPiece) { - setLegalMoves( - chess - .moves({ - verbose: true, - square: square?.square, - }) - .map((move) => move.to), - ); - } - } else { - setFrom(null); - } - if (!isPiece) { - setLegalMoves([]); - } + const isMainBoxColor = (i + j) % 2 !== 0; + const isPiece: boolean = !!square; + const squareRepresentation = (String.fromCharCode( + 97 + j, + ) + + '' + + i) as Square; + const isHighlightedSquare = + from === squareRepresentation || + squareRepresentation === lastMove?.from || + squareRepresentation === lastMove?.to; + const isRightClickedSquare = + rightClickedSquares.includes(squareRepresentation); - if (!from) { - setFrom(squareRepresentation); - setLegalMoves( - chess - .moves({ - verbose: true, - square: square?.square, - }) - .map((move) => move.to), - ); - } else { - try { - let moveResult: Move; - if ( - isPromoting(chess, from, squareRepresentation) - ) { - moveResult = chess.move({ - from, - to: squareRepresentation, - promotion: 'q', - }); - } else { - moveResult = chess.move({ - from, - to: squareRepresentation, + const piece = square && square.type; + const isKingInCheckSquare = + piece === 'k' && + square?.color === chess.turn() && + chess.inCheck(); + + return ( +
{ + if (!started) { + return; + } + if (userSelectedMoveIndex !== null) { + chess.reset(); + moves.forEach((move) => { + chess.move({ from: move.from, to: move.to }); }); + setBoard(chess.board()); + setUserSelectedMoveIndex(null); + return; } - if (moveResult) { - moveAudio.play(); - - if (moveResult?.captured) { - captureAudio.play(); + if (!from && square?.color !== chess.turn()) return; + if (!isMyTurn) return; + if (from != squareRepresentation) { + setFrom(squareRepresentation); + if (isPiece) { + setLegalMoves( + chess + .moves({ + verbose: true, + square: square?.square, + }) + .map((move) => move.to), + ); } - setMoves((prev) => [...prev, moveResult]); + } else { setFrom(null); + } + if (!isPiece) { setLegalMoves([]); - if (moveResult.san.includes('#')) { - setGameOver(true); - } - socket.send( - JSON.stringify({ - type: MOVE, - payload: { - gameId, - move: moveResult, - }, - }), + } + + if (!from) { + setFrom(squareRepresentation); + setLegalMoves( + chess + .moves({ + verbose: true, + square: square?.square, + }) + .map((move) => move.to), ); + } else { + try { + let moveResult: Move; + if ( + isPromoting(chess, from, squareRepresentation) + ) { + moveResult = chess.move({ + from, + to: squareRepresentation, + promotion: 'q', + }); + } else { + moveResult = chess.move({ + from, + to: squareRepresentation, + }); + } + if (moveResult) { + moveAudio.play(); + + if (moveResult?.captured) { + captureAudio.play(); + } + setMoves((prev) => [...prev, moveResult]); + setFrom(null); + setLegalMoves([]); + if (moveResult.san.includes('#')) { + setGameOver(true); + } + socket.send( + JSON.stringify({ + type: MOVE, + payload: { + gameId, + move: moveResult, + }, + }), + ); + } + } catch (e) { + console.log('e', e); + } } - } catch (e) { - console.log('e', e); - } - } - }} - style={{ - width: boxSize, - height: boxSize, - }} - key={j} - className={`${isRightClickedSquare ? (isMainBoxColor ? 'bg-[#CF664E]' : 'bg-[#E87764]') : isKingInCheckSquare ? 'bg-[#FF6347]' : isHighlightedSquare ? `${isMainBoxColor ? 'bg-[#BBCB45]' : 'bg-[#F4F687]'}` : isMainBoxColor ? 'bg-boardDark' : 'bg-boardLight'} ${''}`} - onContextMenu={(e) => { - e.preventDefault(); - }} - onMouseDown={(e) => { - handleMouseDown(e, squareRepresentation); - }} - onMouseUp={(e) => { - handleMouseUp(e, squareRepresentation); - }} - > -
- {square && } - {isFlipped - ? i === 8 && ( - - ) - : i === 1 && ( - - )} - {!!from && - legalMoves.includes(squareRepresentation) && ( - - )} -
-
- ); - })} -
- ); - })} -
+ }} + style={{ + width: boxSize, + height: boxSize, + }} + key={j} + className={`${isRightClickedSquare ? (isMainBoxColor ? 'bg-[#CF664E]' : 'bg-[#E87764]') : isKingInCheckSquare ? 'bg-[#FF6347]' : isHighlightedSquare ? `${isMainBoxColor ? 'bg-[#BBCB45]' : 'bg-[#F4F687]'}` : isMainBoxColor ? 'bg-boardDark' : 'bg-boardLight'} ${''}`} + onContextMenu={(e) => { + e.preventDefault(); + }} + onMouseDown={(e) => { + handleMouseDown(e, squareRepresentation); + }} + onMouseUp={(e) => { + handleMouseUp(e, squareRepresentation); + }} + > +
+ {square && } + {isFlipped + ? i === 8 && ( + + ) + : i === 1 && ( + + )} + {!!from && + legalMoves.includes(squareRepresentation) && ( + + )} +
+
+ ); + }, + )} +
+ ); + })} +
- setCanvas(ref)} - width={boxSize * 8} - height={boxSize * 8} - style={{ - position: 'absolute', - top: 0, - left: 0, - pointerEvents: 'none', - }} - onContextMenu={(e) => e.preventDefault()} - onMouseDown={(e) => { - e.preventDefault(); - }} - onMouseUp={(e) => e.preventDefault()} - > -
- - ); -}); + setCanvas(ref)} + width={boxSize * 8} + height={boxSize * 8} + style={{ + position: 'absolute', + top: 0, + left: 0, + pointerEvents: 'none', + }} + onContextMenu={(e) => e.preventDefault()} + onMouseDown={(e) => { + e.preventDefault(); + }} + onMouseUp={(e) => e.preventDefault()} + > +
+ + ); + }, +); diff --git a/apps/frontend/src/components/ExitGameModel.tsx b/apps/frontend/src/components/ExitGameModel.tsx index 2a5d22e6..293848ae 100644 --- a/apps/frontend/src/components/ExitGameModel.tsx +++ b/apps/frontend/src/components/ExitGameModel.tsx @@ -10,23 +10,28 @@ import { AlertDialogDescription, } from './ui/alert-dialog'; -const ExitGameModel = ({ onClick } : {onClick : () => void}) => { - +const ExitGameModel = ({ onClick }: { onClick: () => void }) => { return ( - Exit - + + Exit + + - Are you absolutely sure? - + + Are you absolutely sure? + + This action cannot be undone. This will be considered as a loss. - Continue + + Continue + Exit diff --git a/apps/frontend/src/components/Footer.tsx b/apps/frontend/src/components/Footer.tsx index d7240c21..23d3881e 100644 --- a/apps/frontend/src/components/Footer.tsx +++ b/apps/frontend/src/components/Footer.tsx @@ -10,10 +10,9 @@ export const Footer = () => {