diff --git a/source/conf.py b/source/conf.py index 02c0d25..770a6a2 100644 --- a/source/conf.py +++ b/source/conf.py @@ -19,7 +19,7 @@ # -- Project information ----------------------------------------------------- project = 'F-sek' -copyright = '2024, F-sektionen inom TLTH' +copyright = '2026, F-sektionen inom TLTH' author = 'F-sektionen' # The short X.Y version diff --git a/source/examples/backend/backend.rst b/source/examples/backend/backend.rst new file mode 100644 index 0000000..db0959e --- /dev/null +++ b/source/examples/backend/backend.rst @@ -0,0 +1,310 @@ +.. _backend_tutorial: + +Backend Code Tutorial +===================== + +This tutorial will guide you through the basics of our backend code. You will be creating a simple data object and learning how to interact with it using CRUD (Create, Read, Update, Delete) operations. + +Preparation +----------- + +To work through this tutorial, you will need: + +- A local copy of the backend codebase. Install it using the `github README instructions`_. +- Git installed and configured on your machine. Use only the git part of :ref:`this guide `. + +.. _`github README instructions`: https://github.com/fsek/WebWebWeb + +.. _how_the_backend_works: + +How the Backend Works +--------------------- + +When a request comes into the backend (for example, when a user tries to load a page and sends a GET request), it goes through several layers before reaching the database and sending a response back to the user. Below is a simplified overview of the process. You don't need to understand all the details now, but at the end of the tutorial you should be able to see how the different parts fit together. You can return to this section later as you work through the tutorial. + +1. The request hits our server (``uvicorn``) and gets passed to FastAPI's application object in ``main.py``. +2. FastAPI matches the URL (eg. "/fruits") and HTTP verb (eg. "GET") to one of the routers in ``routes/`` (you will add ``fruit_router`` there later in this tutorial). +3. Dependencies run before the route handler (the handler is the function which handles the request. eg. "get_fruit()"). We create a database session (``DB_dependency``) and check permissions (``Permission.require(...)``). +4. The route handler uses SQLAlchemy ORM models in ``db_models/`` to read or change rows in the database inside that session. +5. Pydantic schemas in ``api_schemas/`` validate incoming data and serialize outgoing data so responses have the shapes and types we expect. +6. The handler returns a Python object. Pydantic once again validates this outgoing data against the schemas. +7. FastAPI turns the Python object into JSON, sets the HTTP status code, and sends the response back to the client. +8. If something goes wrong (for example, a fruit is not found), the handler raises ``HTTPException`` so FastAPI can send the right error code to the caller. + +Keep this mental model handy as you work through the steps below - each step in the tutorial plugs into one of these layers. + + +Create a Git Branch +-------------------- + +Git is great for keeping track of changes in code. You should always create a new branch when working on a new feature or bugfix. This keeps your changes organized and makes it easier for others to help you later on. First run this to make sure you are up to date with the latest changes, and branch off the main branch: :: + + git checkout main + git pull origin main + +Now we create the branch of off main. You should run this in the terminal: :: + + git checkout -b COOL-NAME-FOR-YOUR-BRANCH + +Replace ``COOL-NAME-FOR-YOUR-BRANCH`` with a descriptive name for your branch. + +Creating a Data Object +--------------------- + +Great! Now we are ready to start coding. We will be creating a simple data object called "Fruit" with attributes like "name" and "color". The first step is to define the data model. Go to the ``db_models`` directory and create a new file called ``fruit_model.py``. In this file, define the Fruit model using SQLAlchemy ORM (Object-Relational Mapping): :: + + from db_models.base_model import BaseModel_DB + from sqlalchemy import String + from sqlalchemy.orm import mapped_column, Mapped + + class Fruit_DB(BaseModel_DB): + __tablename__ = "fruit_table" + + id: Mapped[int] = mapped_column(primary_key=True, init=False) + + name: Mapped[str] = mapped_column() + + color: Mapped[str] = mapped_column() + + price: Mapped[int] = mapped_column() + + +This code defines a Fruit model with four attributes: id, name, color, and price. The import statements bring in the necessary SQLAlchemy components to define the model. You don't need to understand all the details for now, but I'll try to explain the important parts: + +- BaseModel_DB is the base class for all database models in our codebase. We primarily use it for timestamps and other common functionality. +- __tablename__ specifies the name of the database table that will store Fruit objects. This gets created automatically so you don't need to worry about it. +- Mapped and mapped_column are SQLAlchemy's way of saying “this class attribute is a database column.”. Most of the time, you can look at similar examples in other models to figure out how to define new attributes. +- The id attribute is special. Since names, colors and prices can be the same for different fruits, we need a unique ID which can be used to retrieve a specific fruit. Since id is marked primary_key, it has this function. We also mark it with init=False which tells SQLAlchemy not to expect this value when creating a new fruit object, as it is generated automatically by the database. + +You are very welcome to add another attribute if you want to! This will force you to think through the example code a little bit more and not simply copy/paste all the examples. Something simple like a boolean ``is_moldy`` might be suitable, and won't require you to change the example code too much. + +Now that we've got a basic model, we want to move on to: + +Database Schema +--------------- + +The database schema tells our backend server what types of things it should expect to receive and send out, so that it can perform type checking and tell us if something goes wrong right away. The schema will for example prevent sending a string "thirty one" as the price of the fruit. This part is pretty simple. Go to the ``api_schemas`` directory and add a new file ``fruit_schema.py``: :: + + from api_schemas.base_schema import BaseSchema + + class FruitRead(BaseSchema): + id: int + name: str + color: str + price: int + + + class FruitCreate(BaseSchema): + name: str + color: str + price: int + + + class FruitUpdate(BaseSchema): + name: str | None = None + color: str | None = None + price: int | None = None + +.. hint:: + You might wonder why we define the fields twice (once in ``db_models`` and once here). The **Model** represents the database table, while the **Schema** represents the public API. Keeping them separate allows us to hide internal database fields (like passwords or internal flags) from the public API. + +As you can see, we import the BaseSchema which gives us some nice basics, then we define three schemas for different operations and tell Pydantic (basically the type checker) what fields and types to expect. ``| None = None`` essentially says "If we don't get any value for this field, just pretend it's None.". This allows us to only include the fields we want to change when updating a fruit. Note that this doesn't mean the object in the database will be updated to have None in those fields, any changes to the database happen later in the router code. + +.. note:: + This maps directly to :ref:`How the Backend Works ` steps 5-6 (schema validation and serialization). + +With the database schema done, we should not get any type errors when moving on to the next step: + +Creating a Router +----------------- + +The router defines what people are allowed to do with the fruits in our database. We will only add the CRUD (Create, Read, Update, Delete) operations, but it's possible to get a lot more creative with what the routes do. + +.. note:: + In :ref:`How the Backend Works ` steps 2-4, routers, dependencies, and handlers sit in the middle of the request flow. + +Let's start by creating the router file. This file will contain the four routes we will make for this tutorial. This is easily the most involved and complex part of the tutorial, routes can (and do!) get very long with a lot of complex logic to allow or forbid users from doing certain things with the database objects. This tutorial will try to keep things pretty simple, but remember you can always ask a su-perman if you feel something is especially confusing. + +All our routes are in the ``routes`` directory, create a new file ``fruit_router.py`` in there and add the following imports to that file. Don't worry too much about understanding these. :: + + from fastapi import APIRouter, HTTPException, status + from api_schemas.fruit_schema import FruitCreate, FruitRead, FruitUpdate + from database import DB_dependency + from db_models.fruit_model import Fruit_DB + from user.permission import Permission + + fruit_router = APIRouter() + +``fruit_router`` is now the router which will contain all of our individual routes, which we'll go through one by one now. + +Read +^^^^ + +We tend to start our router files with the Read route(s) for some reason. You should use something like this: :: + + @fruit_router.get("/{fruit_id}", response_model=FruitRead) + def get_fruit(fruit_id: int, db: DB_dependency): + fruit = db.query(Fruit_DB).filter_by(id=fruit_id).one_or_none() + if fruit is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + return fruit + +I'll walk through this line by line: :: + + @fruit_router.get("/{fruit_id}", response_model=FruitRead) + +This line tells FastAPI that this function is a GET route at the URL path /{fruit_id}. The {fruit_id} part is a variable that will be filled in when calling the route. The response_model=FruitRead part tells FastAPI (which tells Pydantic) to use the FruitRead schema to validate and serialize the response data. :: + + def get_fruit(fruit_id: int, db: DB_dependency): + +This line defines the function get_fruit which takes two parameters: fruit_id and db. The passing of db happens automatically and just connects the route to the database. :: + + fruit = db.query(Fruit_DB).filter_by(id=fruit_id).one_or_none() + +Now we query the database for a fruit with the given fruit_id. If no such fruit exists, one_or_none() will return None. :: + + if fruit is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + +If no fruit was found, we raise a 404 Not Found HTTP exception. :: + + return fruit + +If a fruit was found, we return it. FastAPI will automatically serialize it to JSON using the FruitRead schema since we specified that in the first line. + +.. note:: + This is a concrete example of :ref:`How the Backend Works ` steps 5-7. + +Create +^^^^^^ + +We can now read fruit objects, so let's add a route to create new fruits! :: + + @fruit_router.post("/", response_model=FruitRead, dependencies=[Permission.require("manage", "User")]) + def create_fruit(fruit_data: FruitCreate, db: DB_dependency): + fruit = Fruit_DB( + name=fruit_data.name, + color=fruit_data.color, + price=fruit_data.price, + ) + db.add(fruit) + db.commit() + return fruit + +I'll explain this one line by line as well: :: + + @fruit_router.post("/", response_model=FruitRead, dependencies=[Permission.require("manage", "User")]) + +This line tells FastAPI that this function is a POST route at the URL path /. The response_model=FruitRead part tells FastAPI to use the FruitRead schema to validate and serialize the response data. The dependencies part ensures that only users with the "manage" permission for "User" can access this route. + +.. note:: + Usually you really don't want to use "User" here, but adding a new permission target for "Fruit" is difficult for this tutorial so we use "User" for now. + +:: + + def create_fruit(fruit_data: FruitCreate, db: DB_dependency): + +Like before, we define the function create_fruit which takes two parameters: fruit_data and db. The fruit_data parameter is automatically populated by FastAPI from the request body using the FruitCreate schema. :: + + fruit = Fruit_DB( + name=fruit_data.name, + color=fruit_data.color, + price=fruit_data.price, + ) + +Here we create a new Fruit_DB object using the data from fruit_data. :: + + db.add(fruit) + db.commit() + +db.add(fruit) adds the new fruit to the database session, and db.commit() saves the changes to the database. :: + + return fruit + +Finally, we return the newly created fruit. FastAPI will serialize it to JSON using the FruitRead schema. In our frontend, this is rarely used since we usually GET all fruits at once after creating it to make sure we have the latest data. + +Okay, now we can move on to the Update route. This one will go a little faster since you should be getting the hang of it by now. + +Update +^^^^^^ + +Use this code: :: + + @fruit_router.patch("/{fruit_id}", response_model=FruitRead, dependencies=[Permission.require("manage", "User")]) + def update_fruit(fruit_id: int, fruit_data: FruitUpdate, db: DB_dependency): + fruit = db.query(Fruit_DB).filter_by(id=fruit_id).one_or_none() + if fruit is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + # This does not allow one to "unset" values that could be null but aren't currently + for var, value in vars(fruit_data).items(): + if value is not None: + setattr(fruit, var, value) + + db.commit() + return fruit + +As you can see, we still only allow users with the "manage" permission for "User" to access this route. The rest of the code is similar to what we've seen before. The only new part is the loop that updates the fruit's attributes based on the data provided in fruit_data. If a value is None, we skip updating that attribute. ``setattr(fruit, var, value)`` is a built-in Python function that sets the attribute named var of the fruit object to the given value. + + +Delete +^^^^^^ + +You have all the knowledge needed to understand this last route. Here it is: :: + + @fruit_router.delete("/{fruit_id}", response_model=FruitRead, dependencies=[Permission.require("manage", "User")]) + def delete_fruit(fruit_id: int, db: DB_dependency): + fruit = db.query(Fruit_DB).filter_by(id=fruit_id).one_or_none() + if fruit is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + db.delete(fruit) + db.commit() + return fruit + +As in the earlier routes, the response path matches :ref:`How the Backend Works ` steps 5-7. + + +Add the Router to the Application +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The main program needs to know about your new router in order for it to work. Go to routes/__init__.py and add the following import at the top. :: + + from routes.fruit_router import fruit_router + +Then, find the section where other routers are included and add this line: :: + + app.include_router(fruit_router, prefix="/fruits", tags=["Fruits"]) + +.. note:: + This is the wiring described in :ref:`How the Backend Works ` step 2 (router registration). + +Great! You have now created all four CRUD routes for the Fruit model. This is a good time to take a step back and review what you've done. Next up is testing your new routes to make sure they work as expected. + + +Testing the Routes +------------------ + +You should always test your routes to make sure they work as you expect them to. We really encourage you to write automated tests for your routes (ask a su-perman if you need help with that), but the easiest way to test them quickly is to use the built-in Swagger UI that comes with FastAPI. Start the backend with the command ``uvicorn main:app --reload`` and open your web browser to ``http://localhost:8000/docs``. + +You should see the Swagger UI with a list of all available routes. You will have to log in first using the "Authorize" button in the top right corner to test the routes that require permissions. Ask a su-perman for the account details. + +.. tip:: + Testing can really help during development. Test your routes manually as you create them, and when you create pull requests always include automated tests to ensure your code works as expected and to prevent future changes from breaking it. (You can ask a su-perman or bot for help with writing tests!) + +Now that you're logged in, find your fruit routes in the list and test them out one by one. Make sure to test all four CRUD operations to ensure everything works as expected. + +If everything works, congratulations! You've successfully created a new data object with CRUD operations in our backend codebase. If you encounter any issues, don't hesitate to ask a su-perman for help. + + +Next Steps +---------- + +This is a really simple example meant to get you started. When you add new objects in the real codebase, it often helps to start from an existing similar object and modify it to fit your needs. You will also often need to add more complex logic to the routes, for example to handle relationships between different objects or to enforce more specific permission checks. If you want to continue this example, you could try adding: + +- Actually using "Fruit" as a permission target instead of "User". +- Writing automated tests for the fruit routes to ensure they work as expected. The more complex the routes get, the more important this becomes. +- Adding relationships to other models, for example a "Basket" model that can contain multiple fruits. +- Adding an is_moldy attribute which defaults to False and halves the price if toggled to True and doubles it if untoggled. + + + diff --git a/source/examples/examples.rst b/source/examples/examples.rst index 4b07119..18235c6 100644 --- a/source/examples/examples.rst +++ b/source/examples/examples.rst @@ -10,3 +10,5 @@ provide some understanding of underlying code mechanics. :Caption: Contents ./app/app + ./backend/backend + ./frontend/frontend diff --git a/source/examples/frontend/frontend.rst b/source/examples/frontend/frontend.rst new file mode 100644 index 0000000..eb6fdf9 --- /dev/null +++ b/source/examples/frontend/frontend.rst @@ -0,0 +1,460 @@ +Frontend Code Tutorial +====================== + +In this tutorial, we will create an admin page for managing fruits using our backend API. Frontend code can be very verbose, so don't worry if you don't understand every line. The goal here is to give you a basic idea of how to set up a frontend page that interacts a backend object very similar to the one we created in :ref:`the backend tutorial `. You don't have to have done that tutorial to follow along here, all the code will be provided. + +.. note:: + This tutorial will focus on viewing and adding fruits only. While full CRUD (Create, Read, Update, Delete) operations are typically implemented in production applications, we'll keep this tutorial simpler by implementing only the viewing and adding functionality. If you want to add editing and deleting features later, you can look at other admin pages in the codebase for examples. + + +.. warning:: + The code from this tutorial is intentionally oversimplified. Don't use it as a basis for new features in production. + +Preparation +----------- + +To work through this tutorial, you will need: + +- A local copy of the frontend codebase. Install it using the `github README instructions`_. +- A local copy of the backend codebase. Install it using the `github README instructions for backend`_. +- Git installed and configured on your machine. Use only the git part of :ref:`this guide `. + +.. _`github README instructions`: https://github.com/fsek/WWW-Web +.. _`github README instructions for backend`: https://github.com/fsek/WebWebWeb + +How the Frontend Works +---------------------- + +The frontend is built using Next.js, a popular React framework for building web applications. It uses TypeScript, a typed superset of JavaScript that adds static types to the language. The frontend communicates with the backend and serves as the user interface for interacting with the backend API. Both the frontend and backend are processes that run on the server, but the frontend also sends code to the user's browser to be executed there. When a user clicks on a "save" button, their local client version of the frontend code sends a request directly to the backend API running on the main server to save the data. When they click on a link to view a page, the frontend code running on the server generates the HTML (it's a bit more complicated than that, but that's the basic idea) and sends it to the user's browser to be displayed. When you are building the frontend, you are therefore working both with code that runs on the server and code that runs on the client; if you're unsure which side a piece of code runs on, assume it must be safe for both. + +Create a Git Branch +------------------- + +Git is great for keeping track of changes in code. You should always create a new branch when working on a new feature or bugfix. This keeps your changes organized and makes it easier for others to help you later on. First run this to make sure you are up to date with the latest changes, and branch off the main branch: :: + + git checkout main + git pull origin main + +Now we want to create the new branch. You should run this in the terminal: :: + + git checkout -b COOL-NAME-FOR-YOUR-BRANCH + +Replace ``COOL-NAME-FOR-YOUR-BRANCH`` with a descriptive name for your branch. If you already have local changes, commit or stash them before switching branches to avoid conflicts. + +Starting the Backend +-------------------- + +A backend branch containing all the necessary changes to support the fruit admin page has already been created for you. You want to switch to that branch in your local backend repository. Run these commands in the terminal: :: + + git checkout fruits-example-2026 + git pull origin fruits-example-2026 + +Now rebuild the backend (``Crtl+Shift+P`` in VSCode and select ``Dev Containers: Rebuild Container``) so that the new changes are applied. After rebuilding, start the backend server. You should check that it is running by opening ``http://localhost:8000/docs`` in your web browser. You should see the API documentation page and be able to see the ``/fruits/`` endpoint. This is what you'll be interacting with from the frontend. + +For the frontend to know about these changes, you have to regenerate the API specification in the frontend codebase. Go to the frontend repository and run this command in the terminal: :: + + bun run generate-api + +This should automatically create new files in ``src/api/`` which the frontend will use to know how it can interact with the backend API. + +.. warning:: + If you forget to run ``bun run generate-api`` after pulling backend changes, the generated API client may be outdated and your queries/mutations will fail with confusing errors. + +Creating the Fruit Admin Page +----------------------------- + +After this tutorial, I recommend copying and modifying code from existing pages to create new pages, as this is often faster than writing everything from scratch. However, for this tutorial, I'll go through the file we want step by step so you can understand how it works. + +Our page has to be located in the right place so that next.js can serve it correctly. Create a new folder called ``fruits`` at ``src/app/admin/``. Inside that folder, create a new file called ``page.tsx``. This file will contain the main code for our fruit admin page. + +Open the file and add the following code to the top: :: + + "use client"; + + import { ActionEnum, TargetEnum, type FruitRead } from "@/api"; + import { useSuspenseQuery } from "@tanstack/react-query"; + import { getAllFruitsOptions } from "@/api/@tanstack/react-query.gen"; + import { createColumnHelper, type Row } from "@tanstack/react-table"; + import AdminTable from "@/widgets/AdminTable"; + import useCreateTable from "@/widgets/useCreateTable"; + import { useTranslation } from "react-i18next"; + import { useState, Suspense } from "react"; + import PermissionWall from "@/components/PermissionWall"; + import { LoadingErrorCard } from "@/components/LoadingErrorCard"; + +This code imports all the necessary modules and components we will use in our page. The ``"use client";`` directive at the top tells Next.js that this file should run on the client side (i.e., in the user's browser), which is standard (and necessary) for interactive pages. + +This is a good time to give a brief overview of how the page will look when it's done. The page will display a table of fruits, allowing users to view and add fruit entries. Each fruit will have properties like name, color, and price. There will be a button above the table to add new fruits. + +The table needs a helper which keeps track of the columns and makes it easier to define them. Add this code below the imports: :: + + const columnHelper = createColumnHelper(); + +As you can see, we are using the ``FruitRead`` type that was generated when we ran ``bun run generate-api`` earlier. This type represents the data structure of a fruit as returned by the backend API. + +The next thing we do is to define the columns of the table. Because these have multi language support, we need to do this inside the main component function so that we can use the translation hook. Add this code below the previous code: :: + + export default function Fruits() { + const { t } = useTranslation("admin"); + const columns = [ + columnHelper.accessor("id", { + header: t("fruits.id"), + cell: (info) => info.getValue(), + }), + columnHelper.accessor("name", { + header: t("fruits.name"), + cell: (info) => info.getValue(), + }), + columnHelper.accessor("color", { + header: t("fruits.color"), + cell: (info) => info.getValue(), + }), + columnHelper.accessor("price", { + header: t("fruits.price"), + cell: (info) => info.getValue(), + }), + ]; + } + +``const { t } = useTranslation("admin");`` initializes the translation hook for the "admin" namespace, allowing us to use translated strings in our table headers. Each column is defined using ``columnHelper.accessor``, specifying the property of the ``FruitRead`` object to display, along with the header and cell rendering logic. + +.. tip:: + Always add translations to the components and pages you create. LibU will be really sad if we end up with a frontend that only works in Swedish. + +To display fruits, we need to fetch them from the backend API. We will use the ``useSuspenseQuery`` hook to do this. Add the following below the columns definitions, inside the ``Fruits`` function: :: + + const { data, error } = useSuspenseQuery({ + ...getAllFruitsOptions(), + }); + +This fetches all the fruits from the backend API and puts it in the ``data`` variable. If there is an error during fetching, it will be stored in the ``error`` variable. + +.. note:: + A hook in React is a special function that lets you "hook into" React features from function components. They allow for things like state management (remembering values between renders) and side effects (performing actions like data fetching when the component renders or updates). + +We shall now define our table using a custom hook called ``useCreateTable``. Add this code below the previous code: :: + + const table = useCreateTable({ data: data ?? [], columns }); + +This creates a table instance using the fetched data and the defined columns. The ``data ?? []`` syntax ensures that if ``data`` is undefined (e.g., while loading), an empty array is used instead to avoid errors. + +Great! Now we can render the actual page. Add this at the bottom of the ``Fruits`` function: :: + + return ( + }> +
+

+ {t("admin:fruits.page_title")} +

+ +

{t("admin:fruits.page_description")}

+ + +
+
+ ); + +This code renders the page content. We use a ``Suspense`` component to handle loading states, it will show ``LoadingErrorCard`` while data is being fetched. Inside, we have a header (h3) and a paragraph (p) that use translated strings. Finally, we render the ``AdminTable`` component, passing in our table instance to display the fruit data. + +.. note:: + **What's this ``className`` stuff?** We are using a CSS framework called **Tailwind CSS** to style our components. The ``className`` attributes contain utility classes that apply specific styles, such as padding, font size, and colors. For example, ``px-8`` adds horizontal padding, ``text-3xl`` sets the text size to 3 times extra large, and ``text-primary`` applies the primary color defined in our theme. This is much easier than writing custom CSS for every component. + +You should now be able to see the fruit admin page by navigating to ``http://localhost:3000/admin/fruits`` in your web browser, after having started the frontend server with the commands: :: + + bun install + bun run generate-api + bun run dev + +Since we haven't added any fruits yet, the page will show an empty table. The title and description should be visible, but will only show placeholder text since we have not added the translation keys we referenced yet. + + +Adding Fruits +------------- + + +Let's get started with adding the functionality to add new fruits. We will add a button above the table that opens a form for adding a new fruit. Create a new file in the same folder as ``page.tsx``, called ``FruitForm.tsx``. This file will contain the code for the form component. + +Imports +^^^^^^^ + +Start by adding the imports to the top of the file: :: + + import { useState, useEffect } from "react"; + import { Button } from "@/components/ui/button"; + import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + } from "@/components/ui/dialog"; + import { useForm } from "react-hook-form"; + import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + } from "@/components/ui/form"; + import { Input } from "@/components/ui/input"; + import { zodResolver } from "@hookform/resolvers/zod"; + import { z } from "zod"; + import { useMutation, useQueryClient } from "@tanstack/react-query"; + import { + createFruitMutation, + getAllFruitsQueryKey, + } from "@/api/@tanstack/react-query.gen"; + import { Plus } from "lucide-react"; + import { useTranslation } from "react-i18next"; + +There's not much to add here yet. Note the imports of ``zod``, which we will use for form validation (checking that the user input is correct) and ``createFruitMutation``, which we will use to send the new fruit data to the backend API. + +Zod Schema +^^^^^^^^^^ + +Zod needs a schema to tell it how to validate the form data. Add this code below the imports: :: + + const fruitSchema = z.object({ + name: z.string().min(1), + color: z.string().min(1), + price: z.number().min(0), + }); + +Here we forbid empty names and colors, and we make sure the price is a non-negative number. + +.. note:: + When using schema validation in the frontend, make sure it matches the validation rules in the backend. The backend API can be interacted with without using the website (e.g. using special tools like Postman), so the frontend should not be a layer of "security" but rather a way to improve user experience by catching errors early. + +Component Logic +^^^^^^^^^^^^^^^ + +Now we define the component itself. We need to manage the state of the dialog (open/closed) and the form submission status. We also initialize the form hook using the schema we just created. Add this code below the schema: :: + + export default function FruitForm() { + const [open, setOpen] = useState(false); + const [submitEnabled, setSubmitEnabled] = useState(true); + const fruitForm = useForm>({ + resolver: zodResolver(fruitSchema), + defaultValues: { + name: "", + color: "", + price: 0, + }, + }); + const { t } = useTranslation("admin"); + const queryClient = useQueryClient(); + +The ``useState(false)`` call creates a state variable called ``open`` initialized to ``false``, along with a function ``setOpen`` that we can use to update it. This controls whether the popup dialog is visible or not. We do the same for ``submitEnabled``, which tracks whether the submit button should be clickable. The ``useForm`` hook initializes the form logic. The ``resolver: zodResolver(fruitSchema)`` part is really important because it connects the Zod validation rules we wrote earlier to the form, so the form knows when data is invalid and can show appropriate error messages. We're gonna use queryClient later to refresh the fruit list after adding a new fruit. + +Handling Data Submission +^^^^^^^^^^^^^^^^^^^^^^^^ + +To send data to the backend, we use a mutation. We also need a function to handle the form submission event. Add this inside the component: :: + + const createFruit = useMutation({ + ...createFruitMutation(), + throwOnError: false, + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: getAllFruitsQueryKey() }); + setOpen(false); + setSubmitEnabled(true); + }, + onError: (error) => { + setSubmitEnabled(true); + }, + }); + +The ``useMutation`` hook comes from React Query. While ``useQuery`` is for fetching data from the backend, ``useMutation`` is specifically for changing data. As you can see, we have defined ``onSuccess`` and ``onError`` handlers. If the mutation is successful, we invalidate the fruit list query so that it gets refetched with the new data, close the dialog, and re-enable the submit button. If there is an error, we just re-enable the submit button so the user can try again. After this, add the ``onSubmit`` function: :: + + function onSubmit(values: z.infer) { + setSubmitEnabled(false); + createFruit.mutate({ + body: { + name: values.name, + color: values.color, + price: values.price, + }, + }); + } + +The ``onSubmit`` function is special because it's only called by the form library if all validation passes. As soon as the function runs, we immediately disable the submit button by calling ``setSubmitEnabled(false)``. This prevents the user from clicking the button multiple times while the request is being processed, which could otherwise create duplicate fruits. Then we can call the mutation we defined earlier with the proper data. + +Resetting the Form +^^^^^^^^^^^^^^^^^^ + +When the user opens the dialog, we want to make sure the form is empty. We can use the ``useEffect`` hook to reset the form whenever the ``open`` state changes to true. Add this below the ``onSubmit`` function: :: + + useEffect(() => { + if (open) { + fruitForm.reset({ + name: "", + color: "", + price: 0, + }); + } + }, [open, fruitForm]); + +The ``useEffect`` hook is designed to run code in response to changes. The array at the end, ``[open, fruitForm]``, is called the dependency array. React will run the code inside the effect whenever any of these variables change. In this case, whenever the dialog opens (when ``open`` becomes true), we reset all the form fields to their default empty values. This helps clear the data from the previous fruit submission. + +Rendering the Dialog Structure +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Finally, we need to render the UI. We'll start with the button that opens the dialog and the basic structure of the form dialog itself. The ``Form`` component acts as a context provider for ``react-hook-form``, passing down all the necessary methods to the input fields we will add later. Don't worry about understanding every line of this code, most of it is just boilerplate. + +Add this return statement at the end of the component: :: + + return ( +
+ + + + + {t("fruits.create_fruit")} + +
+
+ + {/* Form fields will go here */} +
+ +
+
+
+ ); + } + +The ``Form`` component will contain all our form fields (like name, color and price) and handle most of the form logic for us so we don't need to worry about it. The ``Dialog`` component is controlled by the ``open`` state we defined earlier, so when we call ``setOpen(true)`` in the button's click handler, the dialog appears. Just like before, there is a lot of CSS styling via ``className`` attributes to make the dialog look nicer, you don't have to understand them. + +.. note:: + The component we are writing essentially comes in two parts: the button that opens the dialog, and the dialog itself. You can see the button component at the top of the return statement above, with the dialog just below it. The dialog contains the form structure, which we will complete next. + +Adding Input Fields +^^^^^^^^^^^^^^^^^^^ + +Now we need to add the actual input fields inside the ``
`` tag. We use the ``FormField`` component to connect our inputs to the form state. + +The ``FormField`` component takes a ``control`` prop (from our ``fruitForm`` hook) and a ``name`` prop (which must match a key in our Zod schema). The ``render`` prop is where the magic happens: it gives us a ``field`` object containing props like ``onChange``, ``onBlur``, and ``value``, which we spread onto our ``Input`` component. This automatically wires up validation and state management. + +Replace the comment ``{/* Form fields will go here */}`` with the following code: :: + + ( + + {t("fruits.name")} + + + + + )} + /> + ( + + {t("fruits.color")} + + + + + )} + /> + ( + + {t("fruits.price")} + + + field.onChange(Number.parseFloat(e.target.value)) + } + /> + + + )} + /> +
+ +
+ +The ``render={({ field }) => ...}`` pattern might look a bit complex at first. It's what's called a "render prop" in React. Essentially, it's a function that returns JSX which can later be shown. The form library calls this function and passes it the ``field`` object, which contains everything the input needs to work correctly with the form. The syntax ``{...field}`` is JavaScript spread syntax, which is a shortcut for taking all properties inside ``field`` (like ``onChange``, ``value``, ``onBlur``) and adding them as props to the ```` component. Without this shortcut, we would have to write ``onChange={field.onChange} value={field.value} onBlur={field.onBlur}`` and so on, which gets repetitive quickly. Pay special attention to the ``price`` field's ``onChange`` handler. HTML inputs with ``type="number"`` actually return strings (like "10.5") rather than actual numbers, even though they look like numbers. Since our Zod schema expects a real number, we need to override the default ``onChange`` behavior to parse the string into a float using ``Number.parseFloat`` before saving it to the form state. + + +Phew! Our form component is now complete. Before we can use it, we need to actually add it to our fruit admin page. + + +Using the Form Component +^^^^^^^^^^^^^^^^^^^^^^^^ + +Go back to the ``page.tsx`` file we created earlier. We need to import the ``FruitForm`` component and add it above the table. Add this import at the top with the other imports: :: + + import FruitForm from "./FruitForm"; + +Now, add the ```` component just above the ```` component in the return statement: :: + + + + +That's it! You should now be able to open the fruit admin page in your web browser, click the "Create Fruit" button, fill out the form, and submit it. The new fruit should appear in the table after submission. If you get any errors or something doesn't work, just ask a su-perman and they will try to help you. + + +Next Steps +---------- + +Congratulations on completing the fruit admin page tutorial! You've learned how to create a new admin page, fetch data from the backend, display it in a table, and add a form for creating new entries. This is a solid foundation for building more complex admin pages in the future. As mentioned at the start, when you actually get to building new features, it's often faster to copy and modify existing code rather than writing everything from scratch. + +For now, there are some things you can optionally add to improve the page: + +- Add translations for all the translation keys we used in the page and form components. You can find the translation files in ``src/locales/en/admin.json`` and ``src/locales/sv/admin.json``. Add something like this to the bottom of both files: +.. + +:: + + "fruits": { + "id": "ID", + "name": "Name", + "color": "Color", + "price": "Price", + "page_title": "Fruit Management", + "page_description": "Manage fruits in the system", + "create_fruit": "Create Fruit", + "save": "Save" + } + +- Implement editing and deleting fruits. You can look at other admin pages in the codebase for examples of how to do this. Essentially, you will need to find the right API mutations and add support for clicking the table to edit or delete entries. +- Style the page further using Tailwind CSS to make it look nicer. +- Add error handling to show messages if something goes wrong during data fetching or submission. We tend to use toast notifications for this. Again, you can look at other admin pages for examples. diff --git a/source/installing_systems/installation_app.rst b/source/installing_systems/installation_app.rst index f05f3e3..f06356a 100644 --- a/source/installing_systems/installation_app.rst +++ b/source/installing_systems/installation_app.rst @@ -9,10 +9,10 @@ Installing Flutter The F-sektionen app is made with Flutter and Dart, so to get up and running we need to install the Flutter. Follow the links below and install the software if you haven't done so already. - - `Flutter SDK download `_ - Install the correct version (3.19.6) and the right distro for your OS. + - `Flutter SDK download `_ - Download the correct version (3.35.4) for the correct OS. - `Further installation instructions `_ - Select your OS, then select Android. Don't install android studio just yet, we'll do that in the next step. -Do all the steps to install Flutter. +Follow all the steps to install Flutter in the guide above. Try to run: :: diff --git a/source/installing_systems/installation_web/installation.rst b/source/installing_systems/installation_web/installation.rst index 23c2798..a566db8 100644 --- a/source/installing_systems/installation_web/installation.rst +++ b/source/installing_systems/installation_web/installation.rst @@ -1,5 +1,5 @@ - +.. _install_git: ============== Setting up Git ============== diff --git a/source/installing_systems/installation_web/installation_web.rst b/source/installing_systems/installation_web/installation_web.rst index 734f7f3..ae75d76 100644 --- a/source/installing_systems/installation_web/installation_web.rst +++ b/source/installing_systems/installation_web/installation_web.rst @@ -2,7 +2,7 @@ Web Installation Guide ====================== If you are using Windows as your operating system, head over to the :ref:`operating-systems` -page before installing the web. Otherwise, assuming you now have a running Unix based OS in some way or another, +page before installing the web. Otherwise, assuming you now have a way to run Docker (for example with WSL, or by using Linux or MacOS) some way or another, you can continue on and start following the installation guides below to get set up and running. Hopefully everything will go smoothly, but sometimes there are some complications since all of our systems are different. They are fixable though, diff --git a/source/installing_systems/operating_systems.rst b/source/installing_systems/operating_systems.rst index 1ee6908..a1a6f5b 100644 --- a/source/installing_systems/operating_systems.rst +++ b/source/installing_systems/operating_systems.rst @@ -23,21 +23,22 @@ Which OS should I use? - **If you are already using Linux:** Great, you came prepared! - **If you are using macOS:** This works well too! -- **If you are using Windows:** Won't work, you have a couple of choices, ordered below by level of recommendation. +- **If you are using Windows:** Won't work out of the box so you have a couple of choices, ordered below by level of recommendation. - `Windows Subsystems for Linux (WSL)`_. This method essentially installs Linux on Windows. Weird, yes but it works surprisingly well. - - `Dual booting/changing entirely to Linux`_. If you want the most compatability and learn Linux, this is the way to go. It can, however, - be a bit of a hassle and take some time. I have currently dual-booted and have no regrets. + - `Dual booting/changing entirely to Linux`_. If you want the most compatibility and learn Linux, this is the way to go. If you don't, WSL. There are guides for this online, but you won't have the time to do this during our meetings. - - `Using a Virtual Machine (VM)`_. If you have a really good laptop with a good graphics card, this can work fine. If not, this method - will likely only be a waste of time since the OS will be slow and sluggish. ================================== Windows Subsystems for Linux (WSL) ================================== -This might be the easisest and fastest way to get a Linux environment up and running. To install a WSL, simply head to the Windows Store and search +.. warning:: + + This might no longer be working. Look at the `README of the frontend repo `_ for what should be the most up to date installation instructions for WSL2. + +This might be the easiest and fastest way to get a Linux environment up and running. To install a WSL, simply head to the Windows Store and search for Ubuntu (there are many different Linux versions or distributions but Ubuntu is the most widely used). Simply download and install the app and then you need to run a command in PowerShell. Open PowerShell as an administrator (right click and select *Run as administrator*) and run the following command:: @@ -51,20 +52,3 @@ if you want to run several routines at once. To access files created in the Ubuntu environment, use Visual Code. Download it from ``_, install it and install the extension *Remote - WSL*. You can now open files and folders in Ubuntu with Visual Code. - - -======================================= -Dual booting/changing entirely to Linux -======================================= - -Since there are many great tutorials covering this topic, it will not be described in detail here. Just remember to create a backup of your Windows installation -before dual booting in case something goes wrong! Also, **Ubuntu** is the most widely used Linux distribution so if you don't know what to choose, -that is a good start. - -============================ -Using a Virtual Machine (VM) -============================ - -Creating a VM is also covered in great detail in several tutorials online. An open source VM which works okay is VirtualBox. However, there might be some performance -issues which could be solved after some tinkering with settings. Another option is VMWare which is not free but there might be some keys available on the web. -VMWare has generally worked better for me but in the end, WSL is recommended over a VM since you generally only need a terminal and not an entire GUI. diff --git a/source/installing_systems/text_editor.rst b/source/installing_systems/text_editor.rst index 53b868a..f2eeb96 100644 --- a/source/installing_systems/text_editor.rst +++ b/source/installing_systems/text_editor.rst @@ -1,8 +1,8 @@ Installing a text editor ======================== -To actually start coding you also need a text editor, much like when you use Eclipse for Java in the programming course (or the old course atleast). -For this project, I would recommend using Visual Studio Code. Visual Studio Code is a solid editor that is very straightforward, easy to set up and is what I use personally. +To actually start coding you also need a text editor. +For our projects, I would recommend using Visual Studio Code. Visual Studio Code is a solid editor that is very straightforward, easy to set up and is what most of the su-permen use for all their coding work. If you have another editor that you prefer, that's perfectly fine too. Whatever editor you end up using just make sure to use our standard (will work **AUTOMATICALLY** if you use VSCode) To install VScode visit: `VScode download `_ diff --git "a/source/pictures/fixa-snabbl\303\244nk.png" "b/source/pictures/fixa-snabbl\303\244nk.png" deleted file mode 100644 index 96fa1db..0000000 Binary files "a/source/pictures/fixa-snabbl\303\244nk.png" and /dev/null differ diff --git a/source/pictures/hitta-mailalias.png b/source/pictures/hitta-mailalias.png deleted file mode 100644 index 894c5fe..0000000 Binary files a/source/pictures/hitta-mailalias.png and /dev/null differ diff --git "a/source/pictures/hitta-snabbl\303\244nk.png" "b/source/pictures/hitta-snabbl\303\244nk.png" deleted file mode 100644 index 249be48..0000000 Binary files "a/source/pictures/hitta-snabbl\303\244nk.png" and /dev/null differ diff --git a/source/pictures/mailalias.png b/source/pictures/mailalias.png deleted file mode 100644 index 58d9640..0000000 Binary files a/source/pictures/mailalias.png and /dev/null differ diff --git a/source/pictures/medlemskap.png b/source/pictures/medlemskap.png deleted file mode 100644 index 80cc2e8..0000000 Binary files a/source/pictures/medlemskap.png and /dev/null differ diff --git "a/source/pictures/v\303\244gbeskrivning.png" "b/source/pictures/v\303\244gbeskrivning.png" deleted file mode 100644 index 67acd9f..0000000 Binary files "a/source/pictures/v\303\244gbeskrivning.png" and /dev/null differ diff --git a/source/spider_conference/spider_conference_2024/spider_conference_2024.rst b/source/spider_conference/spider_conference_2024/spider_conference_2024.rst index f028ef4..dba820e 100644 --- a/source/spider_conference/spider_conference_2024/spider_conference_2024.rst +++ b/source/spider_conference/spider_conference_2024/spider_conference_2024.rst @@ -1,4 +1,4 @@ Spider Conference 2024 ====================== -The Spider Conference 2024 was the most grand Spider Conference in the history of the Spidermans. To not spoil anything for the new Spiders this site will be updated later in 2025. \ No newline at end of file +The Spider Conference 2024 was the most grand Spider Conference in the history of the Spidermans. To not spoil anything for the new Spiders this site will be updated later in 2025 (or not). \ No newline at end of file diff --git a/source/spiderman_duties.rst b/source/spiderman_duties.rst index b2f8af4..cd2cc45 100644 --- a/source/spiderman_duties.rst +++ b/source/spiderman_duties.rst @@ -10,16 +10,11 @@ Godkänna användare Då medlemmar hör av sig till spindelmännen om att de inte har behörighet någonstans eller att de inte kan använda någon funktion på hemsidan är det troligtvis så att de inte är godkända användare. Detta är enkelt löst. -Steg 1: Gå in på ''Administrera''-menyn och välj ''Användare''. +Steg 1: Gå in till "Admin"-sidan och välj "Medlemmar". -.. image:: pictures/vägbeskrivning.png - :alt: Sätt att hitta användare +Steg 2: Sök upp personen i fråga. - -Steg 2: Klicka ''Ge medlemskap'' - -.. image:: pictures/medlemskap.png - :alt: Medlemskap +Steg 3: Klicka "Gör till medlem". Klar! @@ -28,59 +23,27 @@ Klar! Create mail alias ================= -There is a lot of members that what ot have their own cool mail alias for their blabla in the F-guild. Note that this is only an alias and **not** an new email. These aliases only forward mail to an already excisting email adress. To create a new mail alias you use the website. We can create aliases for name@fsektionen.se or name@farad.nu. - -**Step 1**: Log into the website and go to "Administrate" menu and choose "Mail aliases" in the "User" column. - -Det är många medlemmar som vill ha egna coola mailalias för sitt engagemang på F-sektionen. Observera dock att detta endast är ett alias och INTE en ny mailadress. Dessa mailalias bara vidarebefodrar till en existerande emailadress som medlemmen äger. Skapa mailalias gör du via hemsidan. Vi kan skapa mailalias för blablabla@fsektionen.se eller blablabla@farad.nu. - -Steg 1: Gå in på ''Administrera''-menyn och välj ''Mailalias''. +There are a lot of members that want to have their own cool mail alias for their posts or similar official positions in the F-guild. Note that this is only an alias and **not** an new email. These aliases only forward mail to an already excisting email adress. To create a new mail alias you use the website. We can create aliases for name@fsektionen.se (and maybe name@farad.nu). -.. image:: pictures/hitta-mailalias.png - :alt: Sätt att hitta användare +**Step 1**: Log into the website, go to the admin page and choose "Mail aliases" in the "Webmaster Only" category. +**Step 2**: Search to see if the mailalias already exists, otherwise add it. -Steg 2: Skriv in önskad mailalias i sökrutan och klicka sök. +**Step 3**: Add the persons personal email adress as a target of the source alias. Changes are saved automatically. -Steg 3: Skriv in mailadress som önskas kopplas till mailaliaset. Glöm inte att trycka på "Spara"-knappen! - -.. image:: pictures/mailalias.png - :alt: Sätt att skapa mailalias - - -Klar! +Done! ========================================== Ändra mailadresser kopplade till mailalias ========================================== -Steg 1: Gå in på ''Administrera''-menyn och välj ''Mailalias''. - - -Steg 2: Klicka på "Redigera"-knappen så öppnas ett fält med nuvarande mailadresser kopplade till mailaliaset. - -.. image:: pictures/hitta-mailalias.png - :alt: Sätt att hitta användare - - -Steg 3: Skriv till en mailadress på en ny rad eller ändra en befintlig. Glöm inte att trycka på "Spara"-ikonen! - -Klar! - - -=============== -Skapa snabblänk -=============== - -Steg 1: Gå in på ''Administrera''-menyn och välj ''Snabblänkar''. +Detta sker på ett sätt mycket likt ovanstående guide. -.. image:: pictures/hitta-snabblänk.png - :alt: Sätt att hitta snabblänk +Steg 1: Gå till mailaliassidan från adminsidan. -Steg 2: Skriv in namn på snabblänken i vänster fält och vart snabblänken ska leda i höger fält. Alltså, snabblänken kommer se ut fsektionen.se/google och när man klickar på denna så kommer man till www.google.com +Steg 2: Sök upp mailalias. -.. image:: pictures/fixa-snabblänk.png - :alt: Sätt att fixa snabblänk +Steg 3: Använd plus/minus knapparna eller klicka på ett namn för att ändra. Klar! @@ -92,7 +55,7 @@ Steg 1: Läs mailet. Steg 2: Tänk ut ett bra svar på mailet som är hjälpsamt. -Steg 3: Skriv detta bra-iga svar och tänk på att vidarebefodra svaret till alla spindelmän! +Steg 3: Skriv detta bra-iga svar och tänk på att vidarebefodra svaret till alla spindelmän! Använd "Svara alla" t.ex.! Steg 4: Du har nu +10000000 social credit points. @@ -102,7 +65,7 @@ Klar! Gå på möte ========== -Steg 1: Lägg in i din kalender vilka dagar som det är spindelmöte. Du kan också följa spindelkalendern. +Steg 1: Lägg in i din kalender vilka dagar som det är spindelmöte. Steg 2: Kom till spindelmötet. Var inte sen! diff --git a/source/web/naming.rst b/source/web/naming.rst index 1d497f4..3538704 100644 --- a/source/web/naming.rst +++ b/source/web/naming.rst @@ -7,14 +7,14 @@ Functions The Python functions in a route definition should be named descriptively. The name of the function is used as a description in Swagger, which makes it easy to understand what a route is supposed to do in Swagger if named clearly. For example, a function named ``create_example`` will get the description "Create Example" in Swagger. - **Use snake_case** for function names. -- Choose a name that describes the function’s purpose clearly. For instance, ``create_example`` is more descriptive than ``example_creation``. +- Choose a name that describes the function's purpose clearly. For instance, ``create_example`` is more descriptive than ``example_creation``. Schemas ------- For many different objects, a lot of basic schemas will be used for similar purposes. One common naming convention is to follow CRUD (Create, Read, Update, and Delete), which describes how the most common schemas should be named. -Let’s say we want to create some schemas for a database model called *Example*. For the different routes, the schemas should ideally be named: +Let's say we want to create some schemas for a database model called *Example*. For the different routes, the schemas should ideally be named: - **POST route**: ``ExampleCreate`` - **GET route**: ``ExampleRead`` @@ -26,4 +26,4 @@ All schemas should be written in **PascalCase**. Database Models --------------- -Database models should be written in **PascalCase** with the suffix ``_DB``. For example: ``C +Database models should be written in **PascalCase** with the suffix ``_DB``. For example: ``CrazyChairs_DB``.