From dfb8ffb94b2e61127a3f3442aea052cccd47cfe9 Mon Sep 17 00:00:00 2001
From: 0ry5 <oscar.bloch@posteo.de>
Date: Fri, 13 Sep 2024 18:53:35 +0200
Subject: [PATCH 1/5] test

---
 .gitignore            |  3 ++
 createMR.py           | 55 ++++++++++++++++++++++++++++++
 createMR.sh           | 79 +++++++++++++++++++++++++++++++++++++++++++
 frontend/.gitignore   |  1 +
 issues                |  1 +
 middleware/.gitignore |  2 ++
 6 files changed, 141 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 createMR.py
 create mode 100644 createMR.sh
 create mode 100644 frontend/.gitignore
 create mode 100644 issues
 create mode 100644 middleware/.gitignore

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3c7e834
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.env
+n39env
+.vscode
\ No newline at end of file
diff --git a/createMR.py b/createMR.py
new file mode 100644
index 0000000..699c2ac
--- /dev/null
+++ b/createMR.py
@@ -0,0 +1,55 @@
+import os
+import json
+import argparse
+from git import Repo
+from pyforgejo import AuthenticatedClient
+from dotenv import load_dotenv
+from pyforgejo.api.issue import issue_list_issues
+from pyforgejo.api.repository import repo_create_branch, repo_create_pull_request
+from pyforgejo.models import CreateBranchRepoOption, CreatePullRequestOption
+
+load_dotenv()
+
+parser = argparse.ArgumentParser(description="")
+parser.add_argument("-i", "--issue")
+args = parser.parse_args()
+
+FORJEGO_ACCESS_TOKEN = os.getenv("FORJEGO_ACCESS_TOKEN")
+OWNER = os.getenv("OWNER")
+USER = os.getenv("USER")
+REPO = os.getenv("REPO")
+
+client = AuthenticatedClient(
+    base_url="https://git.n39.eu/api/v1/", token=FORJEGO_ACCESS_TOKEN
+)
+
+issues = json.loads(issue_list_issues.sync_detailed(OWNER, REPO, client=client).content)
+
+
+def filterIssues(issue):
+    return args.issue in issue["title"]
+
+
+issues = list(filter(filterIssues, issues))
+
+branchName = issues[0]["title"].replace(" ", "-")
+
+if len(issues) > 1:
+    raise ValueError("found more than one ticket")
+if len(issues) == 0:
+    raise ValueError("found more than one ticket")
+else:
+    repo_create_branch.sync_detailed(
+        OWNER, REPO, body=CreateBranchRepoOption(branchName, "main"), client=client
+    )
+    res = json.loads(
+        repo_create_pull_request.sync_detailed(
+            OWNER,
+            REPO,
+            body=CreatePullRequestOption(
+                base=branchName, head="main", title="merge" + branchName
+            ),
+            client=client,
+        ).content
+    )
+    print(res)
diff --git a/createMR.sh b/createMR.sh
new file mode 100644
index 0000000..d7da326
--- /dev/null
+++ b/createMR.sh
@@ -0,0 +1,79 @@
+source './.env'
+
+PASSED_ISSUE=''
+
+while test $# -gt 0; do
+  case "$1" in
+    -i|--issue) 
+      shift
+      if test $# -gt 0 
+      then
+        export PASSED_ISSUE=$1
+      fi
+      shift
+      ;;
+    -h|--help) print_usage ;;
+    *) print_usage
+       break
+       ;;
+  esac
+done
+
+if [[ -z $PASSED_ISSUE ]]
+then
+  echo -e "missing issue; use -i \e[3missue\e[0m"
+  exit 1
+fi
+
+printf "\n"
+
+ISSUES=$(curl -X 'GET' \
+  'https://git.n39.eu/api/v1/repos/'$OWNER'/'$REPO'/issues' \
+  -H 'accept: application/json' | tr ' ' -)
+
+printf "\n"
+echo $ISSUES
+
+ISSUE=$(jq '.[] | select(.title|test("'$PASSED_ISSUE'"))' <<< $ISSUES)
+
+printf "\n"
+echo $ISSUE
+
+NAME=$(jq '.title' <<< $ISSUE)
+
+printf "\n"
+echo $NAME
+
+BRANCH=$(curl -X 'POST' 'https://git.n39.eu/api/v1/repos/'$OWNER'/'$REPO'/branches' \
+    -H "accept: application/json" \
+    -H 'Authorization: token '${FORJEGO_ACCESS_TOKEN} \
+    -H "Content-Type: application/json" -d '{"new_branch_name": '$NAME', "old_branch_name": "main"}' -i)
+
+echo $BRANCH
+
+USER=$(git config --global user.name)
+
+MR=$(curl -X 'POST' 'https://git.n39.eu/api/v1/repos/'$OWNER'/'$REPO'/pulls' \
+    -H "accept: application/json" \
+    -H 'Authorization: token '${FORJEGO_ACCESS_TOKEN} \
+    -H "Content-Type: application/json" -d '{
+        "assignee": "'$USER'",
+        "assignees": [
+            "string"
+          ],
+          "base": "string",
+          "body": "string",
+          "head": "string",
+          "labels": [
+            0
+          ],
+          "milestone": 0,
+          "title": "string"
+        }' -i)
+
+USER=$(git checkout $BRANCH)
+
+
+#git browse -- issues
+#git checkout -b ''
+#git push -o merge_request.create origin my-branch
\ No newline at end of file
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..b512c09
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/issues b/issues
new file mode 100644
index 0000000..7bb88ed
--- /dev/null
+++ b/issues
@@ -0,0 +1 @@
+[{"id":844,"url":"https://git.n39.eu/api/v1/repos/0Ry5/n39librarian/issues/5","html_url":"https://git.n39.eu/0Ry5/n39librarian/issues/5","number":5,"user":{"id":118,"login":"0Ry5","login_name":"","full_name":"","email":"0ry5@git.n39.eu","avatar_url":"https://git.n39.eu/avatar/b3977e6b4b95a3a537127207119712a2","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2024-09-09T20:35:12+02:00","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"0Ry5"},"original_author":"","original_author_id":0,"title":"checkout modal can be submitted if its invalid","body":"","ref":"","assets":[],"labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"created_at":"2024-09-13T14:25:22+02:00","updated_at":"2024-09-13T14:25:22+02:00","closed_at":null,"due_date":null,"pull_request":null,"repository":{"id":65,"name":"n39librarian","owner":"0Ry5","full_name":"0Ry5/n39librarian"},"pin_order":0},{"id":843,"url":"https://git.n39.eu/api/v1/repos/0Ry5/n39librarian/issues/4","html_url":"https://git.n39.eu/0Ry5/n39librarian/issues/4","number":4,"user":{"id":118,"login":"0Ry5","login_name":"","full_name":"","email":"0ry5@git.n39.eu","avatar_url":"https://git.n39.eu/avatar/b3977e6b4b95a3a537127207119712a2","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2024-09-09T20:35:12+02:00","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"0Ry5"},"original_author":"","original_author_id":0,"title":"undefined should not be a valid input for the checkout modal","body":"","ref":"","assets":[],"labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"created_at":"2024-09-13T14:25:07+02:00","updated_at":"2024-09-13T14:25:07+02:00","closed_at":null,"due_date":null,"pull_request":null,"repository":{"id":65,"name":"n39librarian","owner":"0Ry5","full_name":"0Ry5/n39librarian"},"pin_order":0},{"id":839,"url":"https://git.n39.eu/api/v1/repos/0Ry5/n39librarian/issues/3","html_url":"https://git.n39.eu/0Ry5/n39librarian/issues/3","number":3,"user":{"id":118,"login":"0Ry5","login_name":"","full_name":"","email":"0ry5@git.n39.eu","avatar_url":"https://git.n39.eu/avatar/b3977e6b4b95a3a537127207119712a2","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2024-09-09T20:35:12+02:00","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"0Ry5"},"original_author":"","original_author_id":0,"title":"add unit tests","body":"","ref":"","assets":[],"labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"created_at":"2024-09-09T20:49:52+02:00","updated_at":"2024-09-09T20:49:52+02:00","closed_at":null,"due_date":null,"pull_request":null,"repository":{"id":65,"name":"n39librarian","owner":"0Ry5","full_name":"0Ry5/n39librarian"},"pin_order":0},{"id":838,"url":"https://git.n39.eu/api/v1/repos/0Ry5/n39librarian/issues/2","html_url":"https://git.n39.eu/0Ry5/n39librarian/issues/2","number":2,"user":{"id":118,"login":"0Ry5","login_name":"","full_name":"","email":"0ry5@git.n39.eu","avatar_url":"https://git.n39.eu/avatar/b3977e6b4b95a3a537127207119712a2","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2024-09-09T20:35:12+02:00","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"0Ry5"},"original_author":"","original_author_id":0,"title":"try google-books api for book identification by isbn","body":"https://www.npmjs.com/package/@googleapis/books\r\n\r\nadd to CreateBookModal","ref":"","assets":[],"labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"created_at":"2024-09-09T20:49:15+02:00","updated_at":"2024-09-09T20:49:15+02:00","closed_at":null,"due_date":null,"pull_request":null,"repository":{"id":65,"name":"n39librarian","owner":"0Ry5","full_name":"0Ry5/n39librarian"},"pin_order":0},{"id":837,"url":"https://git.n39.eu/api/v1/repos/0Ry5/n39librarian/issues/1","html_url":"https://git.n39.eu/0Ry5/n39librarian/issues/1","number":1,"user":{"id":118,"login":"0Ry5","login_name":"","full_name":"","email":"0ry5@git.n39.eu","avatar_url":"https://git.n39.eu/avatar/b3977e6b4b95a3a537127207119712a2","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2024-09-09T20:35:12+02:00","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"0Ry5"},"original_author":"","original_author_id":0,"title":"Darkmode","body":"","ref":"","assets":[],"labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"created_at":"2024-09-09T20:48:27+02:00","updated_at":"2024-09-09T20:48:27+02:00","closed_at":null,"due_date":null,"pull_request":null,"repository":{"id":65,"name":"n39librarian","owner":"0Ry5","full_name":"0Ry5/n39librarian"},"pin_order":0}]
diff --git a/middleware/.gitignore b/middleware/.gitignore
new file mode 100644
index 0000000..db4c6d9
--- /dev/null
+++ b/middleware/.gitignore
@@ -0,0 +1,2 @@
+dist
+node_modules
\ No newline at end of file

From af569cbde626c556864ebc6ddcf37dcfde4e5b2a Mon Sep 17 00:00:00 2001
From: 0ry5 <oscar.bloch@posteo.de>
Date: Fri, 13 Sep 2024 19:00:18 +0200
Subject: [PATCH 2/5] remove shell script

---
 createMR.py |  2 +-
 createMR.sh | 79 -----------------------------------------------------
 2 files changed, 1 insertion(+), 80 deletions(-)
 delete mode 100644 createMR.sh

diff --git a/createMR.py b/createMR.py
index 699c2ac..7026c71 100644
--- a/createMR.py
+++ b/createMR.py
@@ -47,7 +47,7 @@ else:
             OWNER,
             REPO,
             body=CreatePullRequestOption(
-                base=branchName, head="main", title="merge" + branchName
+                base="main", head=branchName, title="merge-" + branchName
             ),
             client=client,
         ).content
diff --git a/createMR.sh b/createMR.sh
deleted file mode 100644
index d7da326..0000000
--- a/createMR.sh
+++ /dev/null
@@ -1,79 +0,0 @@
-source './.env'
-
-PASSED_ISSUE=''
-
-while test $# -gt 0; do
-  case "$1" in
-    -i|--issue) 
-      shift
-      if test $# -gt 0 
-      then
-        export PASSED_ISSUE=$1
-      fi
-      shift
-      ;;
-    -h|--help) print_usage ;;
-    *) print_usage
-       break
-       ;;
-  esac
-done
-
-if [[ -z $PASSED_ISSUE ]]
-then
-  echo -e "missing issue; use -i \e[3missue\e[0m"
-  exit 1
-fi
-
-printf "\n"
-
-ISSUES=$(curl -X 'GET' \
-  'https://git.n39.eu/api/v1/repos/'$OWNER'/'$REPO'/issues' \
-  -H 'accept: application/json' | tr ' ' -)
-
-printf "\n"
-echo $ISSUES
-
-ISSUE=$(jq '.[] | select(.title|test("'$PASSED_ISSUE'"))' <<< $ISSUES)
-
-printf "\n"
-echo $ISSUE
-
-NAME=$(jq '.title' <<< $ISSUE)
-
-printf "\n"
-echo $NAME
-
-BRANCH=$(curl -X 'POST' 'https://git.n39.eu/api/v1/repos/'$OWNER'/'$REPO'/branches' \
-    -H "accept: application/json" \
-    -H 'Authorization: token '${FORJEGO_ACCESS_TOKEN} \
-    -H "Content-Type: application/json" -d '{"new_branch_name": '$NAME', "old_branch_name": "main"}' -i)
-
-echo $BRANCH
-
-USER=$(git config --global user.name)
-
-MR=$(curl -X 'POST' 'https://git.n39.eu/api/v1/repos/'$OWNER'/'$REPO'/pulls' \
-    -H "accept: application/json" \
-    -H 'Authorization: token '${FORJEGO_ACCESS_TOKEN} \
-    -H "Content-Type: application/json" -d '{
-        "assignee": "'$USER'",
-        "assignees": [
-            "string"
-          ],
-          "base": "string",
-          "body": "string",
-          "head": "string",
-          "labels": [
-            0
-          ],
-          "milestone": 0,
-          "title": "string"
-        }' -i)
-
-USER=$(git checkout $BRANCH)
-
-
-#git browse -- issues
-#git checkout -b ''
-#git push -o merge_request.create origin my-branch
\ No newline at end of file

From f35c4c473d5040f1aa7d5ebf34b2af6fa226c623 Mon Sep 17 00:00:00 2001
From: 0ry5 <oscar.bloch@posteo.de>
Date: Fri, 13 Sep 2024 19:38:46 +0200
Subject: [PATCH 3/5] feat(GoogleApi): add google api to BookModal

---
 .../shared/components/modals/BookModal.tsx    | 60 ++++++++++++-------
 1 file changed, 37 insertions(+), 23 deletions(-)

diff --git a/frontend/src/shared/components/modals/BookModal.tsx b/frontend/src/shared/components/modals/BookModal.tsx
index fb852c1..5c086b6 100644
--- a/frontend/src/shared/components/modals/BookModal.tsx
+++ b/frontend/src/shared/components/modals/BookModal.tsx
@@ -29,7 +29,20 @@ export const BookModal = ({
   }, [book, reset]);
 
   const { scannerError, setScannerRef } = useScanner({
-    onDetected: (result) => {
+    onDetected: async (result) => {
+      const googleBooks = await (
+        await fetch(
+          "https://www.googleapis.com/books/v1/volumes?q=isbn:" + result
+        )
+      ).json();
+      if ("items" in googleBooks) {
+        console.log(googleBooks);
+        setValue(
+          "published",
+          googleBooks.items[0].volumeInfo.publishedDate.substring(0, 4)
+        );
+        setValue("title", googleBooks.items[0].volumeInfo.title);
+      }
       setValue("isbn", result);
       setShowScanner(false);
     },
@@ -38,6 +51,7 @@ export const BookModal = ({
   const values = useWatch({ control });
 
   const [submitError, setSubmitError] = useState<string | undefined>();
+
   const onSubmit = useCallback(
     async (data: Partial<BookForm>) => {
       setSubmitError(undefined);
@@ -89,7 +103,7 @@ export const BookModal = ({
       <ModalHeader
         onClose={onClose}
         title={`${!!book ? "Edit" : "Add new"} Book`}
-        icon={<ImBook size={50} className="ml-0 mr-auto" />}
+        icon={<ImBook size={50} className='ml-0 mr-auto' />}
       />
       <Modal.Body
         style={{
@@ -100,18 +114,18 @@ export const BookModal = ({
       >
         {!showScanner && (
           <Form
-            className="mb-2"
+            className='mb-2'
             onSubmit={(ev) => {
               ev.preventDefault();
               onSubmit(values);
             }}
           >
-            <Form.Group as={Row} className="mb-2">
-              <Col sm="2">
+            <Form.Group as={Row} className='mb-2'>
+              <Col sm='2'>
                 <Form.Label>ISBN</Form.Label>
               </Col>
-              <Col className="d-flex flex-column">
-                <div className="d-flex">
+              <Col className='d-flex flex-column'>
+                <div className='d-flex'>
                   <Form.Control
                     {...register("isbn", { required: true, maxLength: 360 })}
                     isInvalid={!!formState.errors.isbn}
@@ -126,7 +140,7 @@ export const BookModal = ({
                       color: "black",
                       border: "2px solid black",
                     }}
-                    className="mr-2 pt-0"
+                    className='mr-2 pt-0'
                     disabled={showScanner}
                     onClick={() => setShowScanner(true)}
                   >
@@ -135,7 +149,7 @@ export const BookModal = ({
                 </div>
                 <Form.Control.Feedback
                   style={{ display: !formState.errors.isbn ? "none" : "block" }}
-                  type="invalid"
+                  type='invalid'
                 >
                   {!formState.errors.isbn
                     ? "ISBN is required"
@@ -143,8 +157,8 @@ export const BookModal = ({
                 </Form.Control.Feedback>
               </Col>
             </Form.Group>
-            <Form.Group as={Row} className="mb-2">
-              <Col sm="2">
+            <Form.Group as={Row} className='mb-2'>
+              <Col sm='2'>
                 <Form.Label>Title</Form.Label>
               </Col>
               <Col>
@@ -153,15 +167,15 @@ export const BookModal = ({
                   isInvalid={!!formState.errors.title}
                 />
 
-                <Form.Control.Feedback type="invalid">
+                <Form.Control.Feedback type='invalid'>
                   {!values.title
                     ? "Title is required"
                     : formState.errors.title?.message}
                 </Form.Control.Feedback>
               </Col>
             </Form.Group>
-            <Form.Group as={Row} className="mb-2">
-              <Col sm="2">
+            <Form.Group as={Row} className='mb-2'>
+              <Col sm='2'>
                 <Form.Label>Year published</Form.Label>
               </Col>
               <Col>
@@ -187,15 +201,15 @@ export const BookModal = ({
                   isInvalid={!!formState.errors.published}
                 />
 
-                <Form.Control.Feedback type="invalid">
+                <Form.Control.Feedback type='invalid'>
                   {!values.published
                     ? "Year published is required"
                     : formState.errors.published?.message}
                 </Form.Control.Feedback>
               </Col>
             </Form.Group>
-            <Form.Group as={Row} className="mb-2">
-              <Col sm="2">
+            <Form.Group as={Row} className='mb-2'>
+              <Col sm='2'>
                 <Form.Label>Shelf</Form.Label>
               </Col>
               <Col>
@@ -205,21 +219,21 @@ export const BookModal = ({
                 >
                   {Object.keys(bookShelfs).map((key) => (
                     <option
-                      key="key"
+                      key='key'
                       value={bookShelfs[key as keyof typeof bookShelfs]}
                     >
                       {key}
                     </option>
                   ))}
                 </Form.Select>
-                <Form.Control.Feedback type="invalid">
+                <Form.Control.Feedback type='invalid'>
                   {!values.shelf
                     ? "Shelf is required"
                     : formState.errors.shelf?.message}
                 </Form.Control.Feedback>
               </Col>
             </Form.Group>
-            <div className="d-flex mx-auto mb-auto mt-2 w-100">
+            <div className='d-flex mx-auto mb-auto mt-2 w-100'>
               <Button
                 style={{
                   borderRadius: "5px",
@@ -238,12 +252,12 @@ export const BookModal = ({
         )}
         {showScanner && (
           <div
-            className="w-100 overflow-hidden"
+            className='w-100 overflow-hidden'
             ref={(ref) => setScannerRef(ref)}
             style={{ position: "relative", height: "25vh" }}
           >
             <canvas
-              className="drawingBuffer w-100 position-absolute"
+              className='drawingBuffer w-100 position-absolute'
               style={{
                 height: "100%",
                 border: "2px solid black",
@@ -258,7 +272,7 @@ export const BookModal = ({
           </p>
         ) : null}
         {!!submitError && (
-          <Form.Control.Feedback style={{ display: "block" }} type="invalid">
+          <Form.Control.Feedback style={{ display: "block" }} type='invalid'>
             {submitError}
           </Form.Control.Feedback>
         )}

From 6940fab53a5c1902f698b48050e15ddd3207967d Mon Sep 17 00:00:00 2001
From: 0ry5 <oscar.bloch@posteo.de>
Date: Fri, 13 Sep 2024 19:46:16 +0200
Subject: [PATCH 4/5] fix(CheckoutModal): no submit if invalid form

---
 .../src/shared/components/modals/CheckoutModal.tsx   | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/frontend/src/shared/components/modals/CheckoutModal.tsx b/frontend/src/shared/components/modals/CheckoutModal.tsx
index 79d69ac..d3eb42c 100644
--- a/frontend/src/shared/components/modals/CheckoutModal.tsx
+++ b/frontend/src/shared/components/modals/CheckoutModal.tsx
@@ -30,6 +30,9 @@ export const CheckoutBookModal = ({
 
   const onSubmit = useCallback(
     async (data: Partial<BookCheckoutForm>) => {
+      if (!formState.isValid) {
+        return;
+      }
       if (!data.checkoutBy) {
         setError("checkoutBy", {
           message: `please enter ${
@@ -61,7 +64,7 @@ export const CheckoutBookModal = ({
         setFailed(await res.text());
       }
     },
-    [uuid, onClose, setError, isChechout]
+    [formState.isValid, isChechout, uuid, setError, onClose]
   );
 
   return (
@@ -116,6 +119,12 @@ export const CheckoutBookModal = ({
                         "'null' is not a valid nickname"
                       );
                     },
+                    notUndefinedString: (value) => {
+                      return (
+                        value?.toLocaleLowerCase() !== "undefined" ||
+                        "'undefined' is not a valid nickname"
+                      );
+                    },
                   },
                 })}
                 isInvalid={!!formState.errors.checkoutBy}
@@ -184,6 +193,7 @@ export const CheckoutBookModal = ({
                 marginLeft: "auto",
                 marginRight: "10px",
               }}
+              disabled={!formState.isValid}
               onClick={() => onSubmit(values)}
             >
               {`${isChechout ? "Checkout" : "Return"} book`}

From 2016a8619d2824d42e6b25b57607d87feabfe1a6 Mon Sep 17 00:00:00 2001
From: 0ry5 <oscar.bloch@posteo.de>
Date: Fri, 13 Sep 2024 21:28:03 +0200
Subject: [PATCH 5/5] feat(Darkmode): add Darkmode

---
 frontend/src/App.tsx                          |   3 +-
 frontend/src/darkmodeColors.ts                |   5 +
 frontend/src/index.css                        | 227 +++++++++++++++---
 .../src/pages/Library/components/Actions.tsx  |  34 +--
 frontend/src/pages/Main/Main.tsx              |  17 --
 .../components/modals/AuthenticationModal.tsx |  16 +-
 .../shared/components/modals/BookModal.tsx    |  17 +-
 .../components/modals/CheckoutModal.tsx       |  14 +-
 .../components/modals/DeleteBookModal.tsx     |  17 +-
 .../shared/components/modals/ModalHeader.tsx  |  11 +-
 .../shared/components/modals/ScannerModal.tsx |  14 +-
 11 files changed, 216 insertions(+), 159 deletions(-)
 create mode 100644 frontend/src/darkmodeColors.ts

diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index f86f282..b2dae2a 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -6,7 +6,7 @@ import "./App.css";
 import { Main } from "./pages";
 import { Library } from "./pages/Library";
 import { Book } from "./pages/Book";
-import { primary } from "./colors";
+import * as colors from "./colors";
 import { ActiveModalProps } from "./shared/components/modals/types";
 
 export const AuthContext = createContext<{
@@ -50,7 +50,6 @@ function App() {
       style={{
         height: "100vh",
         width: "100vw",
-        backgroundColor: primary,
         fontFamily: "New Amsterdam",
         overflow: "scroll",
       }}
diff --git a/frontend/src/darkmodeColors.ts b/frontend/src/darkmodeColors.ts
new file mode 100644
index 0000000..61021a2
--- /dev/null
+++ b/frontend/src/darkmodeColors.ts
@@ -0,0 +1,5 @@
+export const primary = "#5e6268";
+export const primaryRGBA = "rgba(83,86,91, 0.8)";
+export const secondary = "#5f5e68";
+export const tertiary = "#5e6768";
+export const danger = "#685e62";
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 336c05e..052c22e 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -12,49 +12,208 @@ body {
   }
 }
 
-#bootstrap-overrides .form-select .form-control {
-  background-color: #f2f2e8 !important;
-  border-color: #f2f2e8 !important;
+#bootstrap-overrides {
+  background-color: #f2f3f4;
+  color: #5e6268;
+
+  .btn-primary {
+    background-color: #5e6268;
+    color: #f2f3f4;
+    border: 2px solid #5e6268;
+  }
+
+  form-control {
+    background-color: #f2f2e8 !important;
+    border-color: #5e6268 !important;
+  }
+
+  .form-select .form-control {
+    background-color: #f2f2e8 !important;
+    border-color: #f2f2e8 !important;
+  }
+
+  .dropdown-toggle::after {
+    display: none;
+  }
+
+  .form-label {
+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
+      "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
+      "Helvetica Neue", sans-serif !important;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+    letter-spacing: normal;
+  }
+
+  .table > :not(caption) > * > * {
+    background-color: #f2f2e8 !important;
+    color: #5e6268 !important;
+  }
+
+  table {
+    letter-spacing: 0.25em;
+  }
+
+  .bg-primary {
+    background-color: #f2f3f4 !important;
+  }
+
+  .modal-content {
+    background-color: #f2f3f4 !important;
+    border-radius: 10px;
+  }
+
+  .modal-body {
+    background-color: #f2f3f4 !important;
+    border-radius: 0px 0px 10px 10px;
+  }
+
+  .modal-header {
+    background-color: #f2f3f4 !important;
+    border-radius: 10px 10px 0px 0px;
+    border-bottom: none;
+  }
+
+  .dropdown button {
+    background-color: #f2f2e8 !important;
+    color: #5e6268 !important;
+    border: 2px solid #5e6268;
+  }
+
+  .dropdown-menu {
+    background-color: #f2f2e8 !important;
+    color: #5e6268 !important;
+    border: 2px solid #f2f2e8;
+  }
+
+  .danger {
+    background-color: #f9a9ab !important;
+  }
+
+  .page-link {
+    color: #5e6268 !important;
+    border: 2px solid #5e6268 !important;
+    background-color: transparent !important;
+  }
+
+  .active > .page-link {
+    background-color: #5e6268 !important;
+    color: #f2f3f4 !important;
+  }
+
+  .pagination {
+    margin-top: 10px !important;
+
+    li {
+      min-width: 40px;
+    }
+  }
 }
 
-#bootstrap-overrides .dropdown-toggle::after {
-  display: none;
-}
+@media (prefers-color-scheme: dark) {
+  #bootstrap-overrides {
+    background-color: #5e6268;
+    color: #f2f3f4;
 
-#bootstrap-overrides .form-label {
-  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
-    "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
-    sans-serif !important;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-  letter-spacing: normal;
-}
+    .btn-primary {
+      background-color: #f2f3f4;
+      color: #5e6268;
+      border: 2px solid #5e6268;
+    }
 
-#bootstrap-overrides table {
-  letter-spacing: 0.25em;
-}
+    .form-control {
+      background-color: #f2f2e8 !important;
+      border-color: #5e6268 !important;
+      border-right: none;
+    }
 
-#bootstrap-overrides .bg-primary {
-  background-color: #f2f3f4 !important;
-}
+    .form-select .form-control {
+      background-color: #f2f2e8 !important;
+      border-color: #f2f2e8 !important;
+    }
 
-#bootstrap-overrides .modal-body {
-  background-color: #f2f3f4 !important;
-}
+    .dropdown-toggle::after {
+      display: none;
+    }
 
-#bootstrap-overrides .danger {
-  background-color: #f9a9ab !important;
-}
+    .form-label {
+      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
+        "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
+        "Helvetica Neue", sans-serif !important;
+      -webkit-font-smoothing: antialiased;
+      -moz-osx-font-smoothing: grayscale;
+      letter-spacing: normal;
+    }
 
-#bootstrap-overrides .page-link {
-  color: black !important;
-  border: 2px solid black !important;
-  background-color: transparent !important;
-}
+    .table > :not(caption) > * > * {
+      background-color: #5e6268 !important;
+      color: #f2f2e8 !important;
+    }
 
-#bootstrap-overrides .active > .page-link {
-  background-color: black !important;
-  color: #f2f3f4 !important;
+    table {
+      letter-spacing: 0.25em;
+    }
+
+    .bg-primary {
+      background-color: #5e6268 !important;
+    }
+
+    .dropdown button {
+      background-color: #f2f2e8 !important;
+      color: #5e6268 !important;
+    }
+
+    .dropdown-menu {
+      background-color: #5e6268 !important;
+      border: 2px solid #5e6268;
+    }
+
+    .dropdown-item {
+      color: #f2f2e8 !important;
+      svg {
+        color: #f2f2e8 !important;
+      }
+    }
+    .dropdown-item:hover {
+      color: #5e6268 !important;
+      background-color: #f2f2e8 !important;
+    }
+
+    .modal-content {
+      background-color: #5e6268 !important;
+      border-radius: 10px;
+    }
+
+    .modal-body {
+      background-color: #5e6268 !important;
+      border-radius: 0px 0px 10px 10px;
+    }
+
+    .modal-header {
+      background-color: #5e6268 !important;
+      border-radius: 10px 10px 0px 0px;
+      border-bottom: none;
+    }
+
+    .danger {
+      background-color: #c94414 !important;
+    }
+
+    .page-link {
+      color: #f2f3f4 !important;
+      border: 2px solid #f2f3f4 !important;
+      background-color: transparent !important;
+    }
+
+    .active > .page-link {
+      background-color: #f2f3f4 !important;
+      color: #5e6268 !important;
+    }
+
+    .pagination {
+      margin-top: 10px !important;
+    }
+  }
 }
 
 code {
diff --git a/frontend/src/pages/Library/components/Actions.tsx b/frontend/src/pages/Library/components/Actions.tsx
index 2bc5dd4..59077ad 100644
--- a/frontend/src/pages/Library/components/Actions.tsx
+++ b/frontend/src/pages/Library/components/Actions.tsx
@@ -2,7 +2,8 @@ import { Badge, Dropdown } from "react-bootstrap";
 import { Book } from "../../../types/Book";
 import { ImBin, ImBook, ImBoxAdd, ImBoxRemove, ImMenu } from "react-icons/im";
 import { useMutation } from "@tanstack/react-query";
-import { secondary } from "../../../colors";
+import * as colors from "../../../colors";
+import * as dark from "../../../darkmodeColors";
 import { ModalContextType } from "../../../App";
 import { modalTypes } from "../../../shared/components/modals/types";
 import { useCallback } from "react";
@@ -44,11 +45,7 @@ export const Actions = ({
   return (
     <>
       <Dropdown>
-        <Dropdown.Toggle
-          variant='success'
-          id='dropdown-basic'
-          style={{ backgroundColor: secondary, border: "2px solid black" }}
-        >
+        <Dropdown.Toggle variant='success' id='dropdown-basic'>
           <ImMenu size={25} color='black' />
         </Dropdown.Toggle>
 
@@ -59,14 +56,7 @@ export const Actions = ({
               className='d-flex'
               onClick={() => setActiveModal({ type: bookModal, book, onClose })}
             >
-              <Badge
-                pill
-                className='ml-2 d-flex mr-2'
-                style={{
-                  border: "2px solid black",
-                  backgroundColor: "transparent",
-                }}
-              >
+              <Badge pill className='ml-2 d-flex mr-2'>
                 <ImBook size={25} color='black' className='m-auto' />
               </Badge>
               <p className='m-auto' style={{ paddingLeft: "10px" }}>
@@ -87,13 +77,7 @@ export const Actions = ({
                 })
               }
             >
-              <Badge
-                pill
-                className='ml-2 d-flex mr-2'
-                style={{
-                  border: "2px solid black",
-                }}
-              >
+              <Badge pill className='ml-2 d-flex mr-2'>
                 <ImBoxRemove size={25} color='black' className='m-auto' />
               </Badge>{" "}
               <p className='m-auto' style={{ paddingLeft: "10px" }}>
@@ -130,13 +114,7 @@ export const Actions = ({
                 setActiveModal({ type: del, uuid: book.uuid, onClose })
               }
             >
-              <Badge
-                pill
-                className='ml-2 d-flex danger mr-2'
-                style={{
-                  border: "2px solid black",
-                }}
-              >
+              <Badge pill className='ml-2 d-flex danger mr-2'>
                 <ImBin size={25} color='black' className='m-auto' />
               </Badge>{" "}
               <p className='m-auto' style={{ paddingLeft: "10px" }}>
diff --git a/frontend/src/pages/Main/Main.tsx b/frontend/src/pages/Main/Main.tsx
index 090b13d..a1c7197 100644
--- a/frontend/src/pages/Main/Main.tsx
+++ b/frontend/src/pages/Main/Main.tsx
@@ -13,7 +13,6 @@ import { TfiKey } from "react-icons/tfi";
 
 import { Link, useNavigate } from "react-router-dom";
 import { Book } from "../../types/Book";
-import { secondary } from "../../colors";
 import { useAuth } from "../../shared/utils/useAuthentication";
 import { ModalContext } from "../../App";
 import { modalTypes } from "../../shared/components/modals/types";
@@ -95,16 +94,12 @@ export const Main = (): React.JSX.Element => {
             onChange={(ev) => setTitle(ev.target.value)}
             style={{
               borderRadius: "20px 0px 0px 20px",
-              border: "2px solid black",
               borderRight: "none",
             }}
           />
           <Button
             style={{
               borderRadius: "0px 5px 5px 0px",
-              backgroundColor: secondary,
-              color: "black",
-              border: "2px solid black",
             }}
             className='mr-2'
             disabled={activeModal?.type === scanner || isFetching}
@@ -135,9 +130,6 @@ export const Main = (): React.JSX.Element => {
           <Button
             style={{
               borderRadius: "5px",
-              backgroundColor: secondary,
-              color: "black",
-              border: "2px solid black",
               marginLeft: "auto",
               marginRight: "10px",
             }}
@@ -151,9 +143,6 @@ export const Main = (): React.JSX.Element => {
           <Button
             style={{
               borderRadius: "5px",
-              backgroundColor: secondary,
-              color: "black",
-              border: "2px solid black",
               marginLeft: "auto",
               marginRight: "10px",
             }}
@@ -174,9 +163,6 @@ export const Main = (): React.JSX.Element => {
           <Button
             style={{
               borderRadius: "5px",
-              backgroundColor: secondary,
-              color: "black",
-              border: "2px solid black",
             }}
           >
             <ImBooks /> Browse library{" "}
@@ -186,9 +172,6 @@ export const Main = (): React.JSX.Element => {
           <Button
             style={{
               borderRadius: "5px",
-              backgroundColor: secondary,
-              color: "black",
-              border: "2px solid black",
               marginRight: "auto",
               marginLeft: "10px",
             }}
diff --git a/frontend/src/shared/components/modals/AuthenticationModal.tsx b/frontend/src/shared/components/modals/AuthenticationModal.tsx
index c5c0aa3..af3616c 100644
--- a/frontend/src/shared/components/modals/AuthenticationModal.tsx
+++ b/frontend/src/shared/components/modals/AuthenticationModal.tsx
@@ -2,7 +2,6 @@ import { useCallback, useContext } from "react";
 import { Button, Col, Form, Modal, Row } from "react-bootstrap";
 import { useForm, useWatch } from "react-hook-form";
 import { TfiKey } from "react-icons/tfi";
-import { primaryRGBA, primary, secondary } from "../../../colors";
 import { ModalHeader } from "./ModalHeader";
 import { AuthContext } from "../../../App";
 import { AuthModalProps } from "./types";
@@ -71,12 +70,7 @@ export const AuthenticationModal = ({
   );
 
   return (
-    <Modal
-      show={open}
-      onHide={onClose}
-      style={{ backgroundColor: primaryRGBA }}
-      centered
-    >
+    <Modal show={open} onHide={onClose} centered>
       <ModalHeader
         onClose={onClose}
         title={isUpdate ? "Set new Admin key" : "Admin Login"}
@@ -84,9 +78,7 @@ export const AuthenticationModal = ({
       />
       <Modal.Body
         style={{
-          border: "2px solid black",
           borderTop: "none",
-          backgroundColor: primary,
         }}
       >
         <Form
@@ -141,9 +133,6 @@ export const AuthenticationModal = ({
 
           {!isUpdate && (
             <Form.Group as={Row} className='mb-2'>
-              <Col sm='4'>
-                <Form.Label>Password</Form.Label>
-              </Col>
               <Col>
                 <Form.Control
                   {...register("password", {
@@ -166,9 +155,6 @@ export const AuthenticationModal = ({
             <Button
               style={{
                 borderRadius: "5px",
-                backgroundColor: secondary,
-                color: "black",
-                border: "2px solid black",
                 marginLeft: "auto",
                 marginRight: "10px",
               }}
diff --git a/frontend/src/shared/components/modals/BookModal.tsx b/frontend/src/shared/components/modals/BookModal.tsx
index 5c086b6..3a097ca 100644
--- a/frontend/src/shared/components/modals/BookModal.tsx
+++ b/frontend/src/shared/components/modals/BookModal.tsx
@@ -5,7 +5,6 @@ import { ModalHeader } from "./ModalHeader";
 import { useForm, useWatch } from "react-hook-form";
 import { Book, bookShelfs } from "../../../types/Book";
 import { useScanner } from "../../utils/useScanner";
-import { primary, primaryRGBA, secondary } from "../../../colors";
 import { BookModalProps } from "./types";
 
 type BookForm = Pick<Book, "isbn" | "title" | "shelf" | "published">;
@@ -94,12 +93,7 @@ export const BookModal = ({
   );
 
   return (
-    <Modal
-      show={open}
-      onHide={onClose}
-      style={{ backgroundColor: primaryRGBA }}
-      centered
-    >
+    <Modal show={open} onHide={onClose} centered>
       <ModalHeader
         onClose={onClose}
         title={`${!!book ? "Edit" : "Add new"} Book`}
@@ -107,9 +101,7 @@ export const BookModal = ({
       />
       <Modal.Body
         style={{
-          border: "2px solid black",
           borderTop: "none",
-          backgroundColor: primary,
         }}
       >
         {!showScanner && (
@@ -136,9 +128,6 @@ export const BookModal = ({
                   <Button
                     style={{
                       borderRadius: "0px 5px 5px 0px",
-                      backgroundColor: secondary,
-                      color: "black",
-                      border: "2px solid black",
                     }}
                     className='mr-2 pt-0'
                     disabled={showScanner}
@@ -237,9 +226,6 @@ export const BookModal = ({
               <Button
                 style={{
                   borderRadius: "5px",
-                  backgroundColor: secondary,
-                  color: "black",
-                  border: "2px solid black",
                   marginLeft: "auto",
                   marginRight: "10px",
                 }}
@@ -260,7 +246,6 @@ export const BookModal = ({
               className='drawingBuffer w-100 position-absolute'
               style={{
                 height: "100%",
-                border: "2px solid black",
               }}
             />
           </div>
diff --git a/frontend/src/shared/components/modals/CheckoutModal.tsx b/frontend/src/shared/components/modals/CheckoutModal.tsx
index d3eb42c..41f4077 100644
--- a/frontend/src/shared/components/modals/CheckoutModal.tsx
+++ b/frontend/src/shared/components/modals/CheckoutModal.tsx
@@ -3,7 +3,7 @@ import { Alert, Button, Col, Form, Modal, Row } from "react-bootstrap";
 import { ImBoxAdd, ImBoxRemove } from "react-icons/im";
 import { useForm, useWatch } from "react-hook-form";
 import { Book } from "../../../types/Book";
-import { primary, primaryRGBA, secondary } from "../../../colors";
+import { primary, secondary } from "../../../colors";
 import { ModalHeader } from "./ModalHeader";
 import { CheckoutBookModalProps } from "./types";
 import { AiOutlineExclamationCircle } from "react-icons/ai";
@@ -68,12 +68,7 @@ export const CheckoutBookModal = ({
   );
 
   return (
-    <Modal
-      show={open}
-      onHide={onClose}
-      style={{ backgroundColor: primaryRGBA }}
-      centered
-    >
+    <Modal show={open} onHide={onClose} centered>
       <ModalHeader
         onClose={onClose}
         title={`${isChechout ? "Checkout" : "Return"} book '${title}'`}
@@ -87,9 +82,7 @@ export const CheckoutBookModal = ({
       />
       <Modal.Body
         style={{
-          border: "2px solid black",
           borderTop: "none",
-          backgroundColor: primary,
         }}
       >
         <Alert className='w-80 m-auto my-4' variant='warning'>
@@ -187,9 +180,6 @@ export const CheckoutBookModal = ({
             <Button
               style={{
                 borderRadius: "5px",
-                backgroundColor: secondary,
-                color: "black",
-                border: "2px solid black",
                 marginLeft: "auto",
                 marginRight: "10px",
               }}
diff --git a/frontend/src/shared/components/modals/DeleteBookModal.tsx b/frontend/src/shared/components/modals/DeleteBookModal.tsx
index e352705..eada158 100644
--- a/frontend/src/shared/components/modals/DeleteBookModal.tsx
+++ b/frontend/src/shared/components/modals/DeleteBookModal.tsx
@@ -1,5 +1,5 @@
 import { Button, Modal } from "react-bootstrap";
-import { primary, primaryRGBA, secondary } from "../../../colors";
+import { primary, secondary } from "../../../colors";
 import { ModalHeader } from "./ModalHeader";
 import { ImBin } from "react-icons/im";
 import { useMutation } from "@tanstack/react-query";
@@ -24,12 +24,7 @@ export const DeleteBookModal = ({
   });
 
   return (
-    <Modal
-      show={open}
-      onHide={onClose}
-      style={{ backgroundColor: primaryRGBA }}
-      centered
-    >
+    <Modal show={open} onHide={onClose} centered>
       <ModalHeader
         onClose={onClose}
         title={"Move to Shelf"}
@@ -37,18 +32,13 @@ export const DeleteBookModal = ({
       />
       <Modal.Body
         style={{
-          border: "2px solid black",
           borderTop: "none",
-          backgroundColor: primary,
         }}
       >
         <div className='d-flex mx-auto mb-auto mt-2 w-100'>
           <Button
             style={{
               borderRadius: "5px",
-              backgroundColor: secondary,
-              color: "black",
-              border: "2px solid black",
               marginLeft: "auto",
               marginRight: "10px",
             }}
@@ -59,9 +49,6 @@ export const DeleteBookModal = ({
           <Button
             style={{
               borderRadius: "5px",
-              backgroundColor: secondary,
-              color: "black",
-              border: "2px solid black",
               marginLeft: "auto",
               marginRight: "10px",
             }}
diff --git a/frontend/src/shared/components/modals/ModalHeader.tsx b/frontend/src/shared/components/modals/ModalHeader.tsx
index 9b681d4..115ab79 100644
--- a/frontend/src/shared/components/modals/ModalHeader.tsx
+++ b/frontend/src/shared/components/modals/ModalHeader.tsx
@@ -13,13 +13,7 @@ export const ModalHeader = ({
   icon?: JSX.Element;
 }): React.JSX.Element => {
   return (
-    <Modal.Header
-      style={{
-        border: "2px solid black",
-        borderBottom: "none",
-        backgroundColor: primary,
-      }}
-    >
+    <Modal.Header>
       <Modal.Title className='w-100 d-flex' style={{ height: "fit-content" }}>
         {typeof title === "string" ? (
           <>
@@ -34,9 +28,6 @@ export const ModalHeader = ({
         <Button
           className='ml-auto mr-0'
           style={{
-            backgroundColor: secondary,
-            color: "black",
-            border: "2px solid black",
             width: "fit-content",
             height: "fit-content",
           }}
diff --git a/frontend/src/shared/components/modals/ScannerModal.tsx b/frontend/src/shared/components/modals/ScannerModal.tsx
index a5bdd25..488ee0b 100644
--- a/frontend/src/shared/components/modals/ScannerModal.tsx
+++ b/frontend/src/shared/components/modals/ScannerModal.tsx
@@ -1,7 +1,6 @@
-import React from "react";
+import React, { useContext } from "react";
 import { Modal } from "react-bootstrap";
 import { ImCamera } from "react-icons/im";
-import { primaryRGBA } from "../../../colors";
 import { useScanner } from "../../utils/useScanner";
 import { ModalHeader } from "./ModalHeader";
 
@@ -17,18 +16,13 @@ export const ScannerModal = ({
   const { scannerError, setScannerRef } = useScanner({ onDetected });
 
   return (
-    <Modal
-      show={open}
-      onHide={onClose}
-      style={{ backgroundColor: primaryRGBA }}
-      centered
-    >
+    <Modal show={open} onHide={onClose} centered>
       <ModalHeader
         onClose={onClose}
         title={"Scan barcode"}
         icon={<ImCamera size={50} className='ml-0 mr-auto' />}
       />
-      <Modal.Body style={{ border: "2px solid black", borderTop: "none" }}>
+      <Modal.Body>
         <div
           className='w-100 overflow-hidden'
           ref={(ref) => setScannerRef(ref)}
@@ -41,7 +35,7 @@ export const ScannerModal = ({
             className='drawingBuffer w-100 position-absolute'
             style={{
               height: "100%",
-              border: "2px solid black",
+              border: `2px solid black`,
             }}
           />
         </div>