Compare commits

..
15 Commits
38 changed files with 1431 additions and 611 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 536 KiB

After

Width:  |  Height:  |  Size: 912 KiB

+24 -15
View File
@@ -52,7 +52,7 @@ Clone this repository by running:
git clone --branch v0.1 --single-branch https://github.com/theis-js/stockhome.git git clone --branch v0.1 --single-branch https://github.com/theis-js/stockhome.git
``` ```
You can replace `v0.1` with the latest release tag to get the latest version of the stack. You can also use `dev` to get You can replace `v0.1` with any other release tag to get your desired version of the repo. You can also use `dev` to get
the latest development version, but keep in mind that it may contain bugs and errors. the latest development version, but keep in mind that it may contain bugs and errors.
View the available version tags here: [Releases](https://github.com/theis-js/stockhome/tags) View the available version tags here: [Releases](https://github.com/theis-js/stockhome/tags)
@@ -69,7 +69,7 @@ AUTH_SIGNATURE=
BACKEND_HOST=stockhome-backend BACKEND_HOST=stockhome-backend
``` ```
Make sure that you have set an secure root password and a secure signature. Make sure that you have set a secure root password and a secure signature.
> **Note** These three values cannot contain special characters. > **Note** These three values cannot contain special characters.
@@ -87,7 +87,7 @@ First, navigate into the root directory of this repository and run:
docker compose up -d --build docker compose up -d --build
``` ```
The database and all necessary services are started and initialised automatically. The database and all necessary services are started and initialized automatically.
### 5. First login ### 5. First login
@@ -98,6 +98,12 @@ Username: admin
Password: admin Password: admin
``` ```
The docker config exposes the frontend on port `80`. Therefore, you can access the web page either on
`http://localhost`, if you hosted it on your local machine, or under the IP-Adress or hostname of your server.
If port `80` is occupied, you can change the docker-compose. Therefore, take a look
at [Change Port Config](.github/docs/docker/README.md#ports).
_Keep in mind that you should change the password later in the settings page._ _Keep in mind that you should change the password later in the settings page._
> **Note** If errors occur get yourself some help in the [error code page](./.github/docs/error/README.md). > **Note** If errors occur get yourself some help in the [error code page](./.github/docs/error/README.md).
@@ -109,22 +115,25 @@ repository! [Report a bug](https://github.com/theis-js/stockhome/issues/new/choo
## Update ## Update
To update stockhome to the latest version, navigate into the root directory of this repository and run: To update Stockhome to the latest version, navigate into the root directory of this repository and run:
```shell
git fetch --all --tags && git checkout "$(git describe --tags "$(git rev-list --tags --max-count=1)")"
```
To rebuild the containers run:
```shell ```shell
git pull
latesttag=$(git describe --tags)
git checkout ${latesttag}
docker compose up -d --build docker compose up -d --build
``` ```
To update to a specific version, replace `latesttag` with the version tag you want to update to. You can view the To update/downgrade to a specific version, run the command below. You can replace `v0.1` with the tag/version you want.
available version tags here: [Releases](https://github.com/theis-js/stockhome/tags) You can find all Releases her: [Releases](https://github.com/theis-js/stockhome/tags).
**Example to downgrade/update to version `v0.1`:** **Example to downgrade/update to version `v0.1`:**
```shell ```shell
git pull git fetch --all
git checkout v0.1 git checkout v0.1
docker compose up -d --build docker compose up -d --build
``` ```
@@ -137,20 +146,20 @@ docker compose up -d --build
- Visual Studio Code (or any other IDE) - Visual Studio Code (or any other IDE)
- Cloned the repository - Cloned the repository
### Start backend and database ### Initialize repository dependencies
First, navigate into the root directory of this repository and run: First, navigate into the root directory of this repository and run:
```shell ```shell
docker compose -f docker-compose.dev.yml up -d --build npm install
``` ```
### Start frontend ### Start development environment
Navigate into the `frontend` directory and run: Stay in the root directory and run:
```shell ```shell
npm install npm run build:shared
npm run dev npm run dev
``` ```
+140 -104
View File
@@ -1,133 +1,169 @@
-- ============================================================= -- =============================================================
-- STOCKHOME MOCK DATA -- STOCKHOME MOCK DATA (EN)
-- Generates: 5 Storage Locations, ~100 Products -- Generates: 5 Storage Locations, 94 Products
--
-- Safe to re-run: storage locations are upserted by name, and
-- products reference the locations by looked-up UUID rather
-- than a hardcoded literal. This avoids the FK error 1452 that
-- occurs when a location with the same name already exists
-- (storage_locations.name is UNIQUE) with a different UUID.
--
-- Uncomment the DELETE below if you want a clean product table.
-- ============================================================= -- =============================================================
USE stockhome; USE stockhome;
-- Optional clean slate for products only (keeps user/app settings):
-- DELETE FROM products;
-- ============================================= -- =============================================
-- Storage Locations (5) -- Storage Locations (5)
-- ============================================= -- =============================================
INSERT INTO storage_locations (uuid, name, description) VALUES INSERT INTO storage_locations (uuid, name, description)
(UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), 'Main Freezer', 'Large chest freezer in the basement'), VALUES (UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), 'Main Freezer', 'Large chest freezer in the basement'),
(UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), 'Kitchen Freezer', 'Freezer compartment of the kitchen fridge'), (UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), 'Kitchen Freezer',
(UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), 'Pantry', 'Dry goods storage in the hallway'), 'Freezer compartment of the kitchen fridge'),
(UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), 'Kitchen Fridge', 'Main refrigerator in the kitchen'), (UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), 'Pantry', 'Dry goods storage in the hallway'),
(UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), 'Cellar Shelf', 'Cool storage shelves in the cellar'); (UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), 'Kitchen Fridge', 'Main refrigerator in the kitchen'),
(UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), 'Cellar Shelf', 'Cool storage shelves in the cellar')
ON DUPLICATE KEY UPDATE description = VALUES(description);
-- ============================================= -- =============================================
-- Products (~100) -- Resolve the actual UUIDs (works whether the rows were just
-- inserted or already existed with different UUIDs)
-- =============================================
SET @loc_main_freezer = (SELECT uuid
FROM storage_locations
WHERE name = 'Main Freezer');
SET @loc_kitchen_freezer = (SELECT uuid
FROM storage_locations
WHERE name = 'Kitchen Freezer');
SET @loc_pantry = (SELECT uuid
FROM storage_locations
WHERE name = 'Pantry');
SET @loc_kitchen_fridge = (SELECT uuid
FROM storage_locations
WHERE name = 'Kitchen Fridge');
SET @loc_cellar_shelf = (SELECT uuid
FROM storage_locations
WHERE name = 'Cellar Shelf');
-- =============================================
-- Products (94)
-- uuid, picture, created_at, updated_at and deleted rely on
-- their column defaults.
-- Self-frozen items have a bottling_date. -- Self-frozen items have a bottling_date.
-- Store-bought items have bottling_date = NULL. -- Store-bought items have bottling_date = NULL.
-- ============================================= -- =============================================
INSERT INTO products (uuid, name, description, price, amount, storage_location, expiry_date, bottling_date, picture, deleted) VALUES INSERT INTO products (name, description, price, amount, storage_location, expiry_date, bottling_date)
VALUES
-- ── Self-frozen items (Main Freezer) ── -- ── Self-frozen items (Main Freezer) ──
(UUID_TO_BIN(UUID()), 'Strawberry Puree', 'Blended fresh strawberries', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-12-15', '2026-06-01', NULL, FALSE), ('Strawberry Puree', 'Blended fresh strawberries', NULL, 4, @loc_main_freezer, '2026-12-15', '2026-06-01'),
(UUID_TO_BIN(UUID()), 'Tomato Sauce', 'Homemade marinara sauce', NULL, 6, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-01-10', '2026-05-20', NULL, FALSE), ('Tomato Sauce', 'Homemade marinara sauce', NULL, 6, @loc_main_freezer, '2027-01-10', '2026-05-20'),
(UUID_TO_BIN(UUID()), 'Chicken Broth', 'Slow-cooked bone broth', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-11-30', '2026-04-15', NULL, FALSE), ('Chicken Broth', 'Slow-cooked bone broth', NULL, 3, @loc_main_freezer, '2026-11-30', '2026-04-15'),
(UUID_TO_BIN(UUID()), 'Blanched Green Beans', 'Garden green beans, blanched', NULL, 5, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-02-01', '2026-05-28', NULL, FALSE), ('Blanched Green Beans', 'Garden green beans, blanched', NULL, 5, @loc_main_freezer, '2027-02-01', '2026-05-28'),
(UUID_TO_BIN(UUID()), 'Pesto Cubes', 'Basil pesto in ice cube trays', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-12-01', '2026-05-10', NULL, FALSE), ('Pesto Cubes', 'Basil pesto in ice cube trays', NULL, 2, @loc_main_freezer, '2026-12-01', '2026-05-10'),
(UUID_TO_BIN(UUID()), 'Raspberry Jam', 'Homemade seedless raspberry jam', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-03-01', '2026-06-02', NULL, FALSE), ('Raspberry Jam', 'Homemade seedless raspberry jam', NULL, 3, @loc_main_freezer, '2027-03-01', '2026-06-02'),
(UUID_TO_BIN(UUID()), 'Beef Stew', 'Leftover beef stew, portioned', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-10-15', '2026-04-20', NULL, FALSE), ('Beef Stew', 'Leftover beef stew, portioned', NULL, 4, @loc_main_freezer, '2026-10-15', '2026-04-20'),
(UUID_TO_BIN(UUID()), 'Blanched Spinach', 'Fresh spinach, blanched and pressed', NULL, 6, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-01-20', '2026-05-15', NULL, FALSE), ('Blanched Spinach', 'Fresh spinach, blanched and pressed', NULL, 6, @loc_main_freezer, '2027-01-20', '2026-05-15'),
(UUID_TO_BIN(UUID()), 'Mango Chunks', 'Diced ripe mangoes', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-12-20', '2026-05-25', NULL, FALSE), ('Mango Chunks', 'Diced ripe mangoes', NULL, 3, @loc_main_freezer, '2026-12-20', '2026-05-25'),
(UUID_TO_BIN(UUID()), 'Carrot Soup', 'Pureed carrot ginger soup', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-11-01', '2026-04-10', NULL, FALSE), ('Carrot Soup', 'Pureed carrot ginger soup', NULL, 4, @loc_main_freezer, '2026-11-01', '2026-04-10'),
-- ── Self-frozen items (Kitchen Freezer) ── -- ── Self-frozen items (Kitchen Freezer) ──
(UUID_TO_BIN(UUID()), 'Blueberry Mix', 'Mixed blueberries from the garden', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2026-12-10', '2026-06-03', NULL, FALSE), ('Blueberry Mix', 'Mixed blueberries from the garden', NULL, 2, @loc_kitchen_freezer, '2026-12-10', '2026-06-03'),
(UUID_TO_BIN(UUID()), 'Zucchini Slices', 'Sliced and blanched zucchini', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-01-15', '2026-05-18', NULL, FALSE), ('Zucchini Slices', 'Sliced and blanched zucchini', NULL, 3, @loc_kitchen_freezer, '2027-01-15', '2026-05-18'),
(UUID_TO_BIN(UUID()), 'Apple Sauce', 'Homemade cinnamon apple sauce', NULL, 5, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-02-10', '2026-05-22', NULL, FALSE), ('Apple Sauce', 'Homemade cinnamon apple sauce', NULL, 5, @loc_kitchen_freezer, '2027-02-10', '2026-05-22'),
(UUID_TO_BIN(UUID()), 'Bolognese Sauce', 'Classic meat sauce, portioned', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2026-11-20', '2026-04-25', NULL, FALSE), ('Bolognese Sauce', 'Classic meat sauce, portioned', NULL, 3, @loc_kitchen_freezer, '2026-11-20', '2026-04-25'),
(UUID_TO_BIN(UUID()), 'Herb Butter', 'Parsley and garlic butter logs', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2026-12-30', '2026-05-30', NULL, FALSE), ('Herb Butter', 'Parsley and garlic butter logs', NULL, 4, @loc_kitchen_freezer, '2026-12-30', '2026-05-30'),
(UUID_TO_BIN(UUID()), 'Peach Slices', 'Frozen ripe peach slices', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-01-05', '2026-06-01', NULL, FALSE), ('Peach Slices', 'Frozen ripe peach slices', NULL, 2, @loc_kitchen_freezer, '2027-01-05', '2026-06-01'),
(UUID_TO_BIN(UUID()), 'Corn Kernels', 'Fresh sweet corn, cut off the cob', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-02-20', '2026-05-12', NULL, FALSE), ('Corn Kernels', 'Fresh sweet corn, cut off the cob', NULL, 3, @loc_kitchen_freezer, '2027-02-20', '2026-05-12'),
(UUID_TO_BIN(UUID()), 'Pumpkin Puree', 'Roasted and pureed butternut squash', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-03-10', '2026-05-08', NULL, FALSE), ('Pumpkin Puree', 'Roasted and pureed butternut squash', NULL, 4, @loc_kitchen_freezer, '2027-03-10', '2026-05-08'),
(UUID_TO_BIN(UUID()), 'Lemon Juice Cubes', 'Fresh lemon juice frozen in cubes', NULL, 6, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-04-01', '2026-06-02', NULL, FALSE), ('Lemon Juice Cubes', 'Fresh lemon juice frozen in cubes', NULL, 6, @loc_kitchen_freezer, '2027-04-01', '2026-06-02'),
(UUID_TO_BIN(UUID()), 'Minced Garlic', 'Peeled and minced garlic cloves', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-01-25', '2026-05-14', NULL, FALSE), ('Minced Garlic', 'Peeled and minced garlic cloves', NULL, 2, @loc_kitchen_freezer, '2027-01-25', '2026-05-14'),
-- ── Self-frozen items (split across storages) ── -- ── Self-frozen items (split across storages) ──
(UUID_TO_BIN(UUID()), 'Vegetable Stock', 'Homemade veggie stock', NULL, 5, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-12-05', '2026-04-30', NULL, FALSE), ('Vegetable Stock', 'Homemade veggie stock', NULL, 5, @loc_main_freezer, '2026-12-05', '2026-04-30'),
(UUID_TO_BIN(UUID()), 'Rhubarb Compote', 'Stewed rhubarb with sugar', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-01-30', '2026-05-05', NULL, FALSE), ('Rhubarb Compote', 'Stewed rhubarb with sugar', NULL, 3, @loc_main_freezer, '2027-01-30', '2026-05-05'),
(UUID_TO_BIN(UUID()), 'Cherry Halves', 'Pitted sour cherries', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-12-25', '2026-06-01', NULL, FALSE), ('Cherry Halves', 'Pitted sour cherries', NULL, 2, @loc_main_freezer, '2026-12-25', '2026-06-01'),
(UUID_TO_BIN(UUID()), 'Cauliflower Florets', 'Blanched cauliflower pieces', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-02-15', '2026-05-19', NULL, FALSE), ('Cauliflower Florets', 'Blanched cauliflower pieces', NULL, 4, @loc_kitchen_freezer, '2027-02-15', '2026-05-19'),
(UUID_TO_BIN(UUID()), 'Plum Jam', 'Homemade plum jam', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-03-20', '2026-05-27', NULL, FALSE), ('Plum Jam', 'Homemade plum jam', NULL, 3, @loc_main_freezer, '2027-03-20', '2026-05-27'),
(UUID_TO_BIN(UUID()), 'Mushroom Duxelles', 'Finely chopped sauteed mushrooms', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2026-11-10', '2026-04-18', NULL, FALSE), ('Mushroom Duxelles', 'Finely chopped sauteed mushrooms', NULL, 2, @loc_kitchen_freezer, '2026-11-10', '2026-04-18'),
(UUID_TO_BIN(UUID()), 'Chili Con Carne', 'Portioned homemade chili', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-10-30', '2026-04-12', NULL, FALSE), ('Chili Con Carne', 'Portioned homemade chili', NULL, 3, @loc_main_freezer, '2026-10-30', '2026-04-12'),
(UUID_TO_BIN(UUID()), 'Banana Slices', 'Ripe banana slices for smoothies', NULL, 5, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2026-12-08', '2026-06-03', NULL, FALSE), ('Banana Slices', 'Ripe banana slices for smoothies', NULL, 5, @loc_kitchen_freezer, '2026-12-08', '2026-06-03'),
(UUID_TO_BIN(UUID()), 'Parsley Cubes', 'Chopped parsley in olive oil cubes', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-04-15', '2026-05-31', NULL, FALSE), ('Parsley Cubes', 'Chopped parsley in olive oil cubes', NULL, 4, @loc_kitchen_freezer, '2027-04-15', '2026-05-31'),
(UUID_TO_BIN(UUID()), 'Lentil Soup', 'Thick homemade lentil soup', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-11-25', '2026-04-22', NULL, FALSE), ('Lentil Soup', 'Thick homemade lentil soup', NULL, 3, @loc_main_freezer, '2026-11-25', '2026-04-22'),
(UUID_TO_BIN(UUID()), 'Blackberry Puree', 'Strained wild blackberries', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-01-12', '2026-06-02', NULL, FALSE), ('Blackberry Puree', 'Strained wild blackberries', NULL, 2, @loc_main_freezer, '2027-01-12', '2026-06-02'),
(UUID_TO_BIN(UUID()), 'Roasted Bell Peppers', 'Peeled roasted red peppers', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-02-28', '2026-05-16', NULL, FALSE), ('Roasted Bell Peppers', 'Peeled roasted red peppers', NULL, 3, @loc_kitchen_freezer, '2027-02-28', '2026-05-16'),
(UUID_TO_BIN(UUID()), 'Goulash', 'Hungarian-style beef goulash', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-10-20', '2026-04-08', NULL, FALSE), ('Goulash', 'Hungarian-style beef goulash', NULL, 2, @loc_main_freezer, '2026-10-20', '2026-04-08'),
(UUID_TO_BIN(UUID()), 'Mixed Berry Smoothie Pack', 'Pre-portioned smoothie mix', NULL, 6, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2026-12-18', '2026-06-04', NULL, FALSE), ('Mixed Berry Smoothie Pack', 'Pre-portioned smoothie mix', NULL, 6, @loc_kitchen_freezer, '2026-12-18', '2026-06-04'),
(UUID_TO_BIN(UUID()), 'Leek Cream Soup', 'Pureed leek and potato soup', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-11-15', '2026-04-28', NULL, FALSE), ('Leek Cream Soup', 'Pureed leek and potato soup', NULL, 3, @loc_main_freezer, '2026-11-15', '2026-04-28'),
(UUID_TO_BIN(UUID()), 'Diced Onions', 'Pre-diced yellow onions', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-03-05', '2026-05-24', NULL, FALSE), ('Diced Onions', 'Pre-diced yellow onions', NULL, 4, @loc_kitchen_freezer, '2027-03-05', '2026-05-24'),
(UUID_TO_BIN(UUID()), 'Grape Juice Concentrate', 'Reduced grape juice for desserts', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-02-05', '2026-05-11', NULL, FALSE), ('Grape Juice Concentrate', 'Reduced grape juice for desserts', NULL, 2, @loc_main_freezer, '2027-02-05', '2026-05-11'),
(UUID_TO_BIN(UUID()), 'Currant Jelly', 'Red currant jelly', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-04-10', '2026-06-01', NULL, FALSE), ('Currant Jelly', 'Red currant jelly', NULL, 3, @loc_main_freezer, '2027-04-10', '2026-06-01'),
(UUID_TO_BIN(UUID()), 'Broccoli Florets', 'Blanched broccoli pieces', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-01-08', '2026-05-20', NULL, FALSE), ('Broccoli Florets', 'Blanched broccoli pieces', NULL, 4, @loc_kitchen_freezer, '2027-01-08', '2026-05-20'),
(UUID_TO_BIN(UUID()), 'Sweet Potato Mash', 'Roasted and mashed sweet potatoes', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-12-28', '2026-05-17', NULL, FALSE), ('Sweet Potato Mash', 'Roasted and mashed sweet potatoes', NULL, 3, @loc_main_freezer, '2026-12-28', '2026-05-17'),
(UUID_TO_BIN(UUID()), 'Apricot Halves', 'Frozen ripe apricot halves', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-02-12', '2026-06-03', NULL, FALSE), ('Apricot Halves', 'Frozen ripe apricot halves', NULL, 2, @loc_kitchen_freezer, '2027-02-12', '2026-06-03'),
(UUID_TO_BIN(UUID()), 'Minestrone', 'Chunky Italian vegetable soup', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-11-08', '2026-04-14', NULL, FALSE), ('Minestrone', 'Chunky Italian vegetable soup', NULL, 3, @loc_main_freezer, '2026-11-08', '2026-04-14'),
(UUID_TO_BIN(UUID()), 'Pea Puree', 'Fresh garden peas, pureed', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-03-15', '2026-05-26', NULL, FALSE), ('Pea Puree', 'Fresh garden peas, pureed', NULL, 4, @loc_kitchen_freezer, '2027-03-15', '2026-05-26'),
(UUID_TO_BIN(UUID()), 'Cranberry Sauce', 'Homemade whole berry cranberry sauce', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-01-18', '2026-05-09', NULL, FALSE), ('Cranberry Sauce', 'Homemade whole berry cranberry sauce', NULL, 2, @loc_main_freezer, '2027-01-18', '2026-05-09'),
(UUID_TO_BIN(UUID()), 'Egg Noodles', 'Homemade egg noodles, pre-cooked', NULL, 5, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2026-12-12', '2026-05-29', NULL, FALSE), ('Egg Noodles', 'Homemade egg noodles, pre-cooked', NULL, 5, @loc_kitchen_freezer, '2026-12-12', '2026-05-29'),
(UUID_TO_BIN(UUID()), 'Gooseberry Compote', 'Stewed gooseberries', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-02-22', '2026-06-02', NULL, FALSE), ('Gooseberry Compote', 'Stewed gooseberries', NULL, 2, @loc_main_freezer, '2027-02-22', '2026-06-02'),
(UUID_TO_BIN(UUID()), 'Creamed Corn', 'Homemade creamed corn', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-01-28', '2026-05-21', NULL, FALSE), ('Creamed Corn', 'Homemade creamed corn', NULL, 3, @loc_kitchen_freezer, '2027-01-28', '2026-05-21'),
(UUID_TO_BIN(UUID()), 'Chicken Curry', 'Portioned Thai green curry', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-10-25', '2026-04-16', NULL, FALSE), ('Chicken Curry', 'Portioned Thai green curry', NULL, 2, @loc_main_freezer, '2026-10-25', '2026-04-16'),
(UUID_TO_BIN(UUID()), 'Dill Cubes', 'Chopped dill frozen in water cubes', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-04-20', '2026-06-01', NULL, FALSE), ('Dill Cubes', 'Chopped dill frozen in water cubes', NULL, 4, @loc_kitchen_freezer, '2027-04-20', '2026-06-01'),
-- ── Store-bought items (no bottling_date) ── -- ── Store-bought items (no bottling_date) ──
-- Kitchen Fridge -- Kitchen Fridge
(UUID_TO_BIN(UUID()), 'Whole Milk', 'Full-fat milk, 1 liter', '1.19', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-14', NULL, NULL, FALSE), ('Whole Milk', 'Full-fat milk, 1 liter', '1.19', 2, @loc_kitchen_fridge, '2026-06-14', NULL),
(UUID_TO_BIN(UUID()), 'Greek Yogurt', 'Plain Greek yogurt, 500g', '2.49', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-20', NULL, NULL, FALSE), ('Greek Yogurt', 'Plain Greek yogurt, 500g', '2.49', 3, @loc_kitchen_fridge, '2026-06-20', NULL),
(UUID_TO_BIN(UUID()), 'Butter', 'Unsalted butter, 250g', '2.29', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-07-15', NULL, NULL, FALSE), ('Butter', 'Unsalted butter, 250g', '2.29', 2, @loc_kitchen_fridge, '2026-07-15', NULL),
(UUID_TO_BIN(UUID()), 'Cheddar Cheese', 'Mature cheddar block, 400g', '3.49', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-08-10', NULL, NULL, FALSE), ('Cheddar Cheese', 'Mature cheddar block, 400g', '3.49', 1, @loc_kitchen_fridge, '2026-08-10', NULL),
(UUID_TO_BIN(UUID()), 'Cream Cheese', 'Philadelphia original, 200g', '1.89', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-25', NULL, NULL, FALSE), ('Cream Cheese', 'Philadelphia original, 200g', '1.89', 2, @loc_kitchen_fridge, '2026-06-25', NULL),
(UUID_TO_BIN(UUID()), 'Orange Juice', 'Fresh squeezed, 1 liter', '2.99', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-12', NULL, NULL, FALSE), ('Orange Juice', 'Fresh squeezed, 1 liter', '2.99', 1, @loc_kitchen_fridge, '2026-06-12', NULL),
(UUID_TO_BIN(UUID()), 'Eggs', 'Free-range eggs, pack of 10', '3.29', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-18', NULL, NULL, FALSE), ('Eggs', 'Free-range eggs, pack of 10', '3.29', 1, @loc_kitchen_fridge, '2026-06-18', NULL),
(UUID_TO_BIN(UUID()), 'Ham Slices', 'Cooked ham, 200g pack', '2.19', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-10', NULL, NULL, FALSE), ('Ham Slices', 'Cooked ham, 200g pack', '2.19', 2, @loc_kitchen_fridge, '2026-06-10', NULL),
(UUID_TO_BIN(UUID()), 'Mozzarella', 'Fresh mozzarella ball, 125g', '1.49', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-09', NULL, NULL, FALSE), ('Mozzarella', 'Fresh mozzarella ball, 125g', '1.49', 3, @loc_kitchen_fridge, '2026-06-09', NULL),
(UUID_TO_BIN(UUID()), 'Hummus', 'Classic chickpea hummus, 300g', '2.29', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-15', NULL, NULL, FALSE), ('Hummus', 'Classic chickpea hummus, 300g', '2.29', 1, @loc_kitchen_fridge, '2026-06-15', NULL),
-- Pantry -- Pantry
(UUID_TO_BIN(UUID()), 'Pasta Spaghetti', 'Durum wheat spaghetti, 500g', '1.29', 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-03-01', NULL, NULL, FALSE), ('Pasta Spaghetti', 'Durum wheat spaghetti, 500g', '1.29', 4, @loc_pantry, '2028-03-01', NULL),
(UUID_TO_BIN(UUID()), 'Basmati Rice', 'Long grain basmati, 1kg', '2.99', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-06-01', NULL, NULL, FALSE), ('Basmati Rice', 'Long grain basmati, 1kg', '2.99', 2, @loc_pantry, '2028-06-01', NULL),
(UUID_TO_BIN(UUID()), 'Canned Tomatoes', 'Chopped tomatoes, 400g tin', '0.89', 6, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-01-15', NULL, NULL, FALSE), ('Canned Tomatoes', 'Chopped tomatoes, 400g tin', '0.89', 6, @loc_pantry, '2028-01-15', NULL),
(UUID_TO_BIN(UUID()), 'Canned Chickpeas', 'Chickpeas in water, 400g', '0.99', 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-04-10', NULL, NULL, FALSE), ('Canned Chickpeas', 'Chickpeas in water, 400g', '0.99', 4, @loc_pantry, '2028-04-10', NULL),
(UUID_TO_BIN(UUID()), 'Olive Oil', 'Extra virgin olive oil, 500ml', '5.99', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2027-09-01', NULL, NULL, FALSE), ('Olive Oil', 'Extra virgin olive oil, 500ml', '5.99', 1, @loc_pantry, '2027-09-01', NULL),
(UUID_TO_BIN(UUID()), 'Peanut Butter', 'Crunchy peanut butter, 350g', '3.49', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2027-02-01', NULL, NULL, FALSE), ('Peanut Butter', 'Crunchy peanut butter, 350g', '3.49', 2, @loc_pantry, '2027-02-01', NULL),
(UUID_TO_BIN(UUID()), 'Honey', 'Raw wildflower honey, 500g', '6.99', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-12-01', NULL, NULL, FALSE), ('Honey', 'Raw wildflower honey, 500g', '6.99', 1, @loc_pantry, '2028-12-01', NULL),
(UUID_TO_BIN(UUID()), 'Oats', 'Rolled oats, 1kg', '1.99', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2027-08-01', NULL, NULL, FALSE), ('Oats', 'Rolled oats, 1kg', '1.99', 2, @loc_pantry, '2027-08-01', NULL),
(UUID_TO_BIN(UUID()), 'Flour', 'All-purpose wheat flour, 1kg', '0.99', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2027-06-01', NULL, NULL, FALSE), ('Flour', 'All-purpose wheat flour, 1kg', '0.99', 3, @loc_pantry, '2027-06-01', NULL),
(UUID_TO_BIN(UUID()), 'Sugar', 'White granulated sugar, 1kg', '1.49', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2029-01-01', NULL, NULL, FALSE), ('Sugar', 'White granulated sugar, 1kg', '1.49', 2, @loc_pantry, '2029-01-01', NULL),
(UUID_TO_BIN(UUID()), 'Canned Tuna', 'Tuna chunks in sunflower oil, 185g', '1.79', 5, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-07-01', NULL, NULL, FALSE), ('Canned Tuna', 'Tuna chunks in sunflower oil, 185g', '1.79', 5, @loc_pantry, '2028-07-01', NULL),
(UUID_TO_BIN(UUID()), 'Coconut Milk', 'Full-fat coconut milk, 400ml', '1.59', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-02-01', NULL, NULL, FALSE), ('Coconut Milk', 'Full-fat coconut milk, 400ml', '1.59', 3, @loc_pantry, '2028-02-01', NULL),
(UUID_TO_BIN(UUID()), 'Soy Sauce', 'Naturally brewed soy sauce, 250ml', '2.49', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-05-01', NULL, NULL, FALSE), ('Soy Sauce', 'Naturally brewed soy sauce, 250ml', '2.49', 1, @loc_pantry, '2028-05-01', NULL),
(UUID_TO_BIN(UUID()), 'Balsamic Vinegar', 'Aged balsamic vinegar, 250ml', '3.99', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2029-06-01', NULL, NULL, FALSE), ('Balsamic Vinegar', 'Aged balsamic vinegar, 250ml', '3.99', 1, @loc_pantry, '2029-06-01', NULL),
(UUID_TO_BIN(UUID()), 'Dried Lentils', 'Green lentils, 500g', '1.69', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-09-01', NULL, NULL, FALSE), ('Dried Lentils', 'Green lentils, 500g', '1.69', 2, @loc_pantry, '2028-09-01', NULL),
(UUID_TO_BIN(UUID()), 'Cornflakes', 'Classic cornflakes, 500g', '2.19', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2027-04-01', NULL, NULL, FALSE), ('Cornflakes', 'Classic cornflakes, 500g', '2.19', 1, @loc_pantry, '2027-04-01', NULL),
(UUID_TO_BIN(UUID()), 'Black Beans', 'Canned black beans, 400g', '1.09', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-08-01', NULL, NULL, FALSE), ('Black Beans', 'Canned black beans, 400g', '1.09', 3, @loc_pantry, '2028-08-01', NULL),
(UUID_TO_BIN(UUID()), 'Mustard', 'Dijon mustard, 200g jar', '1.89', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2027-11-01', NULL, NULL, FALSE), ('Mustard', 'Dijon mustard, 200g jar', '1.89', 1, @loc_pantry, '2027-11-01', NULL),
(UUID_TO_BIN(UUID()), 'Dried Pasta Penne', 'Penne rigate, 500g', '1.19', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-05-15', NULL, NULL, FALSE), ('Dried Pasta Penne', 'Penne rigate, 500g', '1.19', 3, @loc_pantry, '2028-05-15', NULL),
(UUID_TO_BIN(UUID()), 'Maple Syrup', 'Pure Canadian maple syrup, 250ml', '7.49', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-10-01', NULL, NULL, FALSE), ('Maple Syrup', 'Pure Canadian maple syrup, 250ml', '7.49', 1, @loc_pantry, '2028-10-01', NULL),
-- Cellar Shelf -- Cellar Shelf
(UUID_TO_BIN(UUID()), 'Canned Corn', 'Sweet corn kernels, 340g', '1.09', 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2028-03-15', NULL, NULL, FALSE), ('Canned Corn', 'Sweet corn kernels, 340g', '1.09', 4, @loc_cellar_shelf, '2028-03-15', NULL),
(UUID_TO_BIN(UUID()), 'Canned Peas', 'Garden peas, 400g tin', '0.89', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2028-06-10', NULL, NULL, FALSE), ('Canned Peas', 'Garden peas, 400g tin', '0.89', 3, @loc_cellar_shelf, '2028-06-10', NULL),
(UUID_TO_BIN(UUID()), 'Apple Cider Vinegar', 'Organic apple cider vinegar, 500ml', '3.29', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2028-11-01', NULL, NULL, FALSE), ('Apple Cider Vinegar', 'Organic apple cider vinegar, 500ml', '3.29', 2, @loc_cellar_shelf, '2028-11-01', NULL),
(UUID_TO_BIN(UUID()), 'Sunflower Oil', 'Refined sunflower oil, 1 liter', '2.49', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2027-10-01', NULL, NULL, FALSE), ('Sunflower Oil', 'Refined sunflower oil, 1 liter', '2.49', 1, @loc_cellar_shelf, '2027-10-01', NULL),
(UUID_TO_BIN(UUID()), 'Tomato Paste', 'Double concentrated, 200g tube', '1.29', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2028-01-01', NULL, NULL, FALSE), ('Tomato Paste', 'Double concentrated, 200g tube', '1.29', 3, @loc_cellar_shelf, '2028-01-01', NULL),
(UUID_TO_BIN(UUID()), 'Canned Sardines', 'Sardines in olive oil, 120g', '2.19', 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2028-09-15', NULL, NULL, FALSE), ('Canned Sardines', 'Sardines in olive oil, 120g', '2.19', 4, @loc_cellar_shelf, '2028-09-15', NULL),
(UUID_TO_BIN(UUID()), 'Pickled Cucumbers', 'Whole pickled gherkins, 670g jar', '1.99', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2027-12-01', NULL, NULL, FALSE), ('Pickled Cucumbers', 'Whole pickled gherkins, 670g jar', '1.99', 2, @loc_cellar_shelf, '2027-12-01', NULL),
(UUID_TO_BIN(UUID()), 'Jam Strawberry', 'Store-bought strawberry jam, 450g', '2.79', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2027-07-01', NULL, NULL, FALSE), ('Jam Strawberry', 'Store-bought strawberry jam, 450g', '2.79', 1, @loc_cellar_shelf, '2027-07-01', NULL),
(UUID_TO_BIN(UUID()), 'Canned Peaches', 'Peach halves in syrup, 420g', '1.69', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2028-04-01', NULL, NULL, FALSE), ('Canned Peaches', 'Peach halves in syrup, 420g', '1.69', 3, @loc_cellar_shelf, '2028-04-01', NULL),
(UUID_TO_BIN(UUID()), 'Crackers', 'Whole wheat crackers, 200g', '1.89', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2027-05-01', NULL, NULL, FALSE), ('Crackers', 'Whole wheat crackers, 200g', '1.89', 2, @loc_cellar_shelf, '2027-05-01', NULL),
-- Additional Kitchen Fridge items -- Additional Kitchen Fridge items
(UUID_TO_BIN(UUID()), 'Salami', 'Italian salami slices, 100g', '2.99', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-20', NULL, NULL, FALSE), ('Salami', 'Italian salami slices, 100g', '2.99', 1, @loc_kitchen_fridge, '2026-06-20', NULL),
(UUID_TO_BIN(UUID()), 'Parmesan', 'Parmigiano Reggiano wedge, 200g', '4.99', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-09-01', NULL, NULL, FALSE), ('Parmesan', 'Parmigiano Reggiano wedge, 200g', '4.99', 1, @loc_kitchen_fridge, '2026-09-01', NULL),
(UUID_TO_BIN(UUID()), 'Smoked Salmon', 'Sliced smoked salmon, 100g', '3.99', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-08', NULL, NULL, FALSE), ('Smoked Salmon', 'Sliced smoked salmon, 100g', '3.99', 1, @loc_kitchen_fridge, '2026-06-08', NULL),
(UUID_TO_BIN(UUID()), 'Heavy Cream', 'Whipping cream 35%, 200ml', '1.29', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-16', NULL, NULL, FALSE), ('Heavy Cream', 'Whipping cream 35%, 200ml', '1.29', 2, @loc_kitchen_fridge, '2026-06-16', NULL),
(UUID_TO_BIN(UUID()), 'Tofu', 'Firm organic tofu, 400g', '2.49', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-22', NULL, NULL, FALSE); ('Tofu', 'Firm organic tofu, 400g', '2.49', 1, @loc_kitchen_fridge, '2026-06-22', NULL);
-1
View File
@@ -30,7 +30,6 @@
"react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-i18next": "^17.0.8", "react-i18next": "^17.0.8",
"react-toastify": "^11.1.0",
"tailwindcss": "^4.3.0", "tailwindcss": "^4.3.0",
"validator": "^13.15.35", "validator": "^13.15.35",
"zod": "^4.4.3" "zod": "^4.4.3"
+6 -16
View File
@@ -1,9 +1,11 @@
import { createRouter, RouterProvider } from "@tanstack/react-router"; import { createRouter, RouterProvider } from "@tanstack/react-router";
import "./App.css"; import "./App.css";
import { ToastContainer } from "react-toastify";
import { routeTree } from "./routeTree.gen"; import { routeTree } from "./routeTree.gen";
import { NotFound } from "./components/NotFound.tsx";
import { CssVarsProvider } from "@mui/joy";
import { theme } from "./theme.ts";
const router = createRouter({ routeTree }); const router = createRouter({ routeTree, defaultNotFoundComponent: NotFound });
declare module "@tanstack/react-router" { declare module "@tanstack/react-router" {
interface Register { interface Register {
@@ -13,21 +15,9 @@ declare module "@tanstack/react-router" {
function App() { function App() {
return ( return (
<> <CssVarsProvider theme={theme} defaultMode={"system"}>
<ToastContainer
position="top-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick={false}
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="colored"
/>
<RouterProvider router={router} /> <RouterProvider router={router} />
</> </CssVarsProvider>
); );
} }
+53 -15
View File
@@ -3,7 +3,7 @@ import { Button, Input } from "@mui/joy";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { signInUser } from "../utils/api/auth"; import { signInUser } from "../utils/api/auth";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate, useSearch } from "@tanstack/react-router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import type { AlertInterface } from "../misc/interfaces"; import type { AlertInterface } from "../misc/interfaces";
import { MyAlert } from "./MyAlert.tsx"; import { MyAlert } from "./MyAlert.tsx";
@@ -11,6 +11,7 @@ import { MyAlert } from "./MyAlert.tsx";
export const LoginCard = () => { export const LoginCard = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const search: { loggedOut?: boolean } = useSearch({ from: "/login" });
const [alert, setAlert] = useState<AlertInterface>({ const [alert, setAlert] = useState<AlertInterface>({
isAlert: false, isAlert: false,
type: "neutral", type: "neutral",
@@ -19,12 +20,15 @@ export const LoginCard = () => {
}); });
useEffect(() => { useEffect(() => {
setAlert({ if (search.loggedOut) {
isAlert: true, setAlert({
type: "primary", isAlert: true,
header: t("success"), type: "primary",
text: t("logout-success-text"), header: t("success"),
}); text: t("logout-success-text"),
});
}
void navigate({ to: "/login" });
}, []); }, []);
const form = useForm({ const form = useForm({
@@ -53,7 +57,7 @@ export const LoginCard = () => {
header: "", header: "",
text: "", text: "",
}); });
navigate({ to: "/app/inventory" }); void navigate({ to: "/app/inventory" });
} }
}, },
onError: (error: unknown) => { onError: (error: unknown) => {
@@ -68,14 +72,34 @@ export const LoginCard = () => {
}); });
return ( return (
<div className="flex min-h-screen w-full items-center justify-center bg-[radial-gradient(1200px_circle_at_20%_10%,#e6f2ff_0%,#f7f9fc_40%,#f1f5fb_100%)] px-6 py-10"> <div
className="flex min-h-screen w-full items-center justify-center px-6 py-10"
style={{
background:
"radial-gradient(1200px circle at 20% 10%, var(--joy-palette-primary-100) 0%, var(--joy-palette-background-body) 40%, var(--joy-palette-background-level1) 100%)",
}}
>
<div className="mx-auto flex w-full max-w-4xl items-center justify-center"> <div className="mx-auto flex w-full max-w-4xl items-center justify-center">
<div className="w-full max-w-md rounded-3xl border border-white/70 bg-white/80 p-8 shadow-[0_24px_60px_rgba(12,38,78,0.18)] backdrop-blur"> <div
className="w-full max-w-md rounded-3xl p-8 backdrop-blur"
style={{
border: "1px solid var(--joy-palette-divider)",
backgroundColor: "var(--joy-palette-background-surface)",
boxShadow:
"0 24px 60px color-mix(in srgb, var(--joy-palette-primary-800) 18%, transparent)",
}}
>
<div className="mb-8 space-y-2"> <div className="mb-8 space-y-2">
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-[#0b6bcb]"> <p
className="text-xs font-semibold uppercase tracking-[0.3em]"
style={{ color: "var(--joy-palette-primary-solidBg)" }}
>
Stockhome Stockhome
</p> </p>
<h1 className="text-3xl font-semibold text-slate-900"> <h1
className="text-3xl font-semibold"
style={{ color: "var(--joy-palette-text-primary)" }}
>
{t("login")} {t("login")}
</h1> </h1>
</div> </div>
@@ -101,7 +125,11 @@ export const LoginCard = () => {
placeholder={t("username")} placeholder={t("username")}
variant="outlined" variant="outlined"
size="lg" size="lg"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/> />
)} )}
</form.Field> </form.Field>
@@ -114,7 +142,11 @@ export const LoginCard = () => {
placeholder={t("password")} placeholder={t("password")}
variant="outlined" variant="outlined"
size="lg" size="lg"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/> />
)} )}
</form.Field> </form.Field>
@@ -122,7 +154,13 @@ export const LoginCard = () => {
type="submit" type="submit"
loading={isPending} loading={isPending}
size="lg" size="lg"
className="w-full rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]" color="primary"
variant="solid"
className="w-full rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
> >
{t("login")} {t("login")}
</Button> </Button>
+20
View File
@@ -0,0 +1,20 @@
import { Typography } from "@mui/joy";
import { useTranslation } from "react-i18next";
export const NotFound = () => {
const { t } = useTranslation();
return (
<div className={"flex flex-col items-center gap-2"}>
<Typography color={"primary"} level={"h1"}>
{t("not-found-header")}
</Typography>
<Typography
sx={{ color: "var(--joy-palette-text-primary)" }}
level={"body-lg"}
>
{t("not-found-body")}
</Typography>
</div>
);
};
+76 -15
View File
@@ -7,42 +7,59 @@ import StorageIcon from "@mui/icons-material/Storage";
import SettingsIcon from "@mui/icons-material/Settings"; import SettingsIcon from "@mui/icons-material/Settings";
import ExitToAppIcon from "@mui/icons-material/ExitToApp"; import ExitToAppIcon from "@mui/icons-material/ExitToApp";
import TranslateIcon from "@mui/icons-material/Translate"; import TranslateIcon from "@mui/icons-material/Translate";
import Brightness4Icon from "@mui/icons-material/Brightness4";
import MenuIcon from "@mui/icons-material/Menu"; import MenuIcon from "@mui/icons-material/Menu";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import { useMatchRoute, useNavigate } from "@tanstack/react-router"; import { useMatchRoute, useNavigate } from "@tanstack/react-router";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { changeTranslation } from "../utils/uxFncs"; import { changeTranslation } from "../utils/uxFncs";
import { useColorScheme } from "@mui/joy/styles";
import { useLogout } from "../hooks/useLogout.ts";
export const Sidebar = () => { export const Sidebar = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { logout } = useLogout();
const matchRoute = useMatchRoute(); const matchRoute = useMatchRoute();
const { mode, setMode } = useColorScheme();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const btnClass = const btnClass =
"h-11 w-full justify-start! rounded-2xl px-4 text-left text-sm font-semibold text-slate-700 transition hover:bg-white/80 hover:text-[#0b6bcb] [&_.MuiButton-startDecorator]:mr-3! [&_.MuiButton-startDecorator]:ml-0!"; "h-11 w-full justify-start! rounded-2xl px-4 text-left text-sm font-semibold transition [&_.MuiButton-startDecorator]:mr-3! [&_.MuiButton-startDecorator]:ml-0!";
const variant = (to: string) => const variant = (to: string) =>
!!matchRoute({ to, fuzzy: false }) ? "soft" : "plain"; !!matchRoute({ to, fuzzy: false }) ? "soft" : "plain";
const handleNavigate = (to: string) => { const handleNavigate = (to: string) => {
navigate({ to }); void navigate({ to });
setIsOpen(false); setIsOpen(false);
}; };
return ( return (
<aside className="flex w-full flex-col gap-6 border-b border-white/80 bg-linear-to-b from-[#f7fbff] via-[#f2f6fb] to-[#eef3f9] px-4 py-5 shadow-[0_20px_60px_rgba(11,107,203,0.08)] sm:px-5 lg:h-full lg:min-h-screen lg:max-w-70 lg:border-b-0 lg:border-r lg:px-6 lg:py-8"> <aside
className="flex w-full flex-col gap-6 px-4 py-5 sm:px-5 lg:h-full lg:min-h-screen lg:max-w-70 lg:border-b-0 lg:border-r lg:px-6 lg:py-8"
style={{
borderBottom: "1px solid var(--joy-palette-divider)",
borderRightColor: "var(--joy-palette-divider)",
background:
"linear-gradient(to bottom, var(--joy-palette-primary-50), var(--joy-palette-background-level1), var(--joy-palette-background-level2))",
boxShadow:
"0 20px 60px color-mix(in srgb, var(--joy-palette-primary-solidBg) 8%, transparent)",
}}
>
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Typography <Typography
level="h2" level="h2"
className="text-[22px] font-semibold text-[#0b6bcb]" className="text-[22px] font-semibold"
sx={{ color: "var(--joy-palette-primary-solidBg)" }}
> >
{t("app-title")} {t("app-title")}
</Typography> </Typography>
<Typography <Typography
level="body-lg" level="body-lg"
className="text-sm font-medium text-slate-500" className="text-sm font-medium"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
> >
{Cookies.get("app-name") ? Cookies.get("app-name") : ""} {Cookies.get("app-name") ? Cookies.get("app-name") : ""}
</Typography> </Typography>
@@ -50,7 +67,14 @@ export const Sidebar = () => {
<Button <Button
variant="soft" variant="soft"
size="sm" size="sm"
className="transition-transform duration-200 ease-out" sx={{
display: "inline-flex",
"@media (min-width: 1024px)": { display: "none" },
"&:hover": {
backgroundColor: "var(--joy-palette-primary-900)",
color: "var(--joy-palette-primary-50)",
},
}}
onClick={() => setIsOpen((open) => !open)} onClick={() => setIsOpen((open) => !open)}
startDecorator={ startDecorator={
<span <span
@@ -61,10 +85,6 @@ export const Sidebar = () => {
{isOpen ? <CloseIcon /> : <MenuIcon />} {isOpen ? <CloseIcon /> : <MenuIcon />}
</span> </span>
} }
sx={{
display: "inline-flex",
"@media (min-width: 1024px)": { display: "none" },
}}
> >
{isOpen ? t("close") : t("menu")} {isOpen ? t("close") : t("menu")}
</Button> </Button>
@@ -83,6 +103,13 @@ export const Sidebar = () => {
variant={variant("/app/inventory")} variant={variant("/app/inventory")}
startDecorator={<InventoryIcon />} startDecorator={<InventoryIcon />}
className={btnClass} className={btnClass}
sx={{
color: "var(--joy-palette-text-secondary)",
"&:hover": {
bgcolor: "var(--joy-palette-background-surface)",
color: "var(--joy-palette-primary-solidBg)",
},
}}
> >
{t("inventory")} {t("inventory")}
</Button> </Button>
@@ -91,6 +118,13 @@ export const Sidebar = () => {
variant={variant("/app/add-product")} variant={variant("/app/add-product")}
startDecorator={<AddBoxIcon />} startDecorator={<AddBoxIcon />}
className={btnClass} className={btnClass}
sx={{
color: "var(--joy-palette-text-secondary)",
"&:hover": {
bgcolor: "var(--joy-palette-background-surface)",
color: "var(--joy-palette-primary-solidBg)",
},
}}
> >
{t("add")} {t("add")}
</Button> </Button>
@@ -99,6 +133,13 @@ export const Sidebar = () => {
variant={variant("/app/storages")} variant={variant("/app/storages")}
startDecorator={<StorageIcon />} startDecorator={<StorageIcon />}
className={btnClass} className={btnClass}
sx={{
color: "var(--joy-palette-text-secondary)",
"&:hover": {
bgcolor: "var(--joy-palette-background-surface)",
color: "var(--joy-palette-primary-solidBg)",
},
}}
> >
{t("storages")} {t("storages")}
</Button> </Button>
@@ -107,14 +148,18 @@ export const Sidebar = () => {
variant={variant("/app/app-settings")} variant={variant("/app/app-settings")}
startDecorator={<SettingsIcon />} startDecorator={<SettingsIcon />}
className={btnClass} className={btnClass}
sx={{
color: "var(--joy-palette-text-secondary)",
"&:hover": {
bgcolor: "var(--joy-palette-background-surface)",
color: "var(--joy-palette-primary-solidBg)",
},
}}
> >
{t("settings")} {t("settings")}
</Button> </Button>
<Button <Button
onClick={() => { onClick={logout}
Cookies.remove("token");
handleNavigate("/login?logout=true");
}}
color="danger" color="danger"
startDecorator={<ExitToAppIcon />} startDecorator={<ExitToAppIcon />}
className={btnClass} className={btnClass}
@@ -132,9 +177,25 @@ export const Sidebar = () => {
> >
{t("change-translation")} {t("change-translation")}
</Button> </Button>
<Button
onClick={() => setMode(mode === "dark" ? "light" : "dark")}
color="neutral"
startDecorator={<Brightness4Icon />}
className={btnClass}
>
{mode === "dark" ? "Light Mode" : "Dark Mode"}
</Button>
</div> </div>
<div className="flex items-center gap-3 rounded-2xl border border-white/70 bg-white/80 px-4 py-3 text-xs font-semibold uppercase tracking-[0.2em] text-[#0b6bcb] shadow-[0_12px_30px_rgba(12,38,78,0.12)]"> <div
className="flex items-center gap-3 rounded-2xl px-4 py-3 text-xs font-semibold uppercase tracking-[0.2em]"
style={{
border: "1px solid var(--joy-palette-divider)",
backgroundColor: "var(--joy-palette-background-surface)",
color: "var(--joy-palette-primary-solidBg)",
boxShadow: "0 12px 30px var(--joy-palette-divider)",
}}
>
<img <img
src="/favicon.png" src="/favicon.png"
alt="Stockhome" alt="Stockhome"
+21 -5
View File
@@ -50,7 +50,7 @@ export const StorageRow = ({ storage, onError }: StorageRowProps) => {
(values.description ?? "") !== (storage.description ?? ""); (values.description ?? "") !== (storage.description ?? "");
return ( return (
<tr key={storage.uuid} className="border-t border-slate-200 align-top"> <tr key={storage.uuid} className="align-top">
<td className="px-6 py-5"> <td className="px-6 py-5">
<form.Field name="name"> <form.Field name="name">
{(field) => ( {(field) => (
@@ -60,7 +60,12 @@ export const StorageRow = ({ storage, onError }: StorageRowProps) => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="sm" size="sm"
variant="outlined" variant="outlined"
className="rounded-xl bg-slate-50/80 text-slate-700 shadow-none ring-1 ring-inset ring-slate-200 focus-within:ring-2 focus-within:ring-slate-300" className="rounded-xl shadow-none"
sx={{
bgcolor: "var(--joy-palette-background-level1)",
color: "var(--joy-palette-text-secondary)",
"--Input-focusedHighlight": "var(--joy-palette-neutral-300)",
}}
/> />
)} )}
</form.Field> </form.Field>
@@ -74,15 +79,26 @@ export const StorageRow = ({ storage, onError }: StorageRowProps) => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="sm" size="sm"
variant="outlined" variant="outlined"
className="rounded-xl bg-slate-50/80 text-slate-700 shadow-none ring-1 ring-inset ring-slate-200 focus-within:ring-2 focus-within:ring-slate-300" className="rounded-xl shadow-none"
sx={{
bgcolor: "var(--joy-palette-background-level1)",
color: "var(--joy-palette-text-secondary)",
"--Input-focusedHighlight": "var(--joy-palette-neutral-300)",
}}
/> />
)} )}
</form.Field> </form.Field>
</td> </td>
<td className="px-6 py-5 text-sm text-slate-500"> <td
className="px-6 py-5 text-sm"
style={{ color: "var(--joy-palette-text-tertiary)" }}
>
{formatDate(storage.created_at)} {formatDate(storage.created_at)}
</td> </td>
<td className="px-6 py-5 text-sm text-slate-500"> <td
className="px-6 py-5 text-sm"
style={{ color: "var(--joy-palette-text-tertiary)" }}
>
{formatDate(storage.updated_at)} {formatDate(storage.updated_at)}
</td> </td>
<td className="px-6 py-5 text-right"> <td className="px-6 py-5 text-right">
@@ -51,11 +51,20 @@ export const AddStorageModal = (props: AddStorageModalProps) => {
return ( return (
<> <>
<Modal open={props.isOpen} onClose={() => props.setOpen(false)}> <Modal open={props.isOpen} onClose={() => props.setOpen(false)}>
<ModalDialog className="rounded-3xl border border-white/70 bg-white/90 p-6 shadow-[0_30px_70px_rgba(12,38,78,0.2)] backdrop-blur"> <ModalDialog
<DialogTitle className="text-slate-900"> className="rounded-3xl p-6 backdrop-blur"
sx={{
border: "1px solid",
borderColor: "divider",
bgcolor: "background.surface",
boxShadow:
"0 30px 70px color-mix(in srgb, var(--joy-palette-primary-800) 20%, transparent)",
}}
>
<DialogTitle sx={{ color: "var(--joy-palette-text-primary)" }}>
{t("new-storage-title")} {t("new-storage-title")}
</DialogTitle> </DialogTitle>
<DialogContent className="text-slate-500"> <DialogContent sx={{ color: "var(--joy-palette-text-tertiary)" }}>
{t("new-storage-content")} {t("new-storage-content")}
</DialogContent> </DialogContent>
<form <form
@@ -73,7 +82,11 @@ export const AddStorageModal = (props: AddStorageModalProps) => {
placeholder={t("storage-name")} placeholder={t("storage-name")}
variant="outlined" variant="outlined"
size="lg" size="lg"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/> />
)} )}
</form.Field> </form.Field>
@@ -85,14 +98,24 @@ export const AddStorageModal = (props: AddStorageModalProps) => {
placeholder={t("description")} placeholder={t("description")}
variant="outlined" variant="outlined"
size="lg" size="lg"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/> />
)} )}
</form.Field> </form.Field>
<Button <Button
type="submit" type="submit"
size="lg" size="lg"
className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]" color="primary"
variant="solid"
className="rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
> >
{t("submit")} {t("submit")}
</Button> </Button>
@@ -37,8 +37,6 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
newPasswordRep: "", newPasswordRep: "",
}, },
onSubmit: async ({ value }) => { onSubmit: async ({ value }) => {
console.log(value);
if ( if (
value.newPassword === value.newPasswordRep && value.newPassword === value.newPasswordRep &&
value.newPassword !== "" value.newPassword !== ""
@@ -66,7 +64,6 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
}); });
}, },
onSuccess: () => { onSuccess: () => {
// Sets the success alert in the settings page via component props
props.alert({ props.alert({
isAlert: true, isAlert: true,
type: "success", type: "success",
@@ -74,7 +71,6 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
text: t("SU005"), text: t("SU005"),
}); });
// Sets the success alert locally (in the modal itself)
setAlert({ setAlert({
isAlert: true, isAlert: true,
type: "success", type: "success",
@@ -88,8 +84,17 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
return ( return (
<> <>
<Modal open={props.isOpen} onClose={() => props.setOpen(false)}> <Modal open={props.isOpen} onClose={() => props.setOpen(false)}>
<ModalDialog className="rounded-3xl border border-white/70 bg-white/90 p-6 shadow-[0_30px_70px_rgba(12,38,78,0.2)] backdrop-blur"> <ModalDialog
<DialogTitle className="text-slate-900"> className="rounded-3xl p-6 backdrop-blur"
sx={{
border: "1px solid",
borderColor: "divider",
bgcolor: "background.surface",
boxShadow:
"0 30px 70px color-mix(in srgb, var(--joy-palette-primary-800) 20%, transparent)",
}}
>
<DialogTitle sx={{ color: "var(--joy-palette-text-primary)" }}>
{t("new-password-title")} {t("new-password-title")}
</DialogTitle> </DialogTitle>
<form <form
@@ -108,7 +113,11 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
placeholder={t("current-password")} placeholder={t("current-password")}
variant="outlined" variant="outlined"
size="lg" size="lg"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/> />
)} )}
</form.Field> </form.Field>
@@ -121,7 +130,11 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
placeholder={t("new-password")} placeholder={t("new-password")}
variant="outlined" variant="outlined"
size="lg" size="lg"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/> />
)} )}
</form.Field> </form.Field>
@@ -134,14 +147,24 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
placeholder={t("new-password-rep")} placeholder={t("new-password-rep")}
variant="outlined" variant="outlined"
size="lg" size="lg"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/> />
)} )}
</form.Field> </form.Field>
<Button <Button
type="submit" type="submit"
size="lg" size="lg"
className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]" color="primary"
variant="solid"
className="rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
> >
{t("change")} {t("change")}
</Button> </Button>
+13
View File
@@ -0,0 +1,13 @@
import Cookies from "js-cookie";
import { useNavigate } from "@tanstack/react-router";
export const useLogout = () => {
const navigate = useNavigate();
const logout = () => {
Cookies.remove("token");
void navigate({ to: "/login", search: { loggedOut: true } });
};
return { logout };
};
+104 -22
View File
@@ -90,10 +90,16 @@ export const AddProduct = () => {
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<div className="space-y-1"> <div className="space-y-1">
<Typography level="h2" className="text-slate-900"> <Typography
level="h2"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("add-product")} {t("add-product")}
</Typography> </Typography>
<Typography level="body-lg" className="text-slate-500"> <Typography
level="body-lg"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{t("add-product-subtitle")} {t("add-product-subtitle")}
</Typography> </Typography>
</div> </div>
@@ -106,7 +112,16 @@ export const AddProduct = () => {
</Chip> </Chip>
</div> </div>
</div> </div>
<Box className="mt-6 rounded-3xl border border-white/70 bg-white/80 p-6 shadow-[0_24px_60px_rgba(12,38,78,0.12)] backdrop-blur"> <Box
className="mt-6 rounded-3xl p-6 backdrop-blur"
sx={{
border: "1px solid",
borderColor: "divider",
bgcolor: "background.surface",
boxShadow:
"0 24px 60px color-mix(in srgb, var(--joy-palette-primary-800) 12%, transparent)",
}}
>
<form <form
className="space-y-6" className="space-y-6"
onSubmit={(e) => { onSubmit={(e) => {
@@ -117,7 +132,10 @@ export const AddProduct = () => {
<div className="grid gap-5 lg:grid-cols-[1.2fr_1fr]"> <div className="grid gap-5 lg:grid-cols-[1.2fr_1fr]">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("product-name")} {t("product-name")}
</Typography> </Typography>
<form.Field name="name"> <form.Field name="name">
@@ -130,13 +148,20 @@ export const AddProduct = () => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/> />
)} )}
</form.Field> </form.Field>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("description")} {t("description")}
</Typography> </Typography>
<form.Field name="description"> <form.Field name="description">
@@ -148,14 +173,21 @@ export const AddProduct = () => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/> />
)} )}
</form.Field> </form.Field>
</div> </div>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("expiry-date")} {t("expiry-date")}
</Typography> </Typography>
<form.Field name="expiry_date"> <form.Field name="expiry_date">
@@ -167,13 +199,19 @@ export const AddProduct = () => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/> />
)} )}
</form.Field> </form.Field>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("bottling-date")} {t("bottling-date")}
</Typography> </Typography>
<form.Field name="bottling_date"> <form.Field name="bottling_date">
@@ -185,7 +223,10 @@ export const AddProduct = () => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/> />
)} )}
</form.Field> </form.Field>
@@ -193,14 +234,28 @@ export const AddProduct = () => {
</div> </div>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<div className="rounded-2xl border border-white/70 bg-linear-to-br from-[#f7fbff] via-[#f2f6fb] to-[#eef3f9] p-5 shadow-[0_16px_40px_rgba(12,38,78,0.08)]"> <div
<Typography level="title-md" className="text-slate-900"> className="rounded-2xl p-5"
style={{
border: "1px solid var(--joy-palette-divider)",
background:
"linear-gradient(to bottom right, var(--joy-palette-primary-50), var(--joy-palette-background-level1), var(--joy-palette-background-level2))",
boxShadow: "0 16px 40px var(--joy-palette-divider)",
}}
>
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("inventory")} {t("inventory")}
</Typography> </Typography>
<Divider className="my-3" /> <Divider className="my-3" />
<div className="grid gap-4"> <div className="grid gap-4">
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("amount")} {t("amount")}
</Typography> </Typography>
<form.Field name="amount"> <form.Field name="amount">
@@ -220,13 +275,19 @@ export const AddProduct = () => {
); );
}} }}
onBlur={field.handleBlur} onBlur={field.handleBlur}
className="rounded-2xl bg-white/80" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/> />
)} )}
</form.Field> </form.Field>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("price")} {t("price")}
</Typography> </Typography>
<form.Field name="price"> <form.Field name="price">
@@ -238,16 +299,25 @@ export const AddProduct = () => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/> />
)} )}
</form.Field> </form.Field>
<Typography level="body-sm" className="text-slate-500"> <Typography
level="body-sm"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{Cookies.get("currency")} {Cookies.get("currency")}
</Typography> </Typography>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("storage-place")} {t("storage-place")}
</Typography> </Typography>
<form.Field name="storage_location_uuid"> <form.Field name="storage_location_uuid">
@@ -260,7 +330,10 @@ export const AddProduct = () => {
} }
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
> >
{storages?.map((storage) => ( {storages?.map((storage) => (
<Option key={storage.uuid} value={storage.uuid}> <Option key={storage.uuid} value={storage.uuid}>
@@ -276,7 +349,10 @@ export const AddProduct = () => {
</div> </div>
</div> </div>
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<Typography level="body-sm" className="text-slate-500"> <Typography
level="body-sm"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{t("product-details")} {t("product-details")}
</Typography> </Typography>
<div className="grow"></div> <div className="grow"></div>
@@ -284,7 +360,13 @@ export const AddProduct = () => {
type="submit" type="submit"
loading={isPending} loading={isPending}
size="lg" size="lg"
className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]" color="primary"
variant="solid"
className="rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
> >
{t("save")} {t("save")}
</Button> </Button>
+37 -18
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { ChangeEvent, MouseEvent, useEffect, useState } from "react";
import { Avatar, Button, Checkbox, Chip, CircularProgress, Sheet, Table, Typography, } from "@mui/joy"; import { Avatar, Button, Checkbox, Chip, CircularProgress, Sheet, Table, Typography, } from "@mui/joy";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -9,7 +9,7 @@ import { formatDate } from "../utils/uxFncs";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import type { AlertInterface, ProductRow } from "../misc/interfaces"; import type { AlertInterface, ProductRow } from "../misc/interfaces";
import type { ApiError } from "../utils/api/apiError"; import type { ApiError } from "../utils/api/apiError";
import CategoryIcon from "@mui/icons-material/category"; import CategoryIcon from "@mui/icons-material/Category";
import { MyAlert } from "../components/MyAlert.tsx"; import { MyAlert } from "../components/MyAlert.tsx";
export const InventoryPage = () => { export const InventoryPage = () => {
@@ -73,12 +73,12 @@ export const InventoryPage = () => {
mutationFn: (values: string[]) => deleteSelectedProducts(values), mutationFn: (values: string[]) => deleteSelectedProducts(values),
onSuccess: () => { onSuccess: () => {
setSelected([]); setSelected([]);
queryClient.invalidateQueries({ queryKey: ["products"] }); void queryClient.invalidateQueries({ queryKey: ["products"] });
}, },
onError: showError, onError: showError,
}); });
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => { const handleSelectAllClick = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) { if (event.target.checked) {
setSelected(rows.map((row) => row.id)); setSelected(rows.map((row) => row.id));
return; return;
@@ -86,7 +86,7 @@ export const InventoryPage = () => {
setSelected([]); setSelected([]);
}; };
const handleClick = (_event: React.MouseEvent<unknown>, id: string) => { const handleClick = (_event: MouseEvent<unknown>, id: string) => {
const selectedIndex = selected.indexOf(id); const selectedIndex = selected.indexOf(id);
let newSelected: readonly string[] = []; let newSelected: readonly string[] = [];
if (selectedIndex === -1) { if (selectedIndex === -1) {
@@ -124,9 +124,20 @@ export const InventoryPage = () => {
<Sheet <Sheet
variant="outlined" variant="outlined"
className="mt-6 flex min-h-0 w-full max-w-full flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white/80 shadow-sm sm:h-[calc(100vh-260px)]" className="mt-6 flex min-h-0 w-full max-w-full flex-col overflow-hidden rounded-2xl shadow-sm sm:h-[calc(100vh-260px)]"
sx={{
border: "1px solid",
borderColor: "divider",
bgcolor: "background.surface",
}}
> >
<div className="flex items-center justify-between border-b border-slate-200 px-6 py-4 text-slate-700"> <div
className="flex items-center justify-between px-6 py-4"
style={{
borderBottom: "1px solid var(--joy-palette-divider)",
color: "var(--joy-palette-text-secondary)",
}}
>
<Typography level="body-lg" fontWeight="bold"> <Typography level="body-lg" fontWeight="bold">
{t("inventory")} {t("inventory")}
</Typography> </Typography>
@@ -147,24 +158,26 @@ export const InventoryPage = () => {
stripe="odd" stripe="odd"
variant="plain" variant="plain"
hoverRow hoverRow
className="w-full text-slate-700" className="w-full"
sx={{ sx={{
tableLayout: "fixed", tableLayout: "fixed",
color: "var(--joy-palette-text-secondary)",
"--TableCell-headBackground": "--TableCell-headBackground":
"var(--joy-palette-background-surface)", "var(--joy-palette-background-level1)",
"& thead": { "& thead": {
position: "sticky", position: "sticky",
top: 0, top: 0,
zIndex: 3, zIndex: 3,
backgroundColor: "rgb(248 250 252)", backgroundColor: "var(--joy-palette-background-level1)",
}, },
"& thead tr": { "& thead tr": {
backgroundColor: "rgb(248 250 252)", backgroundColor: "var(--joy-palette-background-level1)",
}, },
"& thead th": { "& thead th": {
zIndex: 2, zIndex: 2,
backgroundColor: "rgb(248 250 252)", backgroundColor: "var(--joy-palette-background-level1)",
backgroundImage: "none", backgroundImage: "none",
color: "var(--joy-palette-text-secondary)",
}, },
"& thead th:nth-child(1)": { "& thead th:nth-child(1)": {
width: "44px", width: "44px",
@@ -198,10 +211,13 @@ export const InventoryPage = () => {
minWidth: "100px", minWidth: "100px",
}, },
"& tr > *:nth-child(n+4)": { textAlign: "left" }, "& tr > *:nth-child(n+4)": { textAlign: "left" },
"& tbody tr": {
borderTop: "1px solid var(--joy-palette-divider)",
},
}} }}
> >
<thead> <thead>
<tr className="text-slate-600"> <tr>
<th className="px-2 py-4"> <th className="px-2 py-4">
<Checkbox <Checkbox
checked={rows.length > 0 && selected.length === rows.length} checked={rows.length > 0 && selected.length === rows.length}
@@ -229,7 +245,6 @@ export const InventoryPage = () => {
return ( return (
<tr <tr
key={row.id} key={row.id}
className="border-t border-slate-200"
onClick={(event) => handleClick(event, row.id)} onClick={(event) => handleClick(event, row.id)}
role="checkbox" role="checkbox"
aria-checked={isItemSelected} aria-checked={isItemSelected}
@@ -254,13 +269,15 @@ export const InventoryPage = () => {
<div className="min-w-0"> <div className="min-w-0">
<Typography <Typography
level="title-md" level="title-md"
className="text-slate-900 truncate" className="truncate"
sx={{ color: "var(--joy-palette-text-primary)" }}
> >
{row.name} {row.name}
</Typography> </Typography>
<Typography <Typography
level="body-sm" level="body-sm"
className="text-slate-500 truncate" className="truncate"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
> >
{row.description} {row.description}
</Typography> </Typography>
@@ -273,7 +290,8 @@ export const InventoryPage = () => {
</Typography> </Typography>
<Typography <Typography
level="body-sm" level="body-sm"
className="truncate text-slate-400" className="truncate"
sx={{ color: "var(--joy-palette-neutral-400)" }}
> >
{Cookies.get("currency")} {Cookies.get("currency")}
</Typography> </Typography>
@@ -294,7 +312,8 @@ export const InventoryPage = () => {
</Typography> </Typography>
<Typography <Typography
level="body-sm" level="body-sm"
className="truncate text-slate-500" className="truncate"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
> >
{row.locationDetail} {row.locationDetail}
</Typography> </Typography>
+103 -38
View File
@@ -1,27 +1,12 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { getStorages } from "../utils/api/storages.ts"; import { getStorages } from "../utils/api/storages.ts";
import { import { Box, Button, Chip, CircularProgress, Divider, Input, Option, Select, Typography, } from "@mui/joy";
Box,
Button,
Chip,
CircularProgress,
Divider,
Input,
Option,
Select,
Typography,
} from "@mui/joy";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "@tanstack/react-form"; import { useForm } from "@tanstack/react-form";
import { getProductDetails, mutateProduct } from "../utils/api/products.ts"; import { getProductDetails, mutateProduct } from "../utils/api/products.ts";
import { toInputDate } from "../utils/uxFncs"; import { toInputDate } from "../utils/uxFncs";
import type { import type { AlertInterface, productDetailsInterface, ProductFormValues, Storage, } from "../misc/interfaces";
AlertInterface,
productDetailsInterface,
ProductFormValues,
Storage,
} from "../misc/interfaces";
import type { ApiError } from "../utils/api/apiError"; import type { ApiError } from "../utils/api/apiError";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import ElectricBoltIcon from "@mui/icons-material/ElectricBolt"; import ElectricBoltIcon from "@mui/icons-material/ElectricBolt";
@@ -140,7 +125,11 @@ export const ProductQuickView = () => {
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<div className="space-y-1"> <div className="space-y-1">
<Typography level="h2" fontWeight="1000" className="text-slate-900"> <Typography
level="h2"
fontWeight="1000"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
<ElectricBoltIcon color="primary" /> <ElectricBoltIcon color="primary" />
{t("product-details-quick")} {t("product-details-quick")}
<ElectricBoltIcon color="primary" /> <ElectricBoltIcon color="primary" />
@@ -157,7 +146,16 @@ export const ProductQuickView = () => {
{productDetailsLoading && <CircularProgress size="sm" />} {productDetailsLoading && <CircularProgress size="sm" />}
</div> </div>
{isSuccess && ( {isSuccess && (
<Box className="mt-6 rounded-3xl border border-white/70 bg-white/80 p-6 shadow-[0_24px_60px_rgba(12,38,78,0.12)] backdrop-blur"> <Box
className="mt-6 rounded-3xl p-6 backdrop-blur"
sx={{
border: "1px solid",
borderColor: "divider",
bgcolor: "background.surface",
boxShadow:
"0 24px 60px color-mix(in srgb, var(--joy-palette-primary-800) 12%, transparent)",
}}
>
<form <form
className="space-y-6" className="space-y-6"
onSubmit={(e) => { onSubmit={(e) => {
@@ -168,7 +166,10 @@ export const ProductQuickView = () => {
<div className="gap-5 lg:grid-cols-[1.2fr_1fr]"> <div className="gap-5 lg:grid-cols-[1.2fr_1fr]">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("product-name")} {t("product-name")}
</Typography> </Typography>
<form.Field name="name"> <form.Field name="name">
@@ -180,13 +181,20 @@ export const ProductQuickView = () => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/> />
)} )}
</form.Field> </form.Field>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("description")} {t("description")}
</Typography> </Typography>
<form.Field name="description"> <form.Field name="description">
@@ -198,14 +206,21 @@ export const ProductQuickView = () => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/> />
)} )}
</form.Field> </form.Field>
</div> </div>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("expiry-date")} {t("expiry-date")}
</Typography> </Typography>
<form.Field name="expiry_date"> <form.Field name="expiry_date">
@@ -217,13 +232,19 @@ export const ProductQuickView = () => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/> />
)} )}
</form.Field> </form.Field>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("bottling-date")} {t("bottling-date")}
</Typography> </Typography>
<form.Field name="bottling_date"> <form.Field name="bottling_date">
@@ -235,7 +256,10 @@ export const ProductQuickView = () => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/> />
)} )}
</form.Field> </form.Field>
@@ -243,14 +267,28 @@ export const ProductQuickView = () => {
</div> </div>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<div className="rounded-2xl border border-white/70 bg-linear-to-br from-[#f7fbff] via-[#f2f6fb] to-[#eef3f9] p-5 shadow-[0_16px_40px_rgba(12,38,78,0.08)]"> <div
<Typography level="title-lg" className="text-slate-900"> className="rounded-2xl p-5"
style={{
border: "1px solid var(--joy-palette-divider)",
background:
"linear-gradient(to bottom right, var(--joy-palette-primary-50), var(--joy-palette-background-level1), var(--joy-palette-background-level2))",
boxShadow: "0 16px 40px var(--joy-palette-divider)",
}}
>
<Typography
level="title-lg"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("inventory")} {t("inventory")}
</Typography> </Typography>
<Divider className="my-3" /> <Divider className="my-3" />
<div className="grid gap-4"> <div className="grid gap-4">
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("amount")} {t("amount")}
</Typography> </Typography>
<form.Field name="amount"> <form.Field name="amount">
@@ -270,13 +308,19 @@ export const ProductQuickView = () => {
); );
}} }}
onBlur={field.handleBlur} onBlur={field.handleBlur}
className="rounded-2xl bg-white/80" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/> />
)} )}
</form.Field> </form.Field>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("price")} {t("price")}
</Typography> </Typography>
<form.Field name="price"> <form.Field name="price">
@@ -288,16 +332,25 @@ export const ProductQuickView = () => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/> />
)} )}
</form.Field> </form.Field>
<Typography level="body-sm" className="text-slate-500"> <Typography
level="body-sm"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{Cookies.get("currency")} {Cookies.get("currency")}
</Typography> </Typography>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("storage-place")} {t("storage-place")}
</Typography> </Typography>
<form.Field name="storage_location_uuid"> <form.Field name="storage_location_uuid">
@@ -309,7 +362,10 @@ export const ProductQuickView = () => {
} }
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
> >
{storages?.map((storage) => ( {storages?.map((storage) => (
<Option key={storage.uuid} value={storage.uuid}> <Option key={storage.uuid} value={storage.uuid}>
@@ -325,14 +381,23 @@ export const ProductQuickView = () => {
</div> </div>
</div> </div>
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<Typography level="body-sm" className="text-slate-500"> <Typography
level="body-sm"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{t("product-details")} {t("product-details")}
</Typography> </Typography>
<Button <Button
type="submit" type="submit"
loading={isPending} loading={isPending}
size="lg" size="lg"
className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]" color="primary"
variant="solid"
className="rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
> >
{t("save")} {t("save")}
</Button> </Button>
+74 -17
View File
@@ -91,10 +91,16 @@ export const Settings = () => {
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<div className="space-y-1"> <div className="space-y-1">
<Typography level="h2" className="text-slate-900"> <Typography
level="h2"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("settings")} {t("settings")}
</Typography> </Typography>
<Typography level="body-lg" className="text-slate-500"> <Typography
level="body-lg"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{t("settings-sub")} {t("settings-sub")}
</Typography> </Typography>
</div> </div>
@@ -108,7 +114,16 @@ export const Settings = () => {
</div> </div>
</div> </div>
<Sheet className="mt-6 rounded-3xl border border-white/70 bg-white/80 p-6 shadow-[0_24px_60px_rgba(12,38,78,0.12)] backdrop-blur"> <Sheet
className="mt-6 rounded-3xl p-6 backdrop-blur"
sx={{
border: "1px solid",
borderColor: "divider",
bgcolor: "background.surface",
boxShadow:
"0 24px 60px rgba(var(--joy-palette-primary-900, 12 38 78) / 0.12)",
}}
>
{alert.isAlert && ( {alert.isAlert && (
<MyAlert type={alert.type} header={alert.header} text={alert.text} /> <MyAlert type={alert.type} header={alert.header} text={alert.text} />
)} )}
@@ -124,13 +139,19 @@ export const Settings = () => {
form.handleSubmit(); form.handleSubmit();
}} }}
> >
<div className="grid gap-6 lg:grid-cols-[1.1fr_0.9fr]"> <div className="grid gap-6 lg:grid-cols-[1.1fr\_0.9fr]">
<div className="space-y-5"> <div className="space-y-5">
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("app-name")} {t("app-name")}
</Typography> </Typography>
<Typography level="body-sm" className="text-slate-500"> <Typography
level="body-sm"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{t("app-name-sub")} {t("app-name-sub")}
</Typography> </Typography>
<form.Field name="app-name"> <form.Field name="app-name">
@@ -141,16 +162,26 @@ export const Settings = () => {
size="lg" size="lg"
variant="outlined" variant="outlined"
placeholder="Stockhome" placeholder="Stockhome"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/> />
)} )}
</form.Field> </form.Field>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("currency")} {t("currency")}
</Typography> </Typography>
<Typography level="body-sm" className="text-slate-500"> <Typography
level="body-sm"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{t("currency-sub")} {t("currency-sub")}
</Typography> </Typography>
<form.Field name="currency"> <form.Field name="currency">
@@ -161,37 +192,63 @@ export const Settings = () => {
size="lg" size="lg"
variant="outlined" variant="outlined"
placeholder="EUR" placeholder="EUR"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/> />
)} )}
</form.Field> </form.Field>
</div> </div>
</div> </div>
<div className="rounded-2xl border border-white/70 bg-linear-to-br from-[#f7fbff] via-[#f2f6fb] to-[#eef3f9] p-5 shadow-[0_16px_40px_rgba(12,38,78,0.08)]"> <div
<Typography level="title-lg" className="text-slate-900"> className="rounded-2xl p-5"
style={{
border: "1px solid var(--joy-palette-divider)",
background:
"linear-gradient(to bottom right, var(--joy-palette-primary-50), var(--joy-palette-background-level1), var(--joy-palette-background-level2))",
boxShadow: "0 16px 40px var(--joy-palette-divider)",
}}
>
<Typography
level="title-lg"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("quick-tips")} {t("quick-tips")}
</Typography> </Typography>
<Divider className="my-3" /> <Divider className="my-3" />
<div className="space-y-3 text-md text-slate-600"> <div
className="space-y-3 text-md"
style={{ color: "var(--joy-palette-text-secondary)" }}
>
<p>{t("quick-tips-1")}</p> <p>{t("quick-tips-1")}</p>
<p>{t("quick-tips-2")}</p> <p>{t("quick-tips-2")}</p>
</div> </div>
</div> </div>
</div> </div>
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-end gap-3">
<Button <Button
type="submit" type="submit"
size="lg" size="lg"
color="primary" color="primary"
className="rounded-2xl text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]" className="rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
> >
{t("save")} {t("save")}
</Button> </Button>
<Button <Button
onClick={() => setModal(true)} onClick={() => setModal(true)}
size="lg" size="lg"
color="warning" color="primary"
className="rounded-2xl text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]" className="rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
> >
{t("change-password")} {t("change-password")}
</Button> </Button>
+32 -10
View File
@@ -50,10 +50,16 @@ export const Storages = () => {
<> <>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="min-w-65 space-y-2"> <div className="min-w-65 space-y-2">
<Typography level="h2" className="text-slate-900"> <Typography
level="h2"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("storages")} {t("storages")}
</Typography> </Typography>
<Typography level="body-lg" className="text-slate-500"> <Typography
level="body-lg"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{t("storage-delete-info")} {t("storage-delete-info")}
</Typography> </Typography>
</div> </div>
@@ -74,7 +80,12 @@ export const Storages = () => {
<Sheet <Sheet
variant="outlined" variant="outlined"
className="mt-6 flex min-h-0 w-full max-w-full flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white/80 shadow-sm sm:h-[calc(100vh-260px)]" className="mt-6 flex min-h-0 w-full max-w-full flex-col overflow-hidden rounded-2xl shadow-sm sm:h-[calc(100vh-260px)]"
sx={{
border: "1px solid",
borderColor: "divider",
bgcolor: "background.surface",
}}
> >
<AddStorageModal isOpen={modal} setOpen={setModal} /> <AddStorageModal isOpen={modal} setOpen={setModal} />
{isLoading ? ( {isLoading ? (
@@ -83,7 +94,13 @@ export const Storages = () => {
</div> </div>
) : ( ) : (
<> <>
<div className="flex items-center justify-between border-b border-slate-200 px-6 py-4 text-slate-700"> <div
className="flex items-center justify-between px-6 py-4"
style={{
borderBottom: "1px solid var(--joy-palette-divider)",
color: "var(--joy-palette-text-secondary)",
}}
>
<Typography level="body-lg" fontWeight="bold"> <Typography level="body-lg" fontWeight="bold">
{t("storages")} {t("storages")}
</Typography> </Typography>
@@ -94,24 +111,26 @@ export const Storages = () => {
stripe="odd" stripe="odd"
variant="plain" variant="plain"
hoverRow hoverRow
className="w-full text-slate-700" className="w-full"
sx={{ sx={{
tableLayout: "fixed", tableLayout: "fixed",
color: "var(--joy-palette-text-secondary)",
"--TableCell-headBackground": "--TableCell-headBackground":
"var(--joy-palette-background-surface)", "var(--joy-palette-background-level1)",
"& thead": { "& thead": {
position: "sticky", position: "sticky",
top: 0, top: 0,
zIndex: 3, zIndex: 3,
backgroundColor: "rgb(248 250 252)", backgroundColor: "var(--joy-palette-background-level1)",
}, },
"& thead tr": { "& thead tr": {
backgroundColor: "rgb(248 250 252)", backgroundColor: "var(--joy-palette-background-level1)",
}, },
"& thead th": { "& thead th": {
zIndex: 2, zIndex: 2,
backgroundColor: "rgb(248 250 252)", backgroundColor: "var(--joy-palette-background-level1)",
backgroundImage: "none", backgroundImage: "none",
color: "var(--joy-palette-text-secondary)",
}, },
"& thead th:nth-child(1)": { "& thead th:nth-child(1)": {
width: "26%", width: "26%",
@@ -134,10 +153,13 @@ export const Storages = () => {
minWidth: "80px", minWidth: "80px",
}, },
"& tr > *:nth-child(n+3)": { textAlign: "left" }, "& tr > *:nth-child(n+3)": { textAlign: "left" },
"& tbody tr": {
borderTop: "1px solid var(--joy-palette-divider)",
},
}} }}
> >
<thead> <thead>
<tr className="text-slate-600"> <tr>
<th className="px-3 py-4">{t("storage-name")}</th> <th className="px-3 py-4">{t("storage-name")}</th>
<th className="px-3 py-4">{t("description")}</th> <th className="px-3 py-4">{t("description")}</th>
<th className="px-3 py-4">{t("created-at")}</th> <th className="px-3 py-4">{t("created-at")}</th>
+109 -39
View File
@@ -1,27 +1,12 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { getStorages } from "../utils/api/storages.ts"; import { getStorages } from "../utils/api/storages.ts";
import { import { Box, Button, Chip, CircularProgress, Divider, Input, Option, Select, Typography, } from "@mui/joy";
Box,
Button,
Chip,
CircularProgress,
Divider,
Input,
Option,
Select,
Typography,
} from "@mui/joy";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "@tanstack/react-form"; import { useForm } from "@tanstack/react-form";
import { getProductDetails, mutateProduct } from "../utils/api/products.ts"; import { getProductDetails, mutateProduct } from "../utils/api/products.ts";
import { toInputDate } from "../utils/uxFncs"; import { toInputDate } from "../utils/uxFncs";
import type { import type { AlertInterface, productDetailsInterface, ProductFormValues, Storage, } from "../misc/interfaces";
AlertInterface,
productDetailsInterface,
ProductFormValues,
Storage,
} from "../misc/interfaces";
import type { ApiError } from "../utils/api/apiError"; import type { ApiError } from "../utils/api/apiError";
import QrCodeIcon from "@mui/icons-material/QrCode"; import QrCodeIcon from "@mui/icons-material/QrCode";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
@@ -160,7 +145,10 @@ export const ViewProduct = (props: ViewProductProps) => {
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<div className="space-y-1"> <div className="space-y-1">
<Typography level="h2" className="text-slate-900"> <Typography
level="h2"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("product-details")} {t("product-details")}
</Typography> </Typography>
</div> </div>
@@ -175,7 +163,16 @@ export const ViewProduct = (props: ViewProductProps) => {
{productDetailsLoading && <CircularProgress size="sm" />} {productDetailsLoading && <CircularProgress size="sm" />}
</div> </div>
{isSuccess && ( {isSuccess && (
<Box className="mt-6 rounded-3xl border border-white/70 bg-white/80 p-6 shadow-[0_24px_60px_rgba(12,38,78,0.12)] backdrop-blur"> <Box
className="mt-6 rounded-3xl p-6 backdrop-blur"
sx={{
border: "1px solid",
borderColor: "divider",
bgcolor: "background.surface",
boxShadow:
"0 24px 60px color-mix(in srgb, var(--joy-palette-primary-800) 12%, transparent)",
}}
>
<form <form
className="space-y-6" className="space-y-6"
onSubmit={(e) => { onSubmit={(e) => {
@@ -186,7 +183,10 @@ export const ViewProduct = (props: ViewProductProps) => {
<div className="grid gap-5 lg:grid-cols-[1.2fr_1fr]"> <div className="grid gap-5 lg:grid-cols-[1.2fr_1fr]">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("product-name")} {t("product-name")}
</Typography> </Typography>
<form.Field name="name"> <form.Field name="name">
@@ -198,13 +198,20 @@ export const ViewProduct = (props: ViewProductProps) => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/> />
)} )}
</form.Field> </form.Field>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("description")} {t("description")}
</Typography> </Typography>
<form.Field name="description"> <form.Field name="description">
@@ -216,14 +223,21 @@ export const ViewProduct = (props: ViewProductProps) => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/> />
)} )}
</form.Field> </form.Field>
</div> </div>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("expiry-date")} {t("expiry-date")}
</Typography> </Typography>
<form.Field name="expiry_date"> <form.Field name="expiry_date">
@@ -235,13 +249,19 @@ export const ViewProduct = (props: ViewProductProps) => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/> />
)} )}
</form.Field> </form.Field>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("bottling-date")} {t("bottling-date")}
</Typography> </Typography>
<form.Field name="bottling_date"> <form.Field name="bottling_date">
@@ -253,7 +273,10 @@ export const ViewProduct = (props: ViewProductProps) => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/> />
)} )}
</form.Field> </form.Field>
@@ -261,14 +284,28 @@ export const ViewProduct = (props: ViewProductProps) => {
</div> </div>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<div className="rounded-2xl border border-white/70 bg-linear-to-br from-[#f7fbff] via-[#f2f6fb] to-[#eef3f9] p-5 shadow-[0_16px_40px_rgba(12,38,78,0.08)]"> <div
<Typography level="title-lg" className="text-slate-900"> className="rounded-2xl p-5"
style={{
border: "1px solid var(--joy-palette-divider)",
background:
"linear-gradient(to bottom right, var(--joy-palette-primary-50), var(--joy-palette-background-level1), var(--joy-palette-background-level2))",
boxShadow: "0 16px 40px var(--joy-palette-divider)",
}}
>
<Typography
level="title-lg"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("inventory")} {t("inventory")}
</Typography> </Typography>
<Divider className="my-3" /> <Divider className="my-3" />
<div className="grid gap-4"> <div className="grid gap-4">
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("amount")} {t("amount")}
</Typography> </Typography>
<form.Field name="amount"> <form.Field name="amount">
@@ -288,13 +325,19 @@ export const ViewProduct = (props: ViewProductProps) => {
); );
}} }}
onBlur={field.handleBlur} onBlur={field.handleBlur}
className="rounded-2xl bg-white/80" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/> />
)} )}
</form.Field> </form.Field>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("price")} {t("price")}
</Typography> </Typography>
<form.Field name="price"> <form.Field name="price">
@@ -306,16 +349,25 @@ export const ViewProduct = (props: ViewProductProps) => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/> />
)} )}
</form.Field> </form.Field>
<Typography level="body-sm" className="text-slate-500"> <Typography
level="body-sm"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{Cookies.get("currency")} {Cookies.get("currency")}
</Typography> </Typography>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Typography level="title-md" className="text-slate-900"> <Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("storage-place")} {t("storage-place")}
</Typography> </Typography>
<form.Field name="storage_location_uuid"> <form.Field name="storage_location_uuid">
@@ -327,7 +379,10 @@ export const ViewProduct = (props: ViewProductProps) => {
} }
size="lg" size="lg"
variant="outlined" variant="outlined"
className="rounded-2xl bg-white/90" className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
> >
{storages?.map((storage) => ( {storages?.map((storage) => (
<Option key={storage.uuid} value={storage.uuid}> <Option key={storage.uuid} value={storage.uuid}>
@@ -344,7 +399,10 @@ export const ViewProduct = (props: ViewProductProps) => {
</div> </div>
<div> <div>
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<Typography level="body-sm" className="text-slate-500"> <Typography
level="body-sm"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{t("product-details")} {t("product-details")}
</Typography> </Typography>
<div className="grow"></div> <div className="grow"></div>
@@ -353,7 +411,13 @@ export const ViewProduct = (props: ViewProductProps) => {
loading={isPending} loading={isPending}
onClick={() => downloadQRcode()} onClick={() => downloadQRcode()}
size="lg" size="lg"
className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]" color="primary"
variant="solid"
className="rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
> >
{t("download-qr-code")} {t("download-qr-code")}
</Button> </Button>
@@ -361,7 +425,13 @@ export const ViewProduct = (props: ViewProductProps) => {
type="submit" type="submit"
loading={isPending} loading={isPending}
size="lg" size="lg"
className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]" color="primary"
variant="solid"
className="rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
> >
{t("save")} {t("save")}
</Button> </Button>
+125 -97
View File
@@ -12,12 +12,13 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as LoginRouteImport } from './routes/login' import { Route as LoginRouteImport } from './routes/login'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as AppHiddenLayoutRouteImport } from './routes/app/_hiddenLayout' import { Route as AppHiddenLayoutRouteImport } from './routes/app/_hiddenLayout'
import { Route as AppHiddenLayoutViewProductRouteImport } from './routes/app/_hiddenLayout/view-product' import { Route as AppHiddenLayoutAuthedRouteImport } from './routes/app/_hiddenLayout/_authed'
import { Route as AppHiddenLayoutStoragesRouteImport } from './routes/app/_hiddenLayout/storages'
import { Route as AppHiddenLayoutInventoryRouteImport } from './routes/app/_hiddenLayout/inventory'
import { Route as AppHiddenLayoutAppSettingsRouteImport } from './routes/app/_hiddenLayout/app-settings'
import { Route as AppHiddenLayoutAddProductRouteImport } from './routes/app/_hiddenLayout/add-product'
import { Route as AppHiddenLayoutQuickViewProductRouteImport } from './routes/app/_hiddenLayout/quick-view/product' import { Route as AppHiddenLayoutQuickViewProductRouteImport } from './routes/app/_hiddenLayout/quick-view/product'
import { Route as AppHiddenLayoutAuthedViewProductRouteImport } from './routes/app/_hiddenLayout/_authed/view-product'
import { Route as AppHiddenLayoutAuthedStoragesRouteImport } from './routes/app/_hiddenLayout/_authed/storages'
import { Route as AppHiddenLayoutAuthedInventoryRouteImport } from './routes/app/_hiddenLayout/_authed/inventory'
import { Route as AppHiddenLayoutAuthedAppSettingsRouteImport } from './routes/app/_hiddenLayout/_authed/app-settings'
import { Route as AppHiddenLayoutAuthedAddProductRouteImport } from './routes/app/_hiddenLayout/_authed/add-product'
const LoginRoute = LoginRouteImport.update({ const LoginRoute = LoginRouteImport.update({
id: '/login', id: '/login',
@@ -34,62 +35,67 @@ const AppHiddenLayoutRoute = AppHiddenLayoutRouteImport.update({
path: '/app', path: '/app',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AppHiddenLayoutViewProductRoute = const AppHiddenLayoutAuthedRoute = AppHiddenLayoutAuthedRouteImport.update({
AppHiddenLayoutViewProductRouteImport.update({ id: '/_authed',
id: '/view-product',
path: '/view-product',
getParentRoute: () => AppHiddenLayoutRoute,
} as any)
const AppHiddenLayoutStoragesRoute = AppHiddenLayoutStoragesRouteImport.update({
id: '/storages',
path: '/storages',
getParentRoute: () => AppHiddenLayoutRoute, getParentRoute: () => AppHiddenLayoutRoute,
} as any) } as any)
const AppHiddenLayoutInventoryRoute =
AppHiddenLayoutInventoryRouteImport.update({
id: '/inventory',
path: '/inventory',
getParentRoute: () => AppHiddenLayoutRoute,
} as any)
const AppHiddenLayoutAppSettingsRoute =
AppHiddenLayoutAppSettingsRouteImport.update({
id: '/app-settings',
path: '/app-settings',
getParentRoute: () => AppHiddenLayoutRoute,
} as any)
const AppHiddenLayoutAddProductRoute =
AppHiddenLayoutAddProductRouteImport.update({
id: '/add-product',
path: '/add-product',
getParentRoute: () => AppHiddenLayoutRoute,
} as any)
const AppHiddenLayoutQuickViewProductRoute = const AppHiddenLayoutQuickViewProductRoute =
AppHiddenLayoutQuickViewProductRouteImport.update({ AppHiddenLayoutQuickViewProductRouteImport.update({
id: '/quick-view/product', id: '/quick-view/product',
path: '/quick-view/product', path: '/quick-view/product',
getParentRoute: () => AppHiddenLayoutRoute, getParentRoute: () => AppHiddenLayoutRoute,
} as any) } as any)
const AppHiddenLayoutAuthedViewProductRoute =
AppHiddenLayoutAuthedViewProductRouteImport.update({
id: '/view-product',
path: '/view-product',
getParentRoute: () => AppHiddenLayoutAuthedRoute,
} as any)
const AppHiddenLayoutAuthedStoragesRoute =
AppHiddenLayoutAuthedStoragesRouteImport.update({
id: '/storages',
path: '/storages',
getParentRoute: () => AppHiddenLayoutAuthedRoute,
} as any)
const AppHiddenLayoutAuthedInventoryRoute =
AppHiddenLayoutAuthedInventoryRouteImport.update({
id: '/inventory',
path: '/inventory',
getParentRoute: () => AppHiddenLayoutAuthedRoute,
} as any)
const AppHiddenLayoutAuthedAppSettingsRoute =
AppHiddenLayoutAuthedAppSettingsRouteImport.update({
id: '/app-settings',
path: '/app-settings',
getParentRoute: () => AppHiddenLayoutAuthedRoute,
} as any)
const AppHiddenLayoutAuthedAddProductRoute =
AppHiddenLayoutAuthedAddProductRouteImport.update({
id: '/add-product',
path: '/add-product',
getParentRoute: () => AppHiddenLayoutAuthedRoute,
} as any)
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/app': typeof AppHiddenLayoutRouteWithChildren '/app': typeof AppHiddenLayoutAuthedRouteWithChildren
'/app/add-product': typeof AppHiddenLayoutAddProductRoute '/app/add-product': typeof AppHiddenLayoutAuthedAddProductRoute
'/app/app-settings': typeof AppHiddenLayoutAppSettingsRoute '/app/app-settings': typeof AppHiddenLayoutAuthedAppSettingsRoute
'/app/inventory': typeof AppHiddenLayoutInventoryRoute '/app/inventory': typeof AppHiddenLayoutAuthedInventoryRoute
'/app/storages': typeof AppHiddenLayoutStoragesRoute '/app/storages': typeof AppHiddenLayoutAuthedStoragesRoute
'/app/view-product': typeof AppHiddenLayoutViewProductRoute '/app/view-product': typeof AppHiddenLayoutAuthedViewProductRoute
'/app/quick-view/product': typeof AppHiddenLayoutQuickViewProductRoute '/app/quick-view/product': typeof AppHiddenLayoutQuickViewProductRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/app': typeof AppHiddenLayoutRouteWithChildren '/app': typeof AppHiddenLayoutAuthedRouteWithChildren
'/app/add-product': typeof AppHiddenLayoutAddProductRoute '/app/add-product': typeof AppHiddenLayoutAuthedAddProductRoute
'/app/app-settings': typeof AppHiddenLayoutAppSettingsRoute '/app/app-settings': typeof AppHiddenLayoutAuthedAppSettingsRoute
'/app/inventory': typeof AppHiddenLayoutInventoryRoute '/app/inventory': typeof AppHiddenLayoutAuthedInventoryRoute
'/app/storages': typeof AppHiddenLayoutStoragesRoute '/app/storages': typeof AppHiddenLayoutAuthedStoragesRoute
'/app/view-product': typeof AppHiddenLayoutViewProductRoute '/app/view-product': typeof AppHiddenLayoutAuthedViewProductRoute
'/app/quick-view/product': typeof AppHiddenLayoutQuickViewProductRoute '/app/quick-view/product': typeof AppHiddenLayoutQuickViewProductRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
@@ -97,11 +103,12 @@ export interface FileRoutesById {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/app/_hiddenLayout': typeof AppHiddenLayoutRouteWithChildren '/app/_hiddenLayout': typeof AppHiddenLayoutRouteWithChildren
'/app/_hiddenLayout/add-product': typeof AppHiddenLayoutAddProductRoute '/app/_hiddenLayout/_authed': typeof AppHiddenLayoutAuthedRouteWithChildren
'/app/_hiddenLayout/app-settings': typeof AppHiddenLayoutAppSettingsRoute '/app/_hiddenLayout/_authed/add-product': typeof AppHiddenLayoutAuthedAddProductRoute
'/app/_hiddenLayout/inventory': typeof AppHiddenLayoutInventoryRoute '/app/_hiddenLayout/_authed/app-settings': typeof AppHiddenLayoutAuthedAppSettingsRoute
'/app/_hiddenLayout/storages': typeof AppHiddenLayoutStoragesRoute '/app/_hiddenLayout/_authed/inventory': typeof AppHiddenLayoutAuthedInventoryRoute
'/app/_hiddenLayout/view-product': typeof AppHiddenLayoutViewProductRoute '/app/_hiddenLayout/_authed/storages': typeof AppHiddenLayoutAuthedStoragesRoute
'/app/_hiddenLayout/_authed/view-product': typeof AppHiddenLayoutAuthedViewProductRoute
'/app/_hiddenLayout/quick-view/product': typeof AppHiddenLayoutQuickViewProductRoute '/app/_hiddenLayout/quick-view/product': typeof AppHiddenLayoutQuickViewProductRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
@@ -132,11 +139,12 @@ export interface FileRouteTypes {
| '/' | '/'
| '/login' | '/login'
| '/app/_hiddenLayout' | '/app/_hiddenLayout'
| '/app/_hiddenLayout/add-product' | '/app/_hiddenLayout/_authed'
| '/app/_hiddenLayout/app-settings' | '/app/_hiddenLayout/_authed/add-product'
| '/app/_hiddenLayout/inventory' | '/app/_hiddenLayout/_authed/app-settings'
| '/app/_hiddenLayout/storages' | '/app/_hiddenLayout/_authed/inventory'
| '/app/_hiddenLayout/view-product' | '/app/_hiddenLayout/_authed/storages'
| '/app/_hiddenLayout/_authed/view-product'
| '/app/_hiddenLayout/quick-view/product' | '/app/_hiddenLayout/quick-view/product'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
@@ -169,39 +177,11 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppHiddenLayoutRouteImport preLoaderRoute: typeof AppHiddenLayoutRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/app/_hiddenLayout/view-product': { '/app/_hiddenLayout/_authed': {
id: '/app/_hiddenLayout/view-product' id: '/app/_hiddenLayout/_authed'
path: '/view-product' path: ''
fullPath: '/app/view-product' fullPath: '/app'
preLoaderRoute: typeof AppHiddenLayoutViewProductRouteImport preLoaderRoute: typeof AppHiddenLayoutAuthedRouteImport
parentRoute: typeof AppHiddenLayoutRoute
}
'/app/_hiddenLayout/storages': {
id: '/app/_hiddenLayout/storages'
path: '/storages'
fullPath: '/app/storages'
preLoaderRoute: typeof AppHiddenLayoutStoragesRouteImport
parentRoute: typeof AppHiddenLayoutRoute
}
'/app/_hiddenLayout/inventory': {
id: '/app/_hiddenLayout/inventory'
path: '/inventory'
fullPath: '/app/inventory'
preLoaderRoute: typeof AppHiddenLayoutInventoryRouteImport
parentRoute: typeof AppHiddenLayoutRoute
}
'/app/_hiddenLayout/app-settings': {
id: '/app/_hiddenLayout/app-settings'
path: '/app-settings'
fullPath: '/app/app-settings'
preLoaderRoute: typeof AppHiddenLayoutAppSettingsRouteImport
parentRoute: typeof AppHiddenLayoutRoute
}
'/app/_hiddenLayout/add-product': {
id: '/app/_hiddenLayout/add-product'
path: '/add-product'
fullPath: '/app/add-product'
preLoaderRoute: typeof AppHiddenLayoutAddProductRouteImport
parentRoute: typeof AppHiddenLayoutRoute parentRoute: typeof AppHiddenLayoutRoute
} }
'/app/_hiddenLayout/quick-view/product': { '/app/_hiddenLayout/quick-view/product': {
@@ -211,24 +191,72 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppHiddenLayoutQuickViewProductRouteImport preLoaderRoute: typeof AppHiddenLayoutQuickViewProductRouteImport
parentRoute: typeof AppHiddenLayoutRoute parentRoute: typeof AppHiddenLayoutRoute
} }
'/app/_hiddenLayout/_authed/view-product': {
id: '/app/_hiddenLayout/_authed/view-product'
path: '/view-product'
fullPath: '/app/view-product'
preLoaderRoute: typeof AppHiddenLayoutAuthedViewProductRouteImport
parentRoute: typeof AppHiddenLayoutAuthedRoute
}
'/app/_hiddenLayout/_authed/storages': {
id: '/app/_hiddenLayout/_authed/storages'
path: '/storages'
fullPath: '/app/storages'
preLoaderRoute: typeof AppHiddenLayoutAuthedStoragesRouteImport
parentRoute: typeof AppHiddenLayoutAuthedRoute
}
'/app/_hiddenLayout/_authed/inventory': {
id: '/app/_hiddenLayout/_authed/inventory'
path: '/inventory'
fullPath: '/app/inventory'
preLoaderRoute: typeof AppHiddenLayoutAuthedInventoryRouteImport
parentRoute: typeof AppHiddenLayoutAuthedRoute
}
'/app/_hiddenLayout/_authed/app-settings': {
id: '/app/_hiddenLayout/_authed/app-settings'
path: '/app-settings'
fullPath: '/app/app-settings'
preLoaderRoute: typeof AppHiddenLayoutAuthedAppSettingsRouteImport
parentRoute: typeof AppHiddenLayoutAuthedRoute
}
'/app/_hiddenLayout/_authed/add-product': {
id: '/app/_hiddenLayout/_authed/add-product'
path: '/add-product'
fullPath: '/app/add-product'
preLoaderRoute: typeof AppHiddenLayoutAuthedAddProductRouteImport
parentRoute: typeof AppHiddenLayoutAuthedRoute
}
} }
} }
interface AppHiddenLayoutAuthedRouteChildren {
AppHiddenLayoutAuthedAddProductRoute: typeof AppHiddenLayoutAuthedAddProductRoute
AppHiddenLayoutAuthedAppSettingsRoute: typeof AppHiddenLayoutAuthedAppSettingsRoute
AppHiddenLayoutAuthedInventoryRoute: typeof AppHiddenLayoutAuthedInventoryRoute
AppHiddenLayoutAuthedStoragesRoute: typeof AppHiddenLayoutAuthedStoragesRoute
AppHiddenLayoutAuthedViewProductRoute: typeof AppHiddenLayoutAuthedViewProductRoute
}
const AppHiddenLayoutAuthedRouteChildren: AppHiddenLayoutAuthedRouteChildren = {
AppHiddenLayoutAuthedAddProductRoute: AppHiddenLayoutAuthedAddProductRoute,
AppHiddenLayoutAuthedAppSettingsRoute: AppHiddenLayoutAuthedAppSettingsRoute,
AppHiddenLayoutAuthedInventoryRoute: AppHiddenLayoutAuthedInventoryRoute,
AppHiddenLayoutAuthedStoragesRoute: AppHiddenLayoutAuthedStoragesRoute,
AppHiddenLayoutAuthedViewProductRoute: AppHiddenLayoutAuthedViewProductRoute,
}
const AppHiddenLayoutAuthedRouteWithChildren =
AppHiddenLayoutAuthedRoute._addFileChildren(
AppHiddenLayoutAuthedRouteChildren,
)
interface AppHiddenLayoutRouteChildren { interface AppHiddenLayoutRouteChildren {
AppHiddenLayoutAddProductRoute: typeof AppHiddenLayoutAddProductRoute AppHiddenLayoutAuthedRoute: typeof AppHiddenLayoutAuthedRouteWithChildren
AppHiddenLayoutAppSettingsRoute: typeof AppHiddenLayoutAppSettingsRoute
AppHiddenLayoutInventoryRoute: typeof AppHiddenLayoutInventoryRoute
AppHiddenLayoutStoragesRoute: typeof AppHiddenLayoutStoragesRoute
AppHiddenLayoutViewProductRoute: typeof AppHiddenLayoutViewProductRoute
AppHiddenLayoutQuickViewProductRoute: typeof AppHiddenLayoutQuickViewProductRoute AppHiddenLayoutQuickViewProductRoute: typeof AppHiddenLayoutQuickViewProductRoute
} }
const AppHiddenLayoutRouteChildren: AppHiddenLayoutRouteChildren = { const AppHiddenLayoutRouteChildren: AppHiddenLayoutRouteChildren = {
AppHiddenLayoutAddProductRoute: AppHiddenLayoutAddProductRoute, AppHiddenLayoutAuthedRoute: AppHiddenLayoutAuthedRouteWithChildren,
AppHiddenLayoutAppSettingsRoute: AppHiddenLayoutAppSettingsRoute,
AppHiddenLayoutInventoryRoute: AppHiddenLayoutInventoryRoute,
AppHiddenLayoutStoragesRoute: AppHiddenLayoutStoragesRoute,
AppHiddenLayoutViewProductRoute: AppHiddenLayoutViewProductRoute,
AppHiddenLayoutQuickViewProductRoute: AppHiddenLayoutQuickViewProductRoute, AppHiddenLayoutQuickViewProductRoute: AppHiddenLayoutQuickViewProductRoute,
} }
+5 -2
View File
@@ -1,4 +1,4 @@
import { Outlet, createFileRoute } from "@tanstack/react-router"; import { createFileRoute, Outlet } from "@tanstack/react-router";
import { Sidebar } from "../../components/Sidebar"; import { Sidebar } from "../../components/Sidebar";
export const Route = createFileRoute("/app/_hiddenLayout")({ export const Route = createFileRoute("/app/_hiddenLayout")({
@@ -7,7 +7,10 @@ export const Route = createFileRoute("/app/_hiddenLayout")({
function AppLayout() { function AppLayout() {
return ( return (
<div className="flex min-h-screen w-full flex-col bg-[#f7f9fc] lg:flex-row"> <div
className="flex min-h-screen w-full flex-col lg:flex-row"
style={{ backgroundColor: "var(--joy-palette-background-body)" }}
>
<Sidebar /> <Sidebar />
<main className="flex-1 px-4 py-5 sm:px-6 lg:px-8 lg:py-6"> <main className="flex-1 px-4 py-5 sm:px-6 lg:px-8 lg:py-6">
<Outlet /> <Outlet />
@@ -0,0 +1,9 @@
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { verifyLogin } from "../../../utils/api/auth";
export const Route = createFileRoute("/app/_hiddenLayout/_authed")({
beforeLoad: async () => {
await verifyLogin();
},
component: () => <Outlet />,
});
@@ -0,0 +1,10 @@
import { createFileRoute } from "@tanstack/react-router";
import { AddProduct } from "../../../../pages/AddProduct";
export const Route = createFileRoute("/app/_hiddenLayout/_authed/add-product")({
component: RouteComponent,
});
function RouteComponent() {
return <AddProduct />;
}
@@ -0,0 +1,12 @@
import { createFileRoute } from "@tanstack/react-router";
import { Settings } from "../../../../pages/Settings";
export const Route = createFileRoute("/app/_hiddenLayout/_authed/app-settings")(
{
component: RouteComponent,
},
);
function RouteComponent() {
return <Settings />;
}
@@ -0,0 +1,10 @@
import { createFileRoute } from "@tanstack/react-router";
import { InventoryPage } from "../../../../pages/Inventory";
export const Route = createFileRoute("/app/_hiddenLayout/_authed/inventory")({
component: RouteComponent,
});
function RouteComponent() {
return <InventoryPage />;
}
@@ -0,0 +1,10 @@
import { createFileRoute } from "@tanstack/react-router";
import { Storages } from "../../../../pages/Storages";
export const Route = createFileRoute("/app/_hiddenLayout/_authed/storages")({
component: RouteComponent,
});
function RouteComponent() {
return <Storages />;
}
@@ -0,0 +1,17 @@
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
import { ViewProduct } from "../../../../pages/ViewProduct";
export const Route = createFileRoute("/app/_hiddenLayout/_authed/view-product")(
{
validateSearch: z.object({
product: z.string(),
}),
component: RouteComponent,
},
);
function RouteComponent() {
const { product } = Route.useSearch();
return <ViewProduct uuid={product} />;
}
@@ -1,18 +0,0 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { isAuthenticated } from "../../../utils/api/auth";
import { AddProduct } from "../../../pages/AddProduct";
export const Route = createFileRoute("/app/_hiddenLayout/add-product")({
beforeLoad: async () => {
if (!(await isAuthenticated())) {
throw redirect({
to: "/login",
});
}
},
component: RouteComponent,
});
function RouteComponent() {
return <AddProduct />;
}
@@ -1,18 +0,0 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { isAuthenticated } from "../../../utils/api/auth";
import { Settings } from "../../../pages/Settings";
export const Route = createFileRoute("/app/_hiddenLayout/app-settings")({
beforeLoad: async () => {
if (!(await isAuthenticated())) {
throw redirect({
to: "/login",
});
}
},
component: RouteComponent,
});
function RouteComponent() {
return <Settings />;
}
@@ -1,18 +0,0 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { isAuthenticated } from "../../../utils/api/auth";
import { InventoryPage } from "../../../pages/Inventory";
export const Route = createFileRoute("/app/_hiddenLayout/inventory")({
beforeLoad: async () => {
if (!(await isAuthenticated())) {
throw redirect({
to: "/login",
});
}
},
component: RouteComponent,
});
function RouteComponent() {
return <InventoryPage />;
}
@@ -1,18 +0,0 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { isAuthenticated } from "../../../utils/api/auth";
import { Storages } from "../../../pages/Storages";
export const Route = createFileRoute("/app/_hiddenLayout/storages")({
beforeLoad: async () => {
if (!(await isAuthenticated())) {
throw redirect({
to: "/login",
});
}
},
component: RouteComponent,
});
function RouteComponent() {
return <Storages />;
}
@@ -1,23 +0,0 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { z } from "zod";
import { isAuthenticated } from "../../../utils/api/auth";
import { ViewProduct } from "../../../pages/ViewProduct";
export const Route = createFileRoute("/app/_hiddenLayout/view-product")({
beforeLoad: async () => {
if (!(await isAuthenticated())) {
throw redirect({
to: "/login",
});
}
},
validateSearch: z.object({
product: z.string(),
}),
component: RouteComponent,
});
function RouteComponent() {
const { product } = Route.useSearch();
return <ViewProduct uuid={product} />;
}
+1
View File
@@ -6,6 +6,7 @@ export const Route = createFileRoute("/")({
if (!(await isAuthenticated())) { if (!(await isAuthenticated())) {
throw redirect({ throw redirect({
to: "/login", to: "/login",
search: { loggedOut: true },
}); });
} else { } else {
throw redirect({ throw redirect({
+4
View File
@@ -1,7 +1,11 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { LoginCard } from "../components/LoginCard"; import { LoginCard } from "../components/LoginCard";
import { checkLogin } from "../utils/api/auth.ts";
export const Route = createFileRoute("/login")({ export const Route = createFileRoute("/login")({
beforeLoad: async () => {
await checkLogin();
},
component: RouteComponent, component: RouteComponent,
}); });
+265
View File
@@ -0,0 +1,265 @@
import { extendTheme } from "@mui/joy/styles";
// I generated this theme with AI (Claude Opus 4.6)
export const theme = extendTheme({
cssVarPrefix: "joy",
colorSchemes: {
light: {
palette: {
primary: {
50: "#eef7ff",
100: "#dcedff",
200: "#b3d9ff",
300: "#7fbdff",
400: "#469cf5",
500: "#0b6bcb",
600: "#0958a8",
700: "#08478a",
800: "#0a3a6e",
900: "#0c2f59",
solidBg: "#0b6bcb",
solidHoverBg: "#095aa7",
solidActiveBg: "#08478a",
outlinedBorder: "#7fbdff",
outlinedColor: "#08478a",
softBg: "#dcedff", // Color in sidebar for selected view
softColor: "#08478a",
},
success: {
50: "#ecfdf5",
100: "#d1faee",
200: "#a6f4dc",
300: "#6ee7c9",
400: "#34d0ac",
500: "#0d9488",
600: "#0b7c73",
700: "#0a655e",
800: "#0b514c",
900: "#0a423f",
solidBg: "#0d9488",
solidHoverBg: "#0b7c73",
softBg: "#d1faee",
softColor: "#0a655e",
},
warning: {
50: "#fffaeb",
100: "#fef0c7",
200: "#fedf89",
300: "#fdc74a",
400: "#fbaf24",
500: "#f59e0b",
600: "#d67e06",
700: "#b25f08",
800: "#8f4a0e",
900: "#763d10",
solidBg: "#f59e0b",
solidHoverBg: "#d67e06",
softBg: "#fef0c7",
softColor: "#8f4a0e",
},
danger: {
50: "#fff1f2",
100: "#ffe1e4",
200: "#ffc7cd",
300: "#ff9ba7",
400: "#fb6b7d",
500: "#e11d48",
600: "#be123c",
700: "#9b1038",
800: "#7f1230",
900: "#6a122b",
solidBg: "#e11d48",
solidHoverBg: "#be123c",
softBg: "#ffe1e4",
softColor: "#9b1038",
},
neutral: {
50: "#f8fafc",
100: "#f1f5f9",
200: "#e2e8f0",
300: "#cbd5e1",
400: "#94a3b8",
500: "#64748b",
600: "#475569",
700: "#334155",
800: "#1e293b",
900: "#0f172a",
},
background: {
body: "#f7f9fc",
surface: "#ffffff",
level1: "#f1f5f9",
level2: "#e2e8f0",
level3: "#cbd5e1",
},
text: {
primary: "#0f172a",
secondary: "#475569",
tertiary: "#64748b",
},
divider: "rgba(15, 23, 42, 0.08)",
},
},
dark: {
palette: {
primary: {
50: "#0c2f59",
100: "#0a3a6e",
200: "#08478a",
300: "#0958a8",
400: "#0b6bcb",
500: "#3b8ce0",
600: "#6ba9ea",
700: "#9cc6f2",
800: "#c7defa",
900: "#eef7ff",
solidBg: "#3b8ce0",
solidHoverBg: "#6ba9ea",
solidActiveBg: "#0958a8",
outlinedBorder: "#0958a8",
outlinedColor: "#9cc6f2",
softBg: "rgba(59, 140, 224, 0.16)", // Color in sidebar for selected view
softColor: "#9cc6f2",
},
success: {
50: "#0a423f",
100: "#0b514c",
200: "#0a655e",
300: "#0b7c73",
400: "#0d9488",
500: "#2dc8b1",
600: "#6ee7c9",
700: "#a6f4dc",
800: "#d1faee",
900: "#ecfdf5",
solidBg: "#0d9488",
solidHoverBg: "#2dc8b1",
softBg: "rgba(13, 148, 136, 0.18)",
softColor: "#6ee7c9",
},
warning: {
50: "#763d10",
100: "#8f4a0e",
200: "#b25f08",
300: "#d67e06",
400: "#f59e0b",
500: "#fbaf24",
600: "#fdc74a",
700: "#fedf89",
800: "#fef0c7",
900: "#fffaeb",
solidBg: "#f59e0b",
solidHoverBg: "#fbaf24",
softBg: "rgba(245, 158, 11, 0.16)",
softColor: "#fdc74a",
},
danger: {
50: "#6a122b",
100: "#7f1230",
200: "#9b1038",
300: "#be123c",
400: "#e11d48",
500: "#f04463",
600: "#fb6b7d",
700: "#ff9ba7",
800: "#ffc7cd",
900: "#fff1f2",
solidBg: "#e11d48",
solidHoverBg: "#f04463",
softBg: "rgba(225, 29, 72, 0.18)",
softColor: "#fb6b7d",
},
neutral: {
50: "#0b1220",
100: "#0f172a",
200: "#1e293b",
300: "#334155",
400: "#475569",
500: "#64748b",
600: "#94a3b8",
700: "#cbd5e1",
800: "#e2e8f0",
900: "#f1f5f9",
},
background: {
body: "#0b1220",
surface: "#111a2e",
level1: "#16213a",
level2: "#1e293b",
level3: "#334155",
},
text: {
primary: "#f1f5f9",
secondary: "#cbd5e1",
tertiary: "#94a3b8",
},
divider: "rgba(226, 232, 240, 0.08)",
},
},
},
fontFamily: {
body: "'Inter', 'Segoe UI', system-ui, sans-serif",
display: "'Inter', 'Segoe UI', system-ui, sans-serif",
},
radius: {
xs: "6px",
sm: "10px",
md: "14px",
lg: "20px",
xl: "28px",
},
components: {
JoyInput: {
styleOverrides: {
input: ({ theme }) => ({
color: theme.vars.palette.text.primary,
"&::placeholder": {
color: theme.vars.palette.text.tertiary,
opacity: 1,
},
}),
},
},
JoySelect: {
styleOverrides: {
button: ({ theme }) => ({
color: theme.vars.palette.text.primary,
}),
listbox: ({ theme }) => ({
color: theme.vars.palette.text.primary,
backgroundColor: theme.vars.palette.background.surface,
}),
},
},
JoySheet: {
styleOverrides: {
root: ({ theme }) => ({
backgroundColor: theme.vars.palette.background.surface,
borderColor: theme.vars.palette.divider,
}),
},
},
JoyOption: {
styleOverrides: {
root: ({ theme }) => ({
color: theme.vars.palette.text.primary,
backgroundColor: theme.vars.palette.background.surface,
"&:hover": {
backgroundColor: theme.vars.palette.background.level1,
},
'&[aria-selected="true"]': {
backgroundColor: theme.vars.palette.background.level2,
},
}),
},
},
JoyButton: {
styleOverrides: {
root: {
transition: "all 0.15s ease",
},
},
},
},
});
+85 -72
View File
@@ -1,85 +1,98 @@
import {API_BASE} from "../../config/api.config"; import { API_BASE } from "../../config/api.config";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import type {TFunction} from "i18next"; import type { TFunction } from "i18next";
import {fetchSettings} from "./settings"; import { fetchSettings } from "./settings";
import type {ChangePasswordIntf} from "../../misc/interfaces"; import type { ChangePasswordIntf } from "../../misc/interfaces";
import {createApiError} from "./apiError"; import { createApiError } from "./apiError";
import { redirect } from "@tanstack/react-router";
export async function isAuthenticated() { export const isAuthenticated = async () => {
if (Cookies.get("token")) { if (Cookies.get("token")) {
const result = await fetch(`${API_BASE}/users/verify-token`, { const result = await fetch(`${API_BASE}/users/verify-token`, {
method: "POST", method: "POST",
headers: { headers: {
Authorization: `Bearer ${Cookies.get("token") || ""}`, Authorization: `Bearer ${Cookies.get("token") || ""}`,
"Content-Type": "application/json", "Content-Type": "application/json",
Accept: "application/json", Accept: "application/json",
}, },
});
if (result.status === 200) {
return true;
}
}
Cookies.remove("token");
return false;
}
export async function signInUser(
username: string,
password: string,
t: TFunction,
) {
const result = await fetch(`${API_BASE}/users/login`, {
method: "POST",
headers: {
Authorization: `Bearer ${Cookies.get("token") || ""}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({username, password}),
}); });
const response = await result.json(); if (result.status === 200) {
return true;
if (result.status === 202) {
Cookies.set("token", response.data.token);
const settings = await fetchSettings();
Cookies.set("app-name", settings?.data[0].value);
Cookies.set("currency", settings?.data[1].value);
return {ok: true as const};
} }
}
Cookies.remove("token"); Cookies.remove("token");
throw createApiError(response.code, t(response.code || "unknown-error")); return false;
} };
export function signOutUser() { export const signInUser = async (
Cookies.remove("token"); username: string,
return {ok: true as const}; password: string,
} t: TFunction,
) => {
const result = await fetch(`${API_BASE}/users/login`, {
method: "POST",
headers: {
Authorization: `Bearer ${Cookies.get("token") || ""}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ username, password }),
});
const response = await result.json();
if (result.status === 202) {
Cookies.set("token", response.data.token);
const settings = await fetchSettings();
Cookies.set("app-name", settings?.data[0].value);
Cookies.set("currency", settings?.data[1].value);
return { ok: true as const };
}
Cookies.remove("token");
throw createApiError(response.code, t(response.code || "unknown-error"));
};
export const mutatePassword = async (payload: ChangePasswordIntf) => { export const mutatePassword = async (payload: ChangePasswordIntf) => {
const result = await fetch(`${API_BASE}/users/change-password`, { const result = await fetch(`${API_BASE}/users/change-password`, {
method: "POST", method: "POST",
headers: { headers: {
Authorization: `Bearer ${Cookies.get("token") || ""}`, Authorization: `Bearer ${Cookies.get("token") || ""}`,
"Content-Type": "application/json", "Content-Type": "application/json",
Accept: "application/json", Accept: "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
currentPassword: payload.currentPassword, currentPassword: payload.currentPassword,
newPassword: payload.newPassword, newPassword: payload.newPassword,
}), }),
}); });
const response = await result.json(); const response = await result.json();
if (response.code === "SU005") { if (response.code === "SU005") {
return {code: response.code}; return { code: response.code };
} }
throw createApiError(response.code, "Change password failed"); throw createApiError(response.code, "Change password failed");
};
export const verifyLogin = async () => {
if (!(await isAuthenticated())) {
throw redirect({
to: "/login",
search: { loggedOut: true },
});
}
};
export const checkLogin = async () => {
if (await isAuthenticated()) {
throw redirect({
to: "/app/inventory",
});
}
}; };
@@ -58,6 +58,8 @@
"success": "Erfolg", "success": "Erfolg",
"submit": "Absenden", "submit": "Absenden",
"logout-success-text": "Logout erfolgreich!", "logout-success-text": "Logout erfolgreich!",
"not-found-header": "404 - Nicht gefunden",
"not-found-body": "Die Seite nach der du gesucht hast, gibt es nicht.",
"SU005": "Das Passwort wurde erfolgreich geändert!", "SU005": "Das Passwort wurde erfolgreich geändert!",
"SE001": "Die Einstellungen wurden erfolgreich geändert!", "SE001": "Die Einstellungen wurden erfolgreich geändert!",
"EG001": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.", "EG001": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.",
@@ -58,6 +58,8 @@
"success": "Success", "success": "Success",
"submit": "Submit", "submit": "Submit",
"logout-success-text": "Logout successful!", "logout-success-text": "Logout successful!",
"not-found-header": "404 - Not found",
"not-found-body": "The page you were looking for, does not exist.",
"SU005": "Your password is updated successfully!", "SU005": "Your password is updated successfully!",
"SE001": "The settings are updated successfully!", "SE001": "The settings are updated successfully!",
"EG001": "An unexpected error occurred. Please try again later.", "EG001": "An unexpected error occurred. Please try again later.",
-14
View File
@@ -59,7 +59,6 @@
"react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-i18next": "^17.0.8", "react-i18next": "^17.0.8",
"react-toastify": "^11.1.0",
"tailwindcss": "^4.3.0", "tailwindcss": "^4.3.0",
"validator": "^13.15.35", "validator": "^13.15.35",
"zod": "^4.4.3" "zod": "^4.4.3"
@@ -6332,19 +6331,6 @@
"integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-toastify": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.1.0.tgz",
"integrity": "sha512-e9h23x3phN0wbFeB6yovmWp7lobzV4CaCH0LO8nVP6H7Y+3GbcLpIzMm9dJhcp1RXbpyfvjgpfXqO80QAmn7sg==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
}
},
"node_modules/react-transition-group": { "node_modules/react-transition-group": {
"version": "4.4.5", "version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",