Changeset e9a0543 for frontend


Ignore:
Timestamp:
05/05/26 17:55:27 (2 months ago)
Author:
Andrej <asumanovski@…>
Branches:
master
Children:
b3d5fed
Parents:
4f85b41
Message:

Custom tracking category feature complete

Location:
frontend
Files:
5 added
5 edited

Legend:

Unmodified
Added
Removed
  • frontend/package-lock.json

    r4f85b41 re9a0543  
    99      "version": "0.0.0",
    1010      "dependencies": {
     11        "@dnd-kit/core": "^6.3.1",
     12        "@dnd-kit/sortable": "^10.0.0",
     13        "@dnd-kit/utilities": "^3.2.2",
    1114        "@relume_io/relume-tailwind": "^1.3.0",
    1215        "@relume_io/relume-ui": "^1.3.1",
     
    337340      "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
    338341      "license": "MIT"
     342    },
     343    "node_modules/@dnd-kit/accessibility": {
     344      "version": "3.1.1",
     345      "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
     346      "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
     347      "license": "MIT",
     348      "dependencies": {
     349        "tslib": "^2.0.0"
     350      },
     351      "peerDependencies": {
     352        "react": ">=16.8.0"
     353      }
     354    },
     355    "node_modules/@dnd-kit/core": {
     356      "version": "6.3.1",
     357      "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
     358      "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
     359      "license": "MIT",
     360      "dependencies": {
     361        "@dnd-kit/accessibility": "^3.1.1",
     362        "@dnd-kit/utilities": "^3.2.2",
     363        "tslib": "^2.0.0"
     364      },
     365      "peerDependencies": {
     366        "react": ">=16.8.0",
     367        "react-dom": ">=16.8.0"
     368      }
     369    },
     370    "node_modules/@dnd-kit/sortable": {
     371      "version": "10.0.0",
     372      "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
     373      "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
     374      "license": "MIT",
     375      "dependencies": {
     376        "@dnd-kit/utilities": "^3.2.2",
     377        "tslib": "^2.0.0"
     378      },
     379      "peerDependencies": {
     380        "@dnd-kit/core": "^6.3.0",
     381        "react": ">=16.8.0"
     382      }
     383    },
     384    "node_modules/@dnd-kit/utilities": {
     385      "version": "3.2.2",
     386      "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
     387      "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
     388      "license": "MIT",
     389      "dependencies": {
     390        "tslib": "^2.0.0"
     391      },
     392      "peerDependencies": {
     393        "react": ">=16.8.0"
     394      }
    339395    },
    340396    "node_modules/@esbuild/aix-ppc64": {
  • frontend/package.json

    r4f85b41 re9a0543  
    1111  },
    1212  "dependencies": {
     13    "@dnd-kit/core": "^6.3.1",
     14    "@dnd-kit/sortable": "^10.0.0",
     15    "@dnd-kit/utilities": "^3.2.2",
    1316    "@relume_io/relume-tailwind": "^1.3.0",
    1417    "@relume_io/relume-ui": "^1.3.1",
  • frontend/src/App.jsx

    r4f85b41 re9a0543  
    2020import Discipline from "./pages/Dashboard/pages/Discipline/Discipline.jsx";
    2121import DisciplineTracking from "./pages/Dashboard/pages/Discipline/DisciplineTracking.jsx";
     22import CustomCategoryKanban from "./pages/Dashboard/pages/CustomCategory/CustomCategoryKanban.jsx";
    2223
    2324function RequireAuth({ children }) {
     
    6162          <Route path="discipline" element={<Discipline />} />
    6263           <Route path="discipline/tracking" element={<DisciplineTracking />} />
     64           <Route path="custom/:customTrackingId" element={<CustomCategoryKanban />} />
    6365        </Route>
    6466        <Route path="*" element={<Navigate to="/" replace />} />
  • frontend/src/api/discipline.js

    r4f85b41 re9a0543  
    3636}
    3737
     38export async function getCustomTrackingCategories() {
     39  const res = await api.get("/discipline/custom-tracking-categories");
     40  return res?.data;
     41}
     42
     43export async function createCustomTrackingCategory(payload) {
     44  const res = await api.post("/discipline/custom-tracking-categories", payload);
     45  return res?.data;
     46}
     47
     48export async function getCustomCategoryTasks(customTrackingId) {
     49  const res = await api.get(
     50    `/discipline/custom-tracking-categories/${customTrackingId}/tasks`
     51  );
     52  return res?.data;
     53}
     54
     55export async function createCustomCategoryTask(customTrackingId, payload) {
     56  const res = await api.post(
     57    `/discipline/custom-tracking-categories/${customTrackingId}/tasks`,
     58    payload
     59  );
     60  return res?.data;
     61}
     62
     63export async function updateCustomCategoryTask(customTrackingId, taskId, payload) {
     64  const res = await api.put(
     65    `/discipline/custom-tracking-categories/${customTrackingId}/tasks/${taskId}`,
     66    payload
     67  );
     68  return res?.data;
     69}
     70
     71export async function updateCustomCategoryTaskFinished(
     72  customTrackingId,
     73  taskId,
     74  isFinished
     75) {
     76  const res = await api.patch(
     77    `/discipline/custom-tracking-categories/${customTrackingId}/tasks/${taskId}/finished`,
     78    { isFinished }
     79  );
     80  return res?.data;
     81}
     82
     83export async function deleteCustomCategoryTask(customTrackingId, taskId) {
     84  return api.delete(
     85    `/discipline/custom-tracking-categories/${customTrackingId}/tasks/${taskId}`
     86  );
     87}
     88
  • frontend/src/pages/Dashboard/components/DashboardSidebar.jsx

    r4f85b41 re9a0543  
    1 import React from "react";
     1import React, { useEffect, useMemo, useState } from "react";
    22import { NavLink, useLocation } from "react-router-dom";
    33
     
    2020  MdChecklist,
    2121  MdAdd,
     22  MdBook,
     23  MdFlag,
     24  MdStar,
     25  MdBolt,
     26  MdNoteAlt,
     27  MdAutoGraph,
    2228} from "react-icons/md";
    2329
    2430import logo from "../../../assets/logo.png";
     31
     32import AddCustomCategoryModal from "./AddCustomCategoryModal.jsx";
     33import {
     34  createCustomTrackingCategory,
     35  getCustomTrackingCategories,
     36} from "../../../api/discipline.js";
    2537
    2638const navItems = [
     
    3345];
    3446
     47const CUSTOM_ICONS = [MdBook, MdFlag, MdStar, MdBolt, MdNoteAlt, MdAutoGraph];
     48
     49function stableIconForId(id) {
     50  const n = Number(id);
     51  if (!Number.isFinite(n)) return CUSTOM_ICONS[0];
     52  return CUSTOM_ICONS[Math.abs(n) % CUSTOM_ICONS.length];
     53}
     54
    3555const DashboardSidebar = () => {
    3656  const location = useLocation();
     57  const [customOpen, setCustomOpen] = useState(false);
     58  const [customCategories, setCustomCategories] = useState([]);
     59
     60  useEffect(() => {
     61    let mounted = true;
     62    (async () => {
     63      try {
     64        const res = await getCustomTrackingCategories();
     65        const items = res?.items ?? [];
     66        if (mounted) setCustomCategories(items);
     67      } catch {
     68        // If the user isn't authenticated yet or the endpoint fails,
     69        // keep the sidebar usable (no custom categories).
     70        if (mounted) setCustomCategories([]);
     71      }
     72    })();
     73    return () => {
     74      mounted = false;
     75    };
     76  }, []);
     77
     78  const customNavItems = useMemo(() => {
     79    return (customCategories ?? []).map((c) => {
     80      const Icon = stableIconForId(c.customTrackingId);
     81      return {
     82        title: c.name,
     83        to: `/dashboard/custom/${c.customTrackingId}`,
     84        icon: Icon,
     85      };
     86    });
     87  }, [customCategories]);
    3788
    3889  return (
     
    52103          {navItems.map((item) => (
    53104            <SidebarMenuItem key={item.title}>
     105              {(() => {
     106                const isActive =
     107                  location.pathname === item.to ||
     108                  location.pathname.startsWith(`${item.to}/`);
     109
     110                return (
     111                  <SidebarMenuButton
     112                    asChild
     113                    isActive={isActive}
     114                    className={
     115                      isActive
     116                        ? "!bg-green-400 !text-black hover:!bg-green-400 active:!bg-green-400"
     117                        : ""
     118                    }
     119                  >
     120                    <NavLink
     121                      to={item.to}
     122                      className="flex w-full items-center gap-3"
     123                    >
     124                      <item.icon
     125                        className={
     126                          isActive
     127                            ? "size-6 shrink-0 text-black"
     128                            : "size-6 shrink-0"
     129                        }
     130                      />
     131                      <span className={isActive ? "text-black" : ""}>
     132                        {item.title}
     133                      </span>
     134                    </NavLink>
     135                  </SidebarMenuButton>
     136                );
     137              })()}
     138            </SidebarMenuItem>
     139          ))}
     140
     141          {customNavItems.length > 0 ? (
     142            <div className="my-4 border-t border-white/10" />
     143          ) : null}
     144
     145          {customNavItems.map((item) => (
     146            <SidebarMenuItem key={`custom-${item.to}`}>
    54147              {(() => {
    55148                const isActive =
     
    94187            className="w-full justify-start gap-2 !bg-green-400 !text-black hover:!bg-green-500"
    95188            type="button"
     189            onClick={() => setCustomOpen(true)}
    96190          >
    97191            <MdAdd className="size-5" />
     
    99193          </Button>
    100194        </div>
     195
     196        <AddCustomCategoryModal
     197          open={customOpen}
     198          onClose={() => setCustomOpen(false)}
     199          onSubmit={async (payload) => {
     200            const created = await createCustomTrackingCategory(payload);
     201            setCustomCategories((prev) => [created, ...(prev ?? [])]);
     202          }}
     203        />
    101204      </SidebarContent>
    102205    </Sidebar>
Note: See TracChangeset for help on using the changeset viewer.