diff --git a/.cursor/skills/danak-admin/SKILL.md b/.cursor/skills/danak-admin/SKILL.md new file mode 100644 index 0000000..6a52769 --- /dev/null +++ b/.cursor/skills/danak-admin/SKILL.md @@ -0,0 +1,109 @@ +--- +name: danak-admin +description: Documents danak-admin React admin panel conventions — page structure, routing, TanStack Query hooks, axios services, sidebar, and Persian i18n. Use when adding pages or features, wiring API calls, updating navigation, or working in this repository. +--- + +# Danak Admin + +Persian (RTL) admin panel: React 19, Vite, TypeScript, React Router 7, TanStack Query, Formik/Yup, Tailwind, iconsax-react. + +## Stack + +| Layer | Location / tool | +|-------|-----------------| +| Routes (paths) | `src/config/Pages.ts` | +| Route components | `src/router/Main.tsx` | +| Auth routes | `src/router/Auth.tsx` | +| HTTP client | `src/config/axios.ts` — `import axios from "../../../config/axios"` | +| Shared UI | `src/components/` | +| Layout | `src/shared/` (Header, SideBar, Footer) | +| i18n | `src/langs/fa.json`, namespace `global`, `useTranslation('global')` | +| Env | `VITE_BASE_URL`, `VITE_TOKEN_NAME`, `VITE_REFRESH_TOKEN_NAME` | + +## Feature folder layout + +Each domain lives under `src/pages/{feature}/`: + +``` +{feature}/ +├── List.tsx # index table view +├── Create.tsx # create form +├── Update.tsx # edit form (uses :id param) +├── hooks/use{Feature}Data.ts +├── service/{Feature}Service.ts +├── types/{Feature}Types.ts +└── components/ # feature-specific UI (Delete, filters, etc.) +``` + +Mirror an existing feature (e.g. `customer/`, `ads/`) before inventing new patterns. + +## Data layer + +**Service** — plain async functions, one API call each: + +```typescript +import axios from "../../../config/axios"; +import { CreateCustomerType } from "../types/CustomerTypes"; + +export const getCustomers = async (page: number) => { + const { data } = await axios.get(`/users/customers?page=${page}&paginate=1`); + return data; +}; + +export const createCustomer = async (params: CreateCustomerType) => { + const { data } = await axios.post(`/users/customers`, params); + return data; +}; +``` + +**Hooks** — TanStack Query wrappers in `hooks/use{Feature}Data.ts`: + +- Lists: `useQuery({ queryKey: ["customers", page], queryFn: () => api.getCustomers(page) })` +- Detail: add `enabled: !!id` +- Mutations: `useMutation` + `queryClient.invalidateQueries({ queryKey: ["customers"] })` +- Export hooks as `useGetX`, `useCreateX`, `useUpdateX`, `useDeleteX` + +**Types** — `{Feature}ItemType`, `Create{Feature}Type` in `types/`. + +## Page patterns + +**List** — header + optional filters + `PageLoading` while pending + `` with `Td` + `Pagination`. Link actions via `Pages.{feature}.*`. + +**Create / Update** — Formik + Yup validation, `toast.success(t('success'))` on success, `toast.error(error.response?.data?.error.message[0])` on error (`ErrorType` from `src/helpers/types.ts`). Navigate back to list with `useNavigate()` + `Pages.*`. + +**Delete** — small component using `ModalConfrim` + mutation hook; call `refetch()` or rely on query invalidation. + +## Adding a feature + +Follow the checklist in [add-feature.md](add-feature.md). Minimum steps: + +1. Add paths to `src/config/Pages.ts` +2. Create page files under `src/pages/{feature}/` +3. Register `` entries in `src/router/Main.tsx` (static imports, no lazy loading) +4. Add sidebar entry in `src/shared/SideBar.tsx` if user-facing +5. Add translation keys to `src/langs/fa.json` + +Dynamic routes use string concat: `Pages.customer.update + ":id"`. + +## Sidebar & submenu + +- Main items: `SideBarItem` in `SideBar.tsx` with `link`, `isActive`, `activeName` +- Features with sub-nav: add segment to `SideBarItemHasSubMenu` in `src/config/SideBarSubMenu.ts`, create `{Feature}SubMenu.tsx` in `src/shared/components/`, render it in the `subMenuName === "..."` block at bottom of `SideBar.tsx` +- Submenu items use `SubMenuItem` with `Pages.*` links +- `subMenuName` is derived from first URL segment (`location.pathname.split("/")[1]`) + +## Conventions + +- Components: `const MyPage: FC = () => { ... }; export default MyPage` +- Styling: Tailwind utility classes; `clx()` from `src/helpers/utils` for conditional classes +- Icons: `iconsax-react` (Outline/Bold variants, `#8C90A3` inactive / `black` active) +- Do not add lazy-loaded routes — project uses direct imports in `Main.tsx` +- Keep API paths relative to `VITE_BASE_URL`; auth token is attached automatically by axios interceptor +- Prefer reusing `Button`, `Input`, `Select`, `Textarea`, `Pagination`, `PageLoading`, `ModalConfrim` from `src/components/` + +## Do not + +- Commit `.env` or secrets +- Introduce new state libraries (Zustand is only used in `sharedStore` for layout) +- Add English-only UI strings — add keys to `fa.json` +- Change axios/auth/token refresh logic unless explicitly requested diff --git a/.cursor/skills/danak-admin/add-feature.md b/.cursor/skills/danak-admin/add-feature.md new file mode 100644 index 0000000..2501acd --- /dev/null +++ b/.cursor/skills/danak-admin/add-feature.md @@ -0,0 +1,83 @@ +# Add Feature Checklist + +Copy and track when adding a new admin section (e.g. `coupons`): + +``` +- [ ] Pages.ts paths +- [ ] types/{Feature}Types.ts +- [ ] service/{Feature}Service.ts +- [ ] hooks/use{Feature}Data.ts +- [ ] List.tsx (+ Create/Update if CRUD) +- [ ] components/ (Delete, filters, etc.) +- [ ] Main.tsx routes + imports +- [ ] SideBar.tsx (+ SubMenu if multi-page) +- [ ] fa.json translations +``` + +## 1. Pages.ts + +```typescript +coupons: { + list: "/coupons/list", + create: "/coupons/create", + detail: "/coupons/detail/", +}, +``` + +Use kebab-case URL segments consistent with existing routes. + +## 2. Service + hooks + types + +Copy structure from `src/pages/customer/` or `src/pages/ads/`. Adjust API paths to match backend. + +## 3. Main.tsx + +Add import at top, then route(s): + +```tsx +import CouponsList from "../pages/coupons/List"; +// ... +} /> +} /> +} /> +``` + +## 4. Sidebar (optional) + +**Single-page feature** — add `SideBarItem`: + +```tsx +} + title={t("sidebar.coupons")} + isActive={isActive("coupons")} + link={Pages.coupons.list} + activeName="coupons" +/> +``` + +**Multi-page feature** — also: + +1. Add `"coupons"` to `SideBarItemHasSubMenu` in `src/config/SideBarSubMenu.ts` +2. Create `src/shared/components/CouponsSubMenu.tsx` (copy `CustomerSubMenu.tsx`) +3. Import and add branch in `SideBar.tsx`: `subMenuName === "coupons" ? : ...` +4. Point main sidebar link to list path; first URL segment must match `activeName` / submenu key + +## 5. Translations + +Add keys under logical groups in `src/langs/fa.json`: + +```json +"sidebar": { "coupons": "کوپن‌ها" }, +"submenu": { "coupon_list": "لیست کوپن‌ها" }, +"coupon": { "coupon_list": "لیست کوپن‌ها", "add_coupon": "افزودن کوپن" } +``` + +Use `t('coupon.add_coupon')` in components. + +## 6. Verify + +- `npm run build` passes +- List loads, create/update navigate correctly +- Sidebar highlights on correct routes +- Mutation invalidates list query cache diff --git a/src/config/SideBarSubMenu.ts b/src/config/SideBarSubMenu.ts index f734f50..bcdeacf 100644 --- a/src/config/SideBarSubMenu.ts +++ b/src/config/SideBarSubMenu.ts @@ -8,4 +8,5 @@ export const SideBarItemHasSubMenu = [ "blog", "support", "accessLogs", + "taskmanager", ]; diff --git a/src/langs/fa.json b/src/langs/fa.json index 53a0510..b12c21a 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -143,7 +143,11 @@ "guide": "راهنمای سرویس", "access_logs_list": "لیست لاگ‌ها", "access_logs_stats": "آمار لاگ‌ها", - "access_logs_errors": "لاگ‌های خطا" + "access_logs_errors": "لاگ‌های خطا", + "taskmanager_workspace": "فضای کاری", + "taskmanager_create_workspace": "ساخت فضای کاری جدید", + "taskmanager_project_list": "لیست پروژه‌ها", + "taskmanager_create_project": "افزودن پروژه" }, "guide": { "list_guide": "لیست راهنمای سرویس", @@ -972,6 +976,7 @@ "submit_workspace": "ثبت فضای کاری", "workspacename": "نام", "workspacetype": "نوع", + "workspace_info": "اطلاعات", "workspace_description": "توضیحات", "workspace_status": "وضعیت فضای کار", "choose_color": "انتخاب رنگ دلخواه", diff --git a/src/pages/taskmanager/workspace/Create.tsx b/src/pages/taskmanager/workspace/Create.tsx index 28f1198..f3261b4 100644 --- a/src/pages/taskmanager/workspace/Create.tsx +++ b/src/pages/taskmanager/workspace/Create.tsx @@ -1,20 +1,71 @@ +import { FC, useState } from "react"; import { TickCircle } from "iconsax-react"; +import { useFormik } from "formik"; import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import * as Yup from "yup"; import Button from "../../../components/Button"; import Input from "../../../components/Input"; -import Select from "../../../components/Select"; import Textarea from "../../../components/Textarea"; -import CreateWorkspaceSidebar from "./components/CreateWorkspaceSidebar"; +import { Pages } from "../../../config/Pages"; +import { ErrorType } from "../../../helpers/types"; +import { useCreateWorkspace } from "./hooks/useWorkspaceData"; +import { CreateWorkspaceType } from "./types/WorkspaceTypes"; +import CreateWorkspaceSidebar, { + SelectedUser, +} from "./components/CreateWorkspaceSidebar"; -const CreateWorkspace = () => { +const CreateWorkspace: FC = () => { const { t } = useTranslation("global"); + const navigate = useNavigate(); + const createWorkspace = useCreateWorkspace(); + + const [isActive, setIsActive] = useState(true); + const [selectedColor, setSelectedColor] = useState("#A8E6CF"); + const [selectedUsers, setSelectedUsers] = useState([]); + + const handleCreateWorkspace = (values: CreateWorkspaceType) => { + createWorkspace.mutate(values, { + onSuccess: () => { + toast.success(t("success")); + navigate(Pages.taskmanager.workspace + "dpage"); + }, + onError: (error: ErrorType) => { + toast.error(error.response?.data?.error.message[0]); + }, + }); + }; + + const formik = useFormik>({ + initialValues: { + name: "", + description: "", + }, + validationSchema: Yup.object({ + name: Yup.string().required(t("errors.required")), + description: Yup.string(), + }), + onSubmit: (values) => { + handleCreateWorkspace({ + ...values, + color: selectedColor, + isActive, + userIds: selectedUsers.map((user) => user.id), + }); + }, + }); return (
{t("taskmanager.new_workspace")}
-