From 1f610d18bd2d71596ad2a0206ea598566918273d Mon Sep 17 00:00:00 2001 From: MagnusHafstad Date: Fri, 13 Sep 2024 10:09:53 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=C3=98kt=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/pages/ticTacToe/Board.tsx | 28 +++++++++++++++++ .../src/components/pages/ticTacToe/Square.tsx | 30 +++++++++++++++++++ frontend/src/pages/ticTacToe/index.tsx | 23 ++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 frontend/src/components/pages/ticTacToe/Board.tsx create mode 100644 frontend/src/components/pages/ticTacToe/Square.tsx create mode 100644 frontend/src/pages/ticTacToe/index.tsx diff --git a/frontend/src/components/pages/ticTacToe/Board.tsx b/frontend/src/components/pages/ticTacToe/Board.tsx new file mode 100644 index 000000000..a1b0e768a --- /dev/null +++ b/frontend/src/components/pages/ticTacToe/Board.tsx @@ -0,0 +1,28 @@ +import { Box, ButtonBase, Stack } from "@mui/material"; +import { useState } from "react"; +import Square from "./Square"; + +type Props = {}; + +export const Board: React.VFC = ({}) => { + return ( + + + + + + + + + + + + + + + + + + ); +}; +export default Board; diff --git a/frontend/src/components/pages/ticTacToe/Square.tsx b/frontend/src/components/pages/ticTacToe/Square.tsx new file mode 100644 index 000000000..01a10357f --- /dev/null +++ b/frontend/src/components/pages/ticTacToe/Square.tsx @@ -0,0 +1,30 @@ +import { Box, ButtonBase } from "@mui/material"; +import { useState } from "react"; + +type Props = {}; + +export const Square: React.VFC = ({}) => { + const [value, setValue] = useState(""); + const [turn, setTurn] = useState("X"); + + function handleClick() { + if (value === "") { + setValue(turn); + if (turn === "X") { + setTurn("O"); + } else { + setTurn("X"); + } + } + } + + return ( + handleClick()}> + + {value} + + + ); +}; + +export default Square; diff --git a/frontend/src/pages/ticTacToe/index.tsx b/frontend/src/pages/ticTacToe/index.tsx new file mode 100644 index 000000000..1cd56a96b --- /dev/null +++ b/frontend/src/pages/ticTacToe/index.tsx @@ -0,0 +1,23 @@ +"use client"; + +import Board from "@/components/pages/ticTacToe/Board"; +import Square from "@/components/pages/ticTacToe/Square"; +import { NextPageWithLayout } from "@/lib/next"; +import { Box, Stack, Typography } from "@mui/material"; +import { useRouter } from "next/router"; + +const TicTacToe: NextPageWithLayout = () => { + const router = useRouter(); + + return ( + + + + Tic Tac Toe + + + + ); +}; + +export default TicTacToe; From 39d941230e7f70d848daa140542f1b2b90a8f964 Mon Sep 17 00:00:00 2001 From: MagnusHafstad Date: Sun, 15 Sep 2024 17:07:28 +0200 Subject: [PATCH 2/3] feat: prep for course --- .../src/components/pages/ticTacToe/Board.tsx | 27 ++++--- .../pages/ticTacToe/MoveHistory.tsx | 22 +++++ .../src/components/pages/ticTacToe/Square.tsx | 25 ++---- frontend/src/pages/ticTacToe/index.tsx | 81 ++++++++++++++++++- 4 files changed, 121 insertions(+), 34 deletions(-) create mode 100644 frontend/src/components/pages/ticTacToe/MoveHistory.tsx diff --git a/frontend/src/components/pages/ticTacToe/Board.tsx b/frontend/src/components/pages/ticTacToe/Board.tsx index a1b0e768a..62af56995 100644 --- a/frontend/src/components/pages/ticTacToe/Board.tsx +++ b/frontend/src/components/pages/ticTacToe/Board.tsx @@ -1,26 +1,29 @@ -import { Box, ButtonBase, Stack } from "@mui/material"; +import { Box, Stack } from "@mui/material"; import { useState } from "react"; import Square from "./Square"; -type Props = {}; +type Props = { + squares: string[]; + handleMove: (i: number) => void; +}; -export const Board: React.VFC = ({}) => { +export const Board: React.VFC = ({ squares, handleMove }) => { return ( - - - + + + - - - + + + - - - + + + ); diff --git a/frontend/src/components/pages/ticTacToe/MoveHistory.tsx b/frontend/src/components/pages/ticTacToe/MoveHistory.tsx new file mode 100644 index 000000000..e8d71143e --- /dev/null +++ b/frontend/src/components/pages/ticTacToe/MoveHistory.tsx @@ -0,0 +1,22 @@ +import { Box, ButtonBase, List, ListItemButton } from "@mui/material"; +import { useState } from "react"; + +type Props = { + history: string[][]; + jumpTo: (step: number) => void; + currentlyViewing: number; +}; + +export const MoveHistory: React.VFC = ({ history, jumpTo, currentlyViewing }) => { + return ( + + {history.map((history, index) => ( + jumpTo(index)} selected={currentlyViewing === index}> + Move nr. {index + 1} + + ))} + + ); +}; + +export default MoveHistory; diff --git a/frontend/src/components/pages/ticTacToe/Square.tsx b/frontend/src/components/pages/ticTacToe/Square.tsx index 01a10357f..0ee697c0a 100644 --- a/frontend/src/components/pages/ticTacToe/Square.tsx +++ b/frontend/src/components/pages/ticTacToe/Square.tsx @@ -1,25 +1,14 @@ import { Box, ButtonBase } from "@mui/material"; -import { useState } from "react"; -type Props = {}; - -export const Square: React.VFC = ({}) => { - const [value, setValue] = useState(""); - const [turn, setTurn] = useState("X"); - - function handleClick() { - if (value === "") { - setValue(turn); - if (turn === "X") { - setTurn("O"); - } else { - setTurn("X"); - } - } - } +type Props = { + index: number; + value: string; + handleClick: (index: number) => void; +}; +export const Square: React.VFC = ({ index, value, handleClick }) => { return ( - handleClick()}> + handleClick(index)}> {value} diff --git a/frontend/src/pages/ticTacToe/index.tsx b/frontend/src/pages/ticTacToe/index.tsx index 1cd56a96b..ba54eeadc 100644 --- a/frontend/src/pages/ticTacToe/index.tsx +++ b/frontend/src/pages/ticTacToe/index.tsx @@ -1,13 +1,72 @@ "use client"; import Board from "@/components/pages/ticTacToe/Board"; -import Square from "@/components/pages/ticTacToe/Square"; +import MoveHistory from "@/components/pages/ticTacToe/MoveHistory"; import { NextPageWithLayout } from "@/lib/next"; import { Box, Stack, Typography } from "@mui/material"; -import { useRouter } from "next/router"; +import { useRef, useState } from "react"; const TicTacToe: NextPageWithLayout = () => { - const router = useRouter(); + const [winner, setWinner] = useState(""); + const [gameOver, setGameOver] = useState(false); + const [squares, setSquares] = useState(Array(9).fill("")); + const [history, setHistory] = useState([squares]); + const [turn, setTurn] = useState("X"); + const currentlyViewing = useRef(history.length - 1); + + function isGameOver(currentSquares: string[]) { + const lines = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + [0, 4, 8], + [2, 4, 6], + ]; + for (const line of lines) { + const [a, b, c] = line; + if (currentSquares[a] && currentSquares[a] === currentSquares[b] && currentSquares[a] === currentSquares[c]) { + setWinner(currentSquares[a]); + setGameOver(true); + } + } + if (currentSquares.every((currentSquares) => currentSquares !== "")) { + setGameOver(true); + } + } + + function handleMove(i: number) { + console.log(currentlyViewing.current); + console.log(history.length - 1); + if (currentlyViewing.current !== history.length - 1) { + jumpTo(history.length - 1); + return; + } + if (gameOver === true) { + return; + } + if (squares[i] === "") { + const newSquares = squares.slice(); + newSquares[i] = turn; + setSquares(newSquares); + if (turn === "X") { + setTurn("O"); + } else { + setTurn("X"); + } + isGameOver(newSquares); + history.push(newSquares); + currentlyViewing.current = history.length - 1; + } + console.log(currentlyViewing.current); + } + + function jumpTo(step: number) { + setSquares(history[step]); + currentlyViewing.current = step; + } return ( @@ -15,9 +74,23 @@ const TicTacToe: NextPageWithLayout = () => { Tic Tac Toe - + + {gameOver ? gameOverMessage() : ""} + + + + + ); + + function gameOverMessage() { + if (gameOver && winner === "") { + return "Draw!"; + } else if (winner !== "") { + return `${winner} wins!`; + } + } }; export default TicTacToe; From c90c9a2d92f8335933e5937ebb14070ef0b06076 Mon Sep 17 00:00:00 2001 From: MagnusHafstad Date: Thu, 24 Oct 2024 15:46:30 +0200 Subject: [PATCH 3/3] docs: comprehencive backend read me --- README.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d7d618a4..e565d65a4 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ the VSCode editor. 4. Type `cd indok-web` to move into the new folder 5. (optional, but extremely recommended) Set up the backend locally, to get linting, auto-complete and environment variables - Follow steps 1-9 under [Without Docker: Backend](#backend) below -6. (optional, but extremely recommended) Set up the frontend locally, to get pre-commit hooks, linting, auto-complete and +6. (optional, but extremely recommended) Set up the frontend locally, to get pre-commit hooks, linting, auto-complete and environment variables - Follow steps 1-8 under [Without Docker: Frontend](#frontend) below 7. Type `docker compose build` to build the project @@ -562,6 +562,76 @@ An outline of how a developer may work with this project: - If an automatic test fails, click `Details` on it to see what went wrong in the logs - Once your Pull Request is approved, and the tests pass, you can merge it - now your changes are live! +### Comprehenceive Api and Backend Models Guide + +#### Making a model (changes to the database) + +The models.py file is essentially a description of what should be stored in our database. Types.py tells graphene, the service we use to autogenerate a ton of stuff what types the database has. + +If you are adding a brand new models.py file make sure to add your changes LOCAL_APPS in the base.py of our django settings. In order to make the migrations work you will also need a migrations folder containing a file named **init**.py. The file init file is empty but nessecary. + +After changes have been made to thease files you need to push them to the database. This is done first by making the migrations using `docker compose exec backend python manage.py makemigrations` After the migrations are made you migrate useing `docker compose exec backend python manage.py migrate`. For the changes to show up in the admin panel you will need to restart docker + +#### Making a query + +A query is a request for data made to the backend by the frontend. The following is a guide on how to make queries. + +##### Queries in Backend + +All queries have a respective resolver function in the backends `resolvers.py` function. This is how the backend knows what to send to the frontend. A resolver function is ALWAYS named `resolver_something`. Note that Python uses camel case and GraphQL does not. This means that in communication the frontend is going to capitalize the letter(s) after the underscore(s) and remove the underscore. For example, the resolver, `resolve_get_winners_list`, will be referenced as `getWinnersList` in the frontend. + +All resolvers must also be referenced in the `schema.py` file. Here they are again named using camel case and given return types, often the type is just the model from `models.py`. + +If you are making the first queries of this backend folder you will also need to add the schema to `backend/config/schema.py`. This is to tell the command `python manage.py graphql_schema` to include the new schema file that it exists. + +After adding the new resolvers you can run `docker compose exec backend python manage.py graphql_schema` in the `indok-web` directory to add the new schema. The query is now done from the backend perspective. NOTE: if you have forgotten to add the resolver to the schema or done so incorrectly the terminal will say it was successful. After this command, there should be a change in `schema.json`. + +##### Queries in Frontend + +In the frontend all query defining is done in the `graphql` folder. The generated folder should not be touched! (However it can be useful to look at in troubleshooting). + +The `graphql` folder usually has 3 files, mutations, queries and fragments. Fragemnts are selections of fields on one of our API types, that can be reused across different queries/mutations to ensure consistent types. You can read more here: (https://www.apollographql.com/docs/react/data/fragments/). If a selection is used only once it can be written in the queries file directy. + +After having written your query run the command `yarn generate` in the frontend directory to generate machine readable queries. + +After the above is done the queries can be used and tested. In code they are used using the apollo function useQuery. Here is an example: `const { error, loading, data } = useQuery(GetWinnersListDocument);`. Notice the three terms before the query. Things can ALWAYS go wrong with queries and they requre data to be sent, therefore it is common to define what is done in rare cases. + +- `data` is the normal data when the query has returned something expected from the backend. +- `loading` is a waiting status. Queries are not instant and it can be useful to define some action while waiting. +- `error` is what you recive if the query returned that something has gone wrong. This should almost always have some sort of notification to the user and or admins. + +An important security notice: Permissions are handled in the backend. It is possible for anyone to try to call queries withut a frontend interface, it is therefore important that all queries that should have a permission check has them before it is pushed to production regardless of if there exists a frontend interface. + +One last note about queries is that we limit size. If you return more than about 300 items the query will fail. Therefore it is important that sorting and things of that kind is done in the backend. + +#### Making a mutation + +A mutation is a request for data to be changed or added made to the backend by the frontend. The following is a guide on how to make mutations. Note that it is very similar to making a query. + +##### Queries in Backend + +All mutations have a respective mutation class in the backends `mutations.py` file. This is how the backend knows what to change when the mutation is called. A mutation class has no strict naming scheme but it is higly reccommended to end the name in "Mutation". Every mutation class has a function named mutate that does the changing, an Arguments class that takes in the data that can be passed from the frontend, and an ok that is returned if the mutation is successful. + +All mutations must also be referenced in the `schema.py` file. Here they are named using camel case for the name of the mutation. + +If you are making the first mutations of this backend folder you will also need to add the schema to `backend/config/schema.py`. This is to tell the command `python manage.py graphql_schema` to include the new schema file that it exists. + +After adding the new resolvers you can run `docker compose exec backend python manage.py graphql_schema` in the `indok-web` directory to add the new schema. The mutation is now done from the backend perspective. NOTE: if you have forgotten to add the resolver to the schema or done so incorrectly the terminal will say it was successful. After this command, there should be a change in `schema.json`. + +An important security notice: Permissions are handled in the backend. It is possible for anyone to try to call mutations withut a frontend interface, it is therefore important that all mutations that should have a permission check has it before it is pushed to production regardless of if there exists a frontend interface. + +##### Mutations in Frontend + +In the frontend all mutation defining is done in the `graphql` folder. The generated folder should not be touched! (However it can be useful to look at in troubleshooting). + +The `graphql` folder usually has 3 files, mutations, queries and fragments. Obvously mutations are written in the mutations folder. + +After having written your mutation run the command `yarn generate` in the frontend directory to generate machine readable mutation. + +After the above is done the queries can be used and tested. In code they are used using the apollo function useUsemutation to define a function that can be run. Here is an example: `const [logTicTacToe] = useMutation(LogTicTacToeDocument);`. After this the `logTicTacToe` function can be used to run the mutation. It lookes like this: `logTicTacToe({ variables: { winner: "Draw" } })`; Note that this does not trigger a react rerender so if you need to change visuals you need to handle that as well. + +An important security notice: Permissions are handled in the backend. It is possible for anyone to try to call queries withut a frontend interface, it is therefore important that all queries that should have a permission check has them before it is pushed to production regardless of if there exists a frontend interface. Most mutations (mabye all the ones we currenly have) need a permission check. + ## Tech Stack - Frontend