{"id":19977128,"url":"https://github.com/adrianhajdin/refine_dashboard","last_synced_at":"2025-05-04T03:30:39.423Z","repository":{"id":237447888,"uuid":"744075154","full_name":"adrianhajdin/refine_dashboard","owner":"adrianhajdin","description":"Build an admin dashboard with full authentication, a homepage displaying charts and activities, a comprehensive table for companies with CRUD and search, and a Kanban board with real-time synchronization.","archived":false,"fork":false,"pushed_at":"2024-05-21T09:30:09.000Z","size":249,"stargazers_count":111,"open_issues_count":4,"forks_count":24,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-08T02:43:19.476Z","etag":null,"topics":["dashboard","react","refine"],"latest_commit_sha":null,"homepage":"https://refinix2-0.vercel.app/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/adrianhajdin.png","metadata":{"files":{"readme":"README.MD","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-01-16T15:18:58.000Z","updated_at":"2025-03-17T12:03:37.000Z","dependencies_parsed_at":"2024-05-02T00:57:34.281Z","dependency_job_id":"56ff6536-fc47-46c9-a3c1-c85de6ea7a00","html_url":"https://github.com/adrianhajdin/refine_dashboard","commit_stats":null,"previous_names":["adrianhajdin/refine_dashboard"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrianhajdin%2Frefine_dashboard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrianhajdin%2Frefine_dashboard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrianhajdin%2Frefine_dashboard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrianhajdin%2Frefine_dashboard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adrianhajdin","download_url":"https://codeload.github.com/adrianhajdin/refine_dashboard/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252283535,"owners_count":21723491,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dashboard","react","refine"],"created_at":"2024-11-13T03:26:58.071Z","updated_at":"2025-05-04T03:30:38.809Z","avatar_url":"https://github.com/adrianhajdin.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cbr /\u003e\n    \u003ca href=\"https://youtu.be/6a3Dz8gwjdg\" target=\"_blank\"\u003e\n      \u003cimg src=\"https://github.com/sujatagunale/EasyRead/assets/151519281/ad757d91-cdee-45ea-882e-4b19e3fd532f\" alt=\"Project Banner\"\u003e\n    \u003c/a\u003e\n  \u003cbr /\u003e\n\n  \u003cdiv\u003e\n    \u003cimg src=\"https://img.shields.io/badge/-React_JS-black?style=for-the-badge\u0026logoColor=white\u0026logo=react\u0026color=61DAFB\" alt=\"react.js\" /\u003e\n    \u003cimg src=\"https://img.shields.io/badge/-Refine-black?style=for-the-badge\u0026logoColor=white\u0026logo=refine\u0026color=14141F\" alt=\"refine\" /\u003e\n    \u003cimg src=\"https://img.shields.io/badge/-Ant_Design-black?style=for-the-badge\u0026logoColor=white\u0026logo=antdesign\u0026color=0170FE\" alt=\"antd\" /\u003e\n  \u003c/div\u003e\n\n  \u003ch3 align=\"center\"\u003eA CRM Dashboard\u003c/h3\u003e\n\n   \u003cdiv align=\"center\"\u003e\n     Build this project step by step with our detailed tutorial on \u003ca href=\"https://www.youtube.com/@javascriptmastery/videos\" target=\"_blank\"\u003e\u003cb\u003eJavaScript Mastery\u003c/b\u003e\u003c/a\u003e YouTube. Join the JSM family!\n    \u003c/div\u003e\n\u003c/div\u003e\n\n## 📋 \u003ca name=\"table\"\u003eTable of Contents\u003c/a\u003e\n\n1. 🤖 [Introduction](#introduction)\n2. ⚙️ [Tech Stack](#tech-stack)\n3. 🔋 [Features](#features)\n4. 🤸 [Quick Start](#quick-start)\n5. 🕸️ [Snippets](#snippets)\n6. 🔗 [Links](#links)\n7. 🚀 [More](#more)\n\n## 🚨 Tutorial\n\nThis repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, \u003ca href=\"https://www.youtube.com/@javascriptmastery/videos\" target=\"_blank\"\u003e\u003cb\u003eJavaScript Mastery\u003c/b\u003e\u003c/a\u003e. \n\nIf you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner!\n\n\u003ca href=\"https://youtu.be/6a3Dz8gwjdg\" target=\"_blank\"\u003e\u003cimg src=\"https://github.com/sujatagunale/EasyRead/assets/151519281/1736fca5-a031-4854-8c09-bc110e3bc16d\" /\u003e\u003c/a\u003e\n\n## \u003ca name=\"introduction\"\u003e🤖 Introduction\u003c/a\u003e\n\nReact-based CRM dashboard featuring comprehensive authentication, antd charts, sales management, and a fully operational kanban board with live updates for real-time actions across all devices.\n\nIf you're getting started and need assistance or face any bugs, join our active Discord community with over 27k+ members. It's a place where people help each other out.\n\n\u003ca href=\"https://discord.com/invite/n6EdbFJ\" target=\"_blank\"\u003e\u003cimg src=\"https://github.com/sujatagunale/EasyRead/assets/151519281/618f4872-1e10-42da-8213-1d69e486d02e\" /\u003e\u003c/a\u003e\n\n## \u003ca name=\"tech-stack\"\u003e⚙️ Tech Stack\u003c/a\u003e\n\n- React.js\n- TypeScript\n- GraphQL\n- Ant Design\n- Refine\n- Codegen\n- Vite\n\n## \u003ca name=\"features\"\u003e🔋 Features\u003c/a\u003e\n\n👉 **Authentication**: Seamless onboarding with secure login and signup functionalities; robust password recovery ensures a smooth authentication experience.\n\n👉 **Authorization**: Granular access control regulates user actions, maintaining data security and user permissions.\n\n👉 **Home Page**: Dynamic home page showcases interactive charts for key metrics; real-time updates on activities, upcoming events, and a deals chart for business insights.\n\n👉 **Companies Page**: Complete CRUD for company management and sales processes; detailed profiles with add/edit functions, associated contacts/leads, pagination, and field-specific search.\n\n👉 **Kanban Board**: Collaborative board with real-time task updates; customization options include due dates, markdown descriptions, and multi-assignees, dynamically shifting tasks across dashboards.\n\n👉 **Account Settings**: Personalized user account settings for profile management; streamlined configuration options for a tailored application experience.\n\n👉 **Responsive**: Full responsiveness across devices for consistent user experience; fluid design adapts seamlessly to various screen sizes, ensuring accessibility.\n\nand many more, including code architecture and reusability \n\n## \u003ca name=\"quick-start\"\u003e🤸 Quick Start\u003c/a\u003e\n\nFollow these steps to set up the project locally on your machine.\n\n**Prerequisites**\n\nMake sure you have the following installed on your machine:\n\n- [Git](https://git-scm.com/)\n- [Node.js](https://nodejs.org/en)\n- [npm](https://www.npmjs.com/) (Node Package Manager)\n\n**Cloning the Repository**\n\n```bash\ngit clone https://github.com/adrianhajdin/refine_dashboard.git\ncd refine_dashboard\n```\n\n**Installation**\n\nInstall the project dependencies using npm:\n\n```bash\nnpm install\n```\n\n\n**Running the Project**\n\n```bash\nnpm run dev\n```\n\nOpen [http://localhost:5173](http://localhost:5173) in your browser to view the project.\n\n## \u003ca name=\"snippets\"\u003e🕸️ Snippets\u003c/a\u003e\n\n# Code Snippets\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eproviders/auth.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport { AuthBindings } from \"@refinedev/core\";\n\nimport { API_URL, dataProvider } from \"./data\";\n\n// For demo purposes and to make it easier to test the app, you can use the following credentials\nexport const authCredentials = {\n  email: \"michael.scott@dundermifflin.com\",\n  password: \"demodemo\",\n};\n\nexport const authProvider: AuthBindings = {\n  login: async ({ email }) =\u003e {\n    try {\n      // call the login mutation\n      // dataProvider.custom is used to make a custom request to the GraphQL API\n      // this will call dataProvider which will go through the fetchWrapper function\n      const { data } = await dataProvider.custom({\n        url: API_URL,\n        method: \"post\",\n        headers: {},\n        meta: {\n          variables: { email },\n          // pass the email to see if the user exists and if so, return the accessToken\n          rawQuery: `\n            mutation Login($email: String!) {\n              login(loginInput: { email: $email }) {\n                accessToken\n              }\n            }\n          `,\n        },\n      });\n\n      // save the accessToken in localStorage\n      localStorage.setItem(\"access_token\", data.login.accessToken);\n\n      return {\n        success: true,\n        redirectTo: \"/\",\n      };\n    } catch (e) {\n      const error = e as Error;\n\n      return {\n        success: false,\n        error: {\n          message: \"message\" in error ? error.message : \"Login failed\",\n          name: \"name\" in error ? error.name : \"Invalid email or password\",\n        },\n      };\n    }\n  },\n\n  // simply remove the accessToken from localStorage for the logout\n  logout: async () =\u003e {\n    localStorage.removeItem(\"access_token\");\n\n    return {\n      success: true,\n      redirectTo: \"/login\",\n    };\n  },\n\n  onError: async (error) =\u003e {\n    // a check to see if the error is an authentication error\n    // if so, set logout to true\n    if (error.statusCode === \"UNAUTHENTICATED\") {\n      return {\n        logout: true,\n        ...error,\n      };\n    }\n\n    return { error };\n  },\n\n  check: async () =\u003e {\n    try {\n      //  get the identity of the user\n      // this is to know if the user is authenticated or not\n      await dataProvider.custom({\n        url: API_URL,\n        method: \"post\",\n        headers: {},\n        meta: {\n          rawQuery: `\n            query Me {\n              me {\n                name\n              }\n            }\n          `,\n        },\n      });\n\n      // if the user is authenticated, redirect to the home page\n      return {\n        authenticated: true,\n        redirectTo: \"/\",\n      };\n    } catch (error) {\n      // for any other error, redirect to the login page\n      return {\n        authenticated: false,\n        redirectTo: \"/login\",\n      };\n    }\n  },\n\n  // get the user information\n  getIdentity: async () =\u003e {\n    const accessToken = localStorage.getItem(\"access_token\");\n\n    try {\n      // call the GraphQL API to get the user information\n      // we're using me:any because the GraphQL API doesn't have a type for the me query yet.\n      // we'll add some queries and mutations later and change this to User which will be generated by codegen.\n      const { data } = await dataProvider.custom\u003c{ me: any }\u003e({\n        url: API_URL,\n        method: \"post\",\n        headers: accessToken\n          ? {\n              // send the accessToken in the Authorization header\n              Authorization: `Bearer ${accessToken}`,\n            }\n          : {},\n        meta: {\n          // get the user information such as name, email, etc.\n          rawQuery: `\n            query Me {\n              me {\n                id\n                name\n                email\n                phone\n                jobTitle\n                timezone\n                avatarUrl\n              }\n            }\n          `,\n        },\n      });\n\n      return data.me;\n    } catch (error) {\n      return undefined;\n    }\n  },\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eGraphQl and Codegen Setup\u003c/summary\u003e\n\n```bash\nnpm i -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/import-types-preset prettier vite-tsconfig-paths\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003egraphql.config.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport type { IGraphQLConfig } from \"graphql-config\";\n\nconst config: IGraphQLConfig = {\n  // define graphQL schema provided by Refine\n  schema: \"https://api.crm.refine.dev/graphql\",\n  extensions: {\n    // codegen is a plugin that generates typescript types from GraphQL schema\n    // https://the-guild.dev/graphql/codegen\n    codegen: {\n      // hooks are commands that are executed after a certain event\n      hooks: {\n        afterOneFileWrite: [\"eslint --fix\", \"prettier --write\"],\n      },\n      // generates typescript types from GraphQL schema\n      generates: {\n        // specify the output path of the generated types\n        \"src/graphql/schema.types.ts\": {\n          // use typescript plugin\n          plugins: [\"typescript\"],\n          // set the config of the typescript plugin\n          // this defines how the generated types will look like\n          config: {\n            skipTypename: true, // skipTypename is used to remove __typename from the generated types\n            enumsAsTypes: true, // enumsAsTypes is used to generate enums as types instead of enums.\n            // scalars is used to define how the scalars i.e. DateTime, JSON, etc. will be generated\n            // scalar is a type that is not a list and does not have fields. Meaning it is a primitive type.\n            scalars: {\n              // DateTime is a scalar type that is used to represent date and time\n              DateTime: {\n                input: \"string\",\n                output: \"string\",\n                format: \"date-time\",\n              },\n            },\n          },\n        },\n        // generates typescript types from GraphQL operations\n        // graphql operations are queries, mutations, and subscriptions we write in our code to communicate with the GraphQL API\n        \"src/graphql/types.ts\": {\n          // preset is a plugin that is used to generate typescript types from GraphQL operations\n          // import-types suggests to import types from schema.types.ts or other files\n          // this is used to avoid duplication of types\n          // https://the-guild.dev/graphql/codegen/plugins/presets/import-types-preset\n          preset: \"import-types\",\n          // documents is used to define the path of the files that contain GraphQL operations\n          documents: [\"src/**/*.{ts,tsx}\"],\n          // plugins is used to define the plugins that will be used to generate typescript types from GraphQL operations\n          plugins: [\"typescript-operations\"],\n          config: {\n            skipTypename: true,\n            enumsAsTypes: true,\n            // determine whether the generated types should be resolved ahead of time or not.\n            // When preResolveTypes is set to false, the code generator will not try to resolve the types ahead of time.\n            // Instead, it will generate more generic types, and the actual types will be resolved at runtime.\n            preResolveTypes: false,\n            // useTypeImports is used to import types using import type instead of import.\n            useTypeImports: true,\n          },\n          // presetConfig is used to define the config of the preset\n          presetConfig: {\n            typesPath: \"./schema.types\",\n          },\n        },\n      },\n    },\n  },\n};\n\nexport default config;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003egraphql/mutations.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport gql from \"graphql-tag\";\n\n// Mutation to update user\nexport const UPDATE_USER_MUTATION = gql`\n  # The ! after the type means that it is required\n  mutation UpdateUser($input: UpdateOneUserInput!) {\n    # call the updateOneUser mutation with the input and pass the $input argument\n    # $variableName is a convention for GraphQL variables\n    updateOneUser(input: $input) {\n      id\n      name\n      avatarUrl\n      email\n      phone\n      jobTitle\n    }\n  }\n`;\n\n// Mutation to create company\nexport const CREATE_COMPANY_MUTATION = gql`\n  mutation CreateCompany($input: CreateOneCompanyInput!) {\n    createOneCompany(input: $input) {\n      id\n      salesOwner {\n        id\n      }\n    }\n  }\n`;\n\n// Mutation to update company details\nexport const UPDATE_COMPANY_MUTATION = gql`\n  mutation UpdateCompany($input: UpdateOneCompanyInput!) {\n    updateOneCompany(input: $input) {\n      id\n      name\n      totalRevenue\n      industry\n      companySize\n      businessType\n      country\n      website\n      avatarUrl\n      salesOwner {\n        id\n        name\n        avatarUrl\n      }\n    }\n  }\n`;\n\n// Mutation to update task stage of a task\nexport const UPDATE_TASK_STAGE_MUTATION = gql`\n  mutation UpdateTaskStage($input: UpdateOneTaskInput!) {\n    updateOneTask(input: $input) {\n      id\n    }\n  }\n`;\n\n// Mutation to create a new task\nexport const CREATE_TASK_MUTATION = gql`\n  mutation CreateTask($input: CreateOneTaskInput!) {\n    createOneTask(input: $input) {\n      id\n      title\n      stage {\n        id\n        title\n      }\n    }\n  }\n`;\n\n// Mutation to update a task details\nexport const UPDATE_TASK_MUTATION = gql`\n  mutation UpdateTask($input: UpdateOneTaskInput!) {\n    updateOneTask(input: $input) {\n      id\n      title\n      completed\n      description\n      dueDate\n      stage {\n        id\n        title\n      }\n      users {\n        id\n        name\n        avatarUrl\n      }\n      checklist {\n        title\n        checked\n      }\n    }\n  }\n`;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003egraphql/queries.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport gql from \"graphql-tag\";\n\n// Query to get Total Company, Contact and Deal Counts\nexport const DASHBOARD_TOTAL_COUNTS_QUERY = gql`\n  query DashboardTotalCounts {\n    companies {\n      totalCount\n    }\n    contacts {\n      totalCount\n    }\n    deals {\n      totalCount\n    }\n  }\n`;\n\n// Query to get upcoming events\nexport const DASHBORAD_CALENDAR_UPCOMING_EVENTS_QUERY = gql`\n  query DashboardCalendarUpcomingEvents(\n    $filter: EventFilter!\n    $sorting: [EventSort!]\n    $paging: OffsetPaging!\n  ) {\n    events(filter: $filter, sorting: $sorting, paging: $paging) {\n      totalCount\n      nodes {\n        id\n        title\n        color\n        startDate\n        endDate\n      }\n    }\n  }\n`;\n\n// Query to get deals chart\nexport const DASHBOARD_DEALS_CHART_QUERY = gql`\n  query DashboardDealsChart(\n    $filter: DealStageFilter!\n    $sorting: [DealStageSort!]\n    $paging: OffsetPaging\n  ) {\n    dealStages(filter: $filter, sorting: $sorting, paging: $paging) {\n      # Get all deal stages\n      nodes {\n        id\n        title\n        # Get the sum of all deals in this stage and group by closeDateMonth and closeDateYear\n        dealsAggregate {\n          groupBy {\n            closeDateMonth\n            closeDateYear\n          }\n          sum {\n            value\n          }\n        }\n      }\n      # Get the total count of all deals in this stage\n      totalCount\n    }\n  }\n`;\n\n// Query to get latest activities deals\nexport const DASHBOARD_LATEST_ACTIVITIES_DEALS_QUERY = gql`\n  query DashboardLatestActivitiesDeals(\n    $filter: DealFilter!\n    $sorting: [DealSort!]\n    $paging: OffsetPaging\n  ) {\n    deals(filter: $filter, sorting: $sorting, paging: $paging) {\n      totalCount\n      nodes {\n        id\n        title\n        stage {\n          id\n          title\n        }\n        company {\n          id\n          name\n          avatarUrl\n        }\n        createdAt\n      }\n    }\n  }\n`;\n\n// Query to get latest activities audits\nexport const DASHBOARD_LATEST_ACTIVITIES_AUDITS_QUERY = gql`\n  query DashboardLatestActivitiesAudits(\n    $filter: AuditFilter!\n    $sorting: [AuditSort!]\n    $paging: OffsetPaging\n  ) {\n    audits(filter: $filter, sorting: $sorting, paging: $paging) {\n      totalCount\n      nodes {\n        id\n        action\n        targetEntity\n        targetId\n        changes {\n          field\n          from\n          to\n        }\n        createdAt\n        user {\n          id\n          name\n          avatarUrl\n        }\n      }\n    }\n  }\n`;\n\n// Query to get companies list\nexport const COMPANIES_LIST_QUERY = gql`\n  query CompaniesList(\n    $filter: CompanyFilter!\n    $sorting: [CompanySort!]\n    $paging: OffsetPaging!\n  ) {\n    companies(filter: $filter, sorting: $sorting, paging: $paging) {\n      totalCount\n      nodes {\n        id\n        name\n        avatarUrl\n        # Get the sum of all deals in this company\n        dealsAggregate {\n          sum {\n            value\n          }\n        }\n      }\n    }\n  }\n`;\n\n// Query to get users list\nexport const USERS_SELECT_QUERY = gql`\n  query UsersSelect(\n    $filter: UserFilter!\n    $sorting: [UserSort!]\n    $paging: OffsetPaging!\n  ) {\n    # Get all users\n    users(filter: $filter, sorting: $sorting, paging: $paging) {\n      totalCount # Get the total count of users\n      # Get specific fields for each user\n      nodes {\n        id\n        name\n        avatarUrl\n      }\n    }\n  }\n`;\n\n// Query to get contacts associated with a company\nexport const COMPANY_CONTACTS_TABLE_QUERY = gql`\n  query CompanyContactsTable(\n    $filter: ContactFilter!\n    $sorting: [ContactSort!]\n    $paging: OffsetPaging!\n  ) {\n    contacts(filter: $filter, sorting: $sorting, paging: $paging) {\n      totalCount\n      nodes {\n        id\n        name\n        avatarUrl\n        jobTitle\n        email\n        phone\n        status\n      }\n    }\n  }\n`;\n\n// Query to get task stages list\nexport const TASK_STAGES_QUERY = gql`\n  query TaskStages(\n    $filter: TaskStageFilter!\n    $sorting: [TaskStageSort!]\n    $paging: OffsetPaging!\n  ) {\n    taskStages(filter: $filter, sorting: $sorting, paging: $paging) {\n      totalCount # Get the total count of task stages\n      nodes {\n        id\n        title\n      }\n    }\n  }\n`;\n\n// Query to get tasks list\nexport const TASKS_QUERY = gql`\n  query Tasks(\n    $filter: TaskFilter!\n    $sorting: [TaskSort!]\n    $paging: OffsetPaging!\n  ) {\n    tasks(filter: $filter, sorting: $sorting, paging: $paging) {\n      totalCount # Get the total count of tasks\n      nodes {\n        id\n        title\n        description\n        dueDate\n        completed\n        stageId\n        # Get user details associated with this task\n        users {\n          id\n          name\n          avatarUrl\n        }\n        createdAt\n        updatedAt\n      }\n    }\n  }\n`;\n\n// Query to get task stages for select\nexport const TASK_STAGES_SELECT_QUERY = gql`\n  query TaskStagesSelect(\n    $filter: TaskStageFilter!\n    $sorting: [TaskStageSort!]\n    $paging: OffsetPaging!\n  ) {\n    taskStages(filter: $filter, sorting: $sorting, paging: $paging) {\n      totalCount\n      nodes {\n        id\n        title\n      }\n    }\n  }\n`;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003etext.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport React from \"react\";\n\nimport { ConfigProvider, Typography } from \"antd\";\n\nexport type TextProps = {\n  size?:\n    | \"xs\"\n    | \"sm\"\n    | \"md\"\n    | \"lg\"\n    | \"xl\"\n    | \"xxl\"\n    | \"xxxl\"\n    | \"huge\"\n    | \"xhuge\"\n    | \"xxhuge\";\n} \u0026 React.ComponentProps\u003ctypeof Typography.Text\u003e;\n\n// define the font sizes and line heights\nconst sizes = {\n  xs: {\n    fontSize: 12,\n    lineHeight: 20 / 12,\n  },\n  sm: {\n    fontSize: 14,\n    lineHeight: 22 / 14,\n  },\n  md: {\n    fontSize: 16,\n    lineHeight: 24 / 16,\n  },\n  lg: {\n    fontSize: 20,\n    lineHeight: 28 / 20,\n  },\n  xl: {\n    fontSize: 24,\n    lineHeight: 32 / 24,\n  },\n  xxl: {\n    fontSize: 30,\n    lineHeight: 38 / 30,\n  },\n  xxxl: {\n    fontSize: 38,\n    lineHeight: 46 / 38,\n  },\n  huge: {\n    fontSize: 46,\n    lineHeight: 54 / 46,\n  },\n  xhuge: {\n    fontSize: 56,\n    lineHeight: 64 / 56,\n  },\n  xxhuge: {\n    fontSize: 68,\n    lineHeight: 76 / 68,\n  },\n};\n\n// a custom Text component that wraps/extends the antd Typography.Text component\nexport const Text = ({ size = \"sm\", children, ...rest }: TextProps) =\u003e {\n  return (\n    // config provider is a top-level component that allows us to customize the global properties of antd components. For example, default antd theme\n    // token is a term used by antd to refer to the design tokens like font size, font weight, color, etc\n    // https://ant.design/docs/react/customize-theme#customize-design-token\n    \u003cConfigProvider\n      theme={{\n        token: {\n          ...sizes[size],\n        },\n      }}\n    \u003e\n      {/**\n       * Typography.Text is a component from antd that allows us to render text\n       * Typography has different components like Title, Paragraph, Text, Link, etc\n       * https://ant.design/components/typography/#Typography.Text\n       */}\n      \u003cTypography.Text {...rest}\u003e{children}\u003c/Typography.Text\u003e\n    \u003c/ConfigProvider\u003e\n  );\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003ecomponents/layout/account-settings.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport { SaveButton, useForm } from \"@refinedev/antd\";\nimport { HttpError } from \"@refinedev/core\";\nimport { GetFields, GetVariables } from \"@refinedev/nestjs-query\";\n\nimport { CloseOutlined } from \"@ant-design/icons\";\nimport { Button, Card, Drawer, Form, Input, Spin } from \"antd\";\n\nimport { getNameInitials } from \"@/utilities\";\nimport { UPDATE_USER_MUTATION } from \"@/graphql/mutations\";\n\nimport { Text } from \"../text\";\nimport CustomAvatar from \"../custom-avatar\";\n\nimport {\n  UpdateUserMutation,\n  UpdateUserMutationVariables,\n} from \"@/graphql/types\";\n\ntype Props = {\n  opened: boolean;\n  setOpened: (opened: boolean) =\u003e void;\n  userId: string;\n};\n\nexport const AccountSettings = ({ opened, setOpened, userId }: Props) =\u003e {\n  /**\n   * useForm in Refine is used to manage forms. It provides us with a lot of useful props and methods that we can use to manage forms.\n   * https://refine.dev/docs/data/hooks/use-form/#usage\n   */\n\n  /**\n   * saveButtonProps -\u003e contains all the props needed by the \"submit\" button. For example, \"loading\", \"disabled\", \"onClick\", etc.\n   * https://refine.dev/docs/ui-integrations/ant-design/hooks/use-form/#savebuttonprops\n   *\n   * formProps -\u003e It's an instance of HTML form that manages form state and actions like onFinish, onValuesChange, etc.\n   * https://refine.dev/docs/ui-integrations/ant-design/hooks/use-form/#form\n   *\n   * queryResult -\u003e contains the result of the query. For example, isLoading, data, error, etc.\n   * https://refine.dev/docs/packages/react-hook-form/use-form/#queryresult\n   */\n  const { saveButtonProps, formProps, queryResult } = useForm\u003c\n    /**\n     * GetFields is used to get the fields of the mutation i.e., in this case, fields are name, email, jobTitle, and phone\n     * https://refine.dev/docs/data/packages/nestjs-query/#getfields\n     */\n    GetFields\u003cUpdateUserMutation\u003e,\n    // a type that represents an HTTP error. Used to specify the type of error mutation can throw.\n    HttpError,\n    // A third type parameter used to specify the type of variables for the UpdateUserMutation. Meaning that the variables for the UpdateUserMutation should be of type UpdateUserMutationVariables\n    GetVariables\u003cUpdateUserMutationVariables\u003e\n  \u003e({\n    /**\n     * mutationMode is used to determine how the mutation should be performed. For example, optimistic, pessimistic, undoable etc.\n     * optimistic -\u003e redirection and UI updates are executed immediately as if the mutation is successful.\n     * pessimistic -\u003e redirection and UI updates are executed after the mutation is successful.\n     * https://refine.dev/docs/advanced-tutorials/mutation-mode/#overview\n     */\n    mutationMode: \"optimistic\",\n    /**\n     * specify on which resource the mutation should be performed\n     * if not specified, Refine will determine the resource name by the current route\n     */\n    resource: \"users\",\n    /**\n     * specify the action that should be performed on the resource. Behind the scenes, Refine calls useOne hook to get the data of the user for edit action.\n     * https://refine.dev/docs/data/hooks/use-form/#edit\n     */\n    action: \"edit\",\n    id: userId,\n    /**\n     * used to provide any additional information to the data provider.\n     * https://refine.dev/docs/data/hooks/use-form/#meta-\n     */\n    meta: {\n      // gqlMutation is used to specify the mutation that should be performed.\n      gqlMutation: UPDATE_USER_MUTATION,\n    },\n  });\n  const { avatarUrl, name } = queryResult?.data?.data || {};\n\n  const closeModal = () =\u003e {\n    setOpened(false);\n  };\n\n  // if query is processing, show a loading indicator\n  if (queryResult?.isLoading) {\n    return (\n      \u003cDrawer\n        open={opened}\n        width={756}\n        styles={{\n          body: {\n            background: \"#f5f5f5\",\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n          },\n        }}\n      \u003e\n        \u003cSpin /\u003e\n      \u003c/Drawer\u003e\n    );\n  }\n\n  return (\n    \u003cDrawer\n      onClose={closeModal}\n      open={opened}\n      width={756}\n      styles={{\n        body: { background: \"#f5f5f5\", padding: 0 },\n        header: { display: \"none\" },\n      }}\n    \u003e\n      \u003cdiv\n        style={{\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"space-between\",\n          padding: \"16px\",\n          backgroundColor: \"#fff\",\n        }}\n      \u003e\n        \u003cText strong\u003eAccount Settings\u003c/Text\u003e\n        \u003cButton\n          type=\"text\"\n          icon={\u003cCloseOutlined /\u003e}\n          onClick={() =\u003e closeModal()}\n        /\u003e\n      \u003c/div\u003e\n      \u003cdiv\n        style={{\n          padding: \"16px\",\n        }}\n      \u003e\n        \u003cCard\u003e\n          \u003cForm {...formProps} layout=\"vertical\"\u003e\n            \u003cCustomAvatar\n              shape=\"square\"\n              src={avatarUrl}\n              name={getNameInitials(name || \"\")}\n              style={{\n                width: 96,\n                height: 96,\n                marginBottom: \"24px\",\n              }}\n            /\u003e\n            \u003cForm.Item label=\"Name\" name=\"name\"\u003e\n              \u003cInput placeholder=\"Name\" /\u003e\n            \u003c/Form.Item\u003e\n            \u003cForm.Item label=\"Email\" name=\"email\"\u003e\n              \u003cInput placeholder=\"email\" /\u003e\n            \u003c/Form.Item\u003e\n            \u003cForm.Item label=\"Job title\" name=\"jobTitle\"\u003e\n              \u003cInput placeholder=\"jobTitle\" /\u003e\n            \u003c/Form.Item\u003e\n            \u003cForm.Item label=\"Phone\" name=\"phone\"\u003e\n              \u003cInput placeholder=\"Timezone\" /\u003e\n            \u003c/Form.Item\u003e\n          \u003c/Form\u003e\n          \u003cSaveButton\n            {...saveButtonProps}\n            style={{\n              display: \"block\",\n              marginLeft: \"auto\",\n            }}\n          /\u003e\n        \u003c/Card\u003e\n      \u003c/div\u003e\n    \u003c/Drawer\u003e\n  );\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003econstants/index.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport { AuditOutlined, ShopOutlined, TeamOutlined } from \"@ant-design/icons\";\n\nconst IconWrapper = ({\n  color,\n  children,\n}: React.PropsWithChildren\u003c{ color: string }\u003e) =\u003e {\n  return (\n    \u003cdiv\n      style={{\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        width: \"32px\",\n        height: \"32px\",\n        borderRadius: \"50%\",\n        backgroundColor: color,\n      }}\n    \u003e\n      {children}\n    \u003c/div\u003e\n  );\n};\n\nimport {\n  BusinessType,\n  CompanySize,\n  Contact,\n  Industry,\n} from \"@/graphql/schema.types\";\n\nexport type TotalCountType = \"companies\" | \"contacts\" | \"deals\";\n\nexport const totalCountVariants: {\n  [key in TotalCountType]: {\n    primaryColor: string;\n    secondaryColor?: string;\n    icon: React.ReactNode;\n    title: string;\n    data: { index: string; value: number }[];\n  };\n} = {\n  companies: {\n    primaryColor: \"#1677FF\",\n    secondaryColor: \"#BAE0FF\",\n    icon: (\n      \u003cIconWrapper color=\"#E6F4FF\"\u003e\n        \u003cShopOutlined\n          className=\"md\"\n          style={{\n            color: \"#1677FF\",\n          }}\n        /\u003e\n      \u003c/IconWrapper\u003e\n    ),\n    title: \"Number of companies\",\n    data: [\n      {\n        index: \"1\",\n        value: 3500,\n      },\n      {\n        index: \"2\",\n        value: 2750,\n      },\n      {\n        index: \"3\",\n        value: 5000,\n      },\n      {\n        index: \"4\",\n        value: 4250,\n      },\n      {\n        index: \"5\",\n        value: 5000,\n      },\n    ],\n  },\n  contacts: {\n    primaryColor: \"#52C41A\",\n    secondaryColor: \"#D9F7BE\",\n    icon: (\n      \u003cIconWrapper color=\"#F6FFED\"\u003e\n        \u003cTeamOutlined\n          className=\"md\"\n          style={{\n            color: \"#52C41A\",\n          }}\n        /\u003e\n      \u003c/IconWrapper\u003e\n    ),\n    title: \"Number of contacts\",\n    data: [\n      {\n        index: \"1\",\n        value: 10000,\n      },\n      {\n        index: \"2\",\n        value: 19500,\n      },\n      {\n        index: \"3\",\n        value: 13000,\n      },\n      {\n        index: \"4\",\n        value: 17000,\n      },\n      {\n        index: \"5\",\n        value: 13000,\n      },\n      {\n        index: \"6\",\n        value: 20000,\n      },\n    ],\n  },\n  deals: {\n    primaryColor: \"#FA541C\",\n    secondaryColor: \"#FFD8BF\",\n    icon: (\n      \u003cIconWrapper color=\"#FFF2E8\"\u003e\n        \u003cAuditOutlined\n          className=\"md\"\n          style={{\n            color: \"#FA541C\",\n          }}\n        /\u003e\n      \u003c/IconWrapper\u003e\n    ),\n    title: \"Total deals in pipeline\",\n    data: [\n      {\n        index: \"1\",\n        value: 1000,\n      },\n      {\n        index: \"2\",\n        value: 1300,\n      },\n      {\n        index: \"3\",\n        value: 1200,\n      },\n      {\n        index: \"4\",\n        value: 2000,\n      },\n      {\n        index: \"5\",\n        value: 800,\n      },\n      {\n        index: \"6\",\n        value: 1700,\n      },\n      {\n        index: \"7\",\n        value: 1400,\n      },\n      {\n        index: \"8\",\n        value: 1800,\n      },\n    ],\n  },\n};\n\nexport const statusOptions: {\n  label: string;\n  value: Contact[\"status\"];\n}[] = [\n  {\n    label: \"New\",\n    value: \"NEW\",\n  },\n  {\n    label: \"Qualified\",\n    value: \"QUALIFIED\",\n  },\n  {\n    label: \"Unqualified\",\n    value: \"UNQUALIFIED\",\n  },\n  {\n    label: \"Won\",\n    value: \"WON\",\n  },\n  {\n    label: \"Negotiation\",\n    value: \"NEGOTIATION\",\n  },\n  {\n    label: \"Lost\",\n    value: \"LOST\",\n  },\n  {\n    label: \"Interested\",\n    value: \"INTERESTED\",\n  },\n  {\n    label: \"Contacted\",\n    value: \"CONTACTED\",\n  },\n  {\n    label: \"Churned\",\n    value: \"CHURNED\",\n  },\n];\n\nexport const companySizeOptions: {\n  label: string;\n  value: CompanySize;\n}[] = [\n  {\n    label: \"Enterprise\",\n    value: \"ENTERPRISE\",\n  },\n  {\n    label: \"Large\",\n    value: \"LARGE\",\n  },\n  {\n    label: \"Medium\",\n    value: \"MEDIUM\",\n  },\n  {\n    label: \"Small\",\n    value: \"SMALL\",\n  },\n];\n\nexport const industryOptions: {\n  label: string;\n  value: Industry;\n}[] = [\n  { label: \"Aerospace\", value: \"AEROSPACE\" },\n  { label: \"Agriculture\", value: \"AGRICULTURE\" },\n  { label: \"Automotive\", value: \"AUTOMOTIVE\" },\n  { label: \"Chemicals\", value: \"CHEMICALS\" },\n  { label: \"Construction\", value: \"CONSTRUCTION\" },\n  { label: \"Defense\", value: \"DEFENSE\" },\n  { label: \"Education\", value: \"EDUCATION\" },\n  { label: \"Energy\", value: \"ENERGY\" },\n  { label: \"Financial Services\", value: \"FINANCIAL_SERVICES\" },\n  { label: \"Food and Beverage\", value: \"FOOD_AND_BEVERAGE\" },\n  { label: \"Government\", value: \"GOVERNMENT\" },\n  { label: \"Healthcare\", value: \"HEALTHCARE\" },\n  { label: \"Hospitality\", value: \"HOSPITALITY\" },\n  { label: \"Industrial Manufacturing\", value: \"INDUSTRIAL_MANUFACTURING\" },\n  { label: \"Insurance\", value: \"INSURANCE\" },\n  { label: \"Life Sciences\", value: \"LIFE_SCIENCES\" },\n  { label: \"Logistics\", value: \"LOGISTICS\" },\n  { label: \"Media\", value: \"MEDIA\" },\n  { label: \"Mining\", value: \"MINING\" },\n  { label: \"Nonprofit\", value: \"NONPROFIT\" },\n  { label: \"Other\", value: \"OTHER\" },\n  { label: \"Pharmaceuticals\", value: \"PHARMACEUTICALS\" },\n  { label: \"Professional Services\", value: \"PROFESSIONAL_SERVICES\" },\n  { label: \"Real Estate\", value: \"REAL_ESTATE\" },\n  { label: \"Retail\", value: \"RETAIL\" },\n  { label: \"Technology\", value: \"TECHNOLOGY\" },\n  { label: \"Telecommunications\", value: \"TELECOMMUNICATIONS\" },\n  { label: \"Transportation\", value: \"TRANSPORTATION\" },\n  { label: \"Utilities\", value: \"UTILITIES\" },\n];\n\nexport const businessTypeOptions: {\n  label: string;\n  value: BusinessType;\n}[] = [\n  {\n    label: \"B2B\",\n    value: \"B2B\",\n  },\n  {\n    label: \"B2C\",\n    value: \"B2C\",\n  },\n  {\n    label: \"B2G\",\n    value: \"B2G\",\n  },\n];\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003epages/company/contacts-table.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport { useParams } from \"react-router-dom\";\n\nimport { FilterDropdown, useTable } from \"@refinedev/antd\";\nimport { GetFieldsFromList } from \"@refinedev/nestjs-query\";\n\nimport {\n  MailOutlined,\n  PhoneOutlined,\n  SearchOutlined,\n  TeamOutlined,\n} from \"@ant-design/icons\";\nimport { Button, Card, Input, Select, Space, Table } from \"antd\";\n\nimport { Contact } from \"@/graphql/schema.types\";\n\nimport { statusOptions } from \"@/constants\";\nimport { COMPANY_CONTACTS_TABLE_QUERY } from \"@/graphql/queries\";\n\nimport { CompanyContactsTableQuery } from \"@/graphql/types\";\nimport { Text } from \"@/components/text\";\nimport CustomAvatar from \"@/components/custom-avatar\";\nimport { ContactStatusTag } from \"@/components/tags/contact-status-tag\";\n\nexport const CompanyContactsTable = () =\u003e {\n  // get params from the url\n  const params = useParams();\n\n  /**\n   * Refine offers a TanStack Table adapter with @refinedev/react-table that allows us to use the TanStack Table library with Refine.\n   * All features such as sorting, filtering, and pagination come out of the box\n   * Under the hood it uses useList hook to fetch the data.\n   * https://refine.dev/docs/packages/tanstack-table/use-table/#installation\n   */\n  const { tableProps } = useTable\u003cGetFieldsFromList\u003cCompanyContactsTableQuery\u003e\u003e(\n    {\n      // specify the resource for which the table is to be used\n      resource: \"contacts\",\n      syncWithLocation: false,\n      // specify initial sorters\n      sorters: {\n        /**\n         * initial sets the initial value of sorters.\n         * it's not permanent\n         * it will be cleared when the user changes the sorting\n         * https://refine.dev/docs/ui-integrations/ant-design/hooks/use-table/#sortersinitial\n         */\n        initial: [\n          {\n            field: \"createdAt\",\n            order: \"desc\",\n          },\n        ],\n      },\n      // specify initial filters\n      filters: {\n        /**\n         * similar to initial in sorters\n         * https://refine.dev/docs/ui-integrations/ant-design/hooks/use-table/#filtersinitial\n         */\n        initial: [\n          {\n            field: \"jobTitle\",\n            value: \"\",\n            operator: \"contains\",\n          },\n          {\n            field: \"name\",\n            value: \"\",\n            operator: \"contains\",\n          },\n          {\n            field: \"status\",\n            value: undefined,\n            operator: \"in\",\n          },\n        ],\n        /**\n         * permanent filters are the filters that are always applied\n         * https://refine.dev/docs/ui-integrations/ant-design/hooks/use-table/#filterspermanent\n         */\n        permanent: [\n          {\n            field: \"company.id\",\n            operator: \"eq\",\n            value: params?.id as string,\n          },\n        ],\n      },\n      /**\n       * used to provide any additional information to the data provider.\n       * https://refine.dev/docs/data/hooks/use-form/#meta-\n       */\n      meta: {\n        // gqlQuery is used to specify the GraphQL query that should be used to fetch the data.\n        gqlQuery: COMPANY_CONTACTS_TABLE_QUERY,\n      },\n    },\n  );\n\n  return (\n    \u003cCard\n      headStyle={{\n        borderBottom: \"1px solid #D9D9D9\",\n        marginBottom: \"1px\",\n      }}\n      bodyStyle={{ padding: 0 }}\n      title={\n        \u003cSpace size=\"middle\"\u003e\n          \u003cTeamOutlined /\u003e\n          \u003cText\u003eContacts\u003c/Text\u003e\n        \u003c/Space\u003e\n      }\n      // property used to render additional content in the top-right corner of the card\n      extra={\n        \u003c\u003e\n          \u003cText className=\"tertiary\"\u003eTotal contacts: \u003c/Text\u003e\n          \u003cText strong\u003e\n            {/* if pagination is not disabled and total is provided then show the total */}\n            {tableProps?.pagination !== false \u0026\u0026 tableProps.pagination?.total}\n          \u003c/Text\u003e\n        \u003c/\u003e\n      }\n    \u003e\n      \u003cTable\n        {...tableProps}\n        rowKey=\"id\"\n        pagination={{\n          ...tableProps.pagination,\n          showSizeChanger: false, // hide the page size changer\n        }}\n      \u003e\n        \u003cTable.Column\u003cContact\u003e\n          title=\"Name\"\n          dataIndex=\"name\"\n          render={(_, record) =\u003e (\n            \u003cSpace\u003e\n              \u003cCustomAvatar name={record.name} src={record.avatarUrl} /\u003e\n              \u003cText\n                style={{\n                  whiteSpace: \"nowrap\",\n                }}\n              \u003e\n                {record.name}\n              \u003c/Text\u003e\n            \u003c/Space\u003e\n          )}\n          // specify the icon that should be used for filtering\n          filterIcon={\u003cSearchOutlined /\u003e}\n          // render the filter dropdown\n          filterDropdown={(props) =\u003e (\n            \u003cFilterDropdown {...props}\u003e\n              \u003cInput placeholder=\"Search Name\" /\u003e\n            \u003c/FilterDropdown\u003e\n          )}\n        /\u003e\n        \u003cTable.Column\n          title=\"Title\"\n          dataIndex=\"jobTitle\"\n          filterIcon={\u003cSearchOutlined /\u003e}\n          filterDropdown={(props) =\u003e (\n            \u003cFilterDropdown {...props}\u003e\n              \u003cInput placeholder=\"Search Title\" /\u003e\n            \u003c/FilterDropdown\u003e\n          )}\n        /\u003e\n        \u003cTable.Column\u003cContact\u003e\n          title=\"Stage\"\n          dataIndex=\"status\"\n          // render the status tag for each contact\n          render={(_, record) =\u003e \u003cContactStatusTag status={record.status} /\u003e}\n          // allow filtering by selecting multiple status options\n          filterDropdown={(props) =\u003e (\n            \u003cFilterDropdown {...props}\u003e\n              \u003cSelect\n                style={{ width: \"200px\" }}\n                mode=\"multiple\" // allow multiple selection\n                placeholder=\"Select Stage\"\n                options={statusOptions}\n              \u003e\u003c/Select\u003e\n            \u003c/FilterDropdown\u003e\n          )}\n        /\u003e\n        \u003cTable.Column\u003cContact\u003e\n          dataIndex=\"id\"\n          width={112}\n          render={(_, record) =\u003e (\n            \u003cSpace\u003e\n              \u003cButton\n                size=\"small\"\n                href={`mailto:${record.email}`}\n                icon={\u003cMailOutlined /\u003e}\n              /\u003e\n              \u003cButton\n                size=\"small\"\n                href={`tel:${record.phone}`}\n                icon={\u003cPhoneOutlined /\u003e}\n              /\u003e\n            \u003c/Space\u003e\n          )}\n        /\u003e\n      \u003c/Table\u003e\n    \u003c/Card\u003e\n  );\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003ecomponents/tags/contact-status-tag.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport React from \"react\";\n\nimport {\n  CheckCircleOutlined,\n  MinusCircleOutlined,\n  PlayCircleFilled,\n  PlayCircleOutlined,\n} from \"@ant-design/icons\";\nimport { Tag, TagProps } from \"antd\";\n\nimport { ContactStatus } from \"@/graphql/schema.types\";\n\ntype Props = {\n  status: ContactStatus;\n};\n\n/**\n * Renders a tag component representing the contact status.\n * @param status - The contact status.\n */\nexport const ContactStatusTag = ({ status }: Props) =\u003e {\n  let icon: React.ReactNode = null;\n  let color: TagProps[\"color\"] = undefined;\n\n  switch (status) {\n    case \"NEW\":\n    case \"CONTACTED\":\n    case \"INTERESTED\":\n      icon = \u003cPlayCircleOutlined /\u003e;\n      color = \"cyan\";\n      break;\n\n    case \"UNQUALIFIED\":\n      icon = \u003cPlayCircleOutlined /\u003e;\n      color = \"red\";\n      break;\n\n    case \"QUALIFIED\":\n    case \"NEGOTIATION\":\n      icon = \u003cPlayCircleFilled /\u003e;\n      color = \"green\";\n      break;\n\n    case \"LOST\":\n      icon = \u003cPlayCircleFilled /\u003e;\n      color = \"red\";\n      break;\n\n    case \"WON\":\n      icon = \u003cCheckCircleOutlined /\u003e;\n      color = \"green\";\n      break;\n\n    case \"CHURNED\":\n      icon = \u003cMinusCircleOutlined /\u003e;\n      color = \"red\";\n      break;\n\n    default:\n      break;\n  }\n\n  return (\n    \u003cTag color={color} style={{ textTransform: \"capitalize\" }}\u003e\n      {icon} {status.toLowerCase()}\n    \u003c/Tag\u003e\n  );\n};\n```\n\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003ecomponents/text-icon.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport Icon from \"@ant-design/icons\";\nimport type { CustomIconComponentProps } from \"@ant-design/icons/lib/components/Icon\";\n\nexport const TextIconSvg = () =\u003e (\n  \u003csvg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width=\"12\"\n    height=\"12\"\n    viewBox=\"0 0 12 12\"\n    fill=\"none\"\n  \u003e\n    \u003cpath\n      d=\"M1.3125 2.25C1.26094 2.25 1.21875 2.29219 1.21875 2.34375V3C1.21875 3.05156 1.26094 3.09375 1.3125 3.09375H10.6875C10.7391 3.09375 10.7812 3.05156 10.7812 3V2.34375C10.7812 2.29219 10.7391 2.25 10.6875 2.25H1.3125Z\"\n      fill=\"black\"\n      fillOpacity=\"0.65\"\n    /\u003e\n    \u003cpath\n      d=\"M1.3125 5.57812C1.26094 5.57812 1.21875 5.62031 1.21875 5.67188V6.32812C1.21875 6.37969 1.26094 6.42188 1.3125 6.42188H10.6875C10.7391 6.42188 10.7812 6.37969 10.7812 6.32812V5.67188C10.7812 5.62031 10.7391 5.57812 10.6875 5.57812H1.3125Z\"\n      fill=\"black\"\n      fillOpacity=\"0.65\"\n    /\u003e\n    \u003cpath\n      d=\"M1.3125 8.90625C1.26094 8.90625 1.21875 8.94844 1.21875 9V9.65625C1.21875 9.70781 1.26094 9.75 1.3125 9.75H7.6875C7.73906 9.75 7.78125 9.70781 7.78125 9.65625V9C7.78125 8.94844 7.73906 8.90625 7.6875 8.90625H1.3125Z\"\n      fill=\"black\"\n      fillOpacity=\"0.65\"\n    /\u003e\n  \u003c/svg\u003e\n);\n\nexport const TextIcon = (props: Partial\u003cCustomIconComponentProps\u003e) =\u003e (\n  \u003cIcon component={TextIconSvg} {...props} /\u003e\n);\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003ecomponents/tasks/kanban/add-card-button.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport React from \"react\";\n\nimport { PlusSquareOutlined } from \"@ant-design/icons\";\nimport { Button } from \"antd\";\nimport { Text } from \"@/components/text\";\n\ninterface Props {\n  onClick: () =\u003e void;\n}\n\n/** Render a button that allows you to add a new card to a column.\n *\n * @param onClick - a function that is called when the button is clicked.\n * @returns a button that allows you to add a new card to a column.\n */\nexport const KanbanAddCardButton = ({\n  children,\n  onClick,\n}: React.PropsWithChildren\u003cProps\u003e) =\u003e {\n  return (\n    \u003cButton\n      size=\"large\"\n      icon={\u003cPlusSquareOutlined className=\"md\" /\u003e}\n      style={{\n        margin: \"16px\",\n        backgroundColor: \"white\",\n      }}\n      onClick={onClick}\n    \u003e\n      {children ?? (\n        \u003cText size=\"md\" type=\"secondary\"\u003e\n          Add new card\n        \u003c/Text\u003e\n      )}\n    \u003c/Button\u003e\n  );\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003epages/tasks/create.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport { useSearchParams } from \"react-router-dom\";\n\nimport { useModalForm } from \"@refinedev/antd\";\nimport { useNavigation } from \"@refinedev/core\";\n\nimport { Form, Input, Modal } from \"antd\";\n\nimport { CREATE_TASK_MUTATION } from \"@/graphql/mutations\";\n\nconst TasksCreatePage = () =\u003e {\n  // get search params from the url\n  const [searchParams] = useSearchParams();\n\n  /**\n   * useNavigation is a hook by Refine that allows you to navigate to a page.\n   * https://refine.dev/docs/routing/hooks/use-navigation/\n   *\n   * list method navigates to the list page of the specified resource.\n   * https://refine.dev/docs/routing/hooks/use-navigation/#list\n   */ const { list } = useNavigation();\n\n  /**\n   * useModalForm is a hook by Refine that allows you manage a form inside a modal.\n   * it extends the useForm hook from the @refinedev/antd package\n   * https://refine.dev/docs/ui-integrations/ant-design/hooks/use-modal-form/\n   *\n   * formProps -\u003e It's an instance of HTML form that manages form state and actions like onFinish, onValuesChange, etc.\n   * Under the hood, it uses the useForm hook from the @refinedev/antd package\n   * https://refine.dev/docs/ui-integrations/ant-design/hooks/use-modal-form/#formprops\n   *\n   * modalProps -\u003e It's an instance of Modal that manages modal state and actions like onOk, onCancel, etc.\n   * https://refine.dev/docs/ui-integrations/ant-design/hooks/use-modal-form/#modalprops\n   */\n  const { formProps, modalProps, close } = useModalForm({\n    // specify the action to perform i.e., create or edit\n    action: \"create\",\n    // specify whether the modal should be visible by default\n    defaultVisible: true,\n    // specify the gql mutation to be performed\n    meta: {\n      gqlMutation: CREATE_TASK_MUTATION,\n    },\n  });\n\n  return (\n    \u003cModal\n      {...modalProps}\n      onCancel={() =\u003e {\n        // close the modal\n        close();\n\n        // navigate to the list page of the tasks resource\n        list(\"tasks\", \"replace\");\n      }}\n      title=\"Add new card\"\n      width={512}\n    \u003e\n      \u003cForm\n        {...formProps}\n        layout=\"vertical\"\n        onFinish={(values) =\u003e {\n          // on finish, call the onFinish method of useModalForm to perform the mutation\n          formProps?.onFinish?.({\n            ...values,\n            stageId: searchParams.get(\"stageId\")\n              ? Number(searchParams.get(\"stageId\"))\n              : null,\n            userIds: [],\n          });\n        }}\n      \u003e\n        \u003cForm.Item label=\"Title\" name=\"title\" rules={[{ required: true }]}\u003e\n          \u003cInput /\u003e\n        \u003c/Form.Item\u003e\n      \u003c/Form\u003e\n    \u003c/Modal\u003e\n  );\n}\n\nexport default TasksCreatePage;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003epages/tasks/edit.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport { useState } from \"react\";\n\nimport { DeleteButton, useModalForm } from \"@refinedev/antd\";\nimport { useNavigation } from \"@refinedev/core\";\n\nimport {\n  AlignLeftOutlined,\n  FieldTimeOutlined,\n  UsergroupAddOutlined,\n} from \"@ant-design/icons\";\nimport { Modal } from \"antd\";\n\nimport {\n  Accordion,\n  DescriptionForm,\n  DescriptionHeader,\n  DueDateForm,\n  DueDateHeader,\n  StageForm,\n  TitleForm,\n  UsersForm,\n  UsersHeader,\n} from \"@/components\";\nimport { Task } from \"@/graphql/schema.types\";\n\nimport { UPDATE_TASK_MUTATION } from \"@/graphql/mutations\";\n\nconst TasksEditPage = () =\u003e {\n  const [activeKey, setActiveKey] = useState\u003cstring | undefined\u003e();\n\n  // use the list method to navigate to the list page of the tasks resource from the navigation hook\n  const { list } = useNavigation();\n\n  // create a modal form to edit a task using the useModalForm hook\n  // modalProps -\u003e It's an instance of Modal that manages modal state and actions like onOk, onCancel, etc.\n  // close -\u003e It's a function that closes the modal\n  // queryResult -\u003e It's an instance of useQuery from react-query\n  const { modalProps, close, queryResult } = useModalForm\u003cTask\u003e({\n    // specify the action to perform i.e., create or edit\n    action: \"edit\",\n    // specify whether the modal should be visible by default\n    defaultVisible: true,\n    // specify the gql mutation to be performed\n    meta: {\n      gqlMutation: UPDATE_TASK_MUTATION,\n    },\n  });\n\n  // get the data of the task from the queryResult\n  const { description, dueDate, users, title } = queryResult?.data?.data ?? {};\n\n  const isLoading = queryResult?.isLoading ?? true;\n\n  return (\n    \u003cModal\n      {...modalProps}\n      className=\"kanban-update-modal\"\n      onCancel={() =\u003e {\n        close();\n        list(\"tasks\", \"replace\");\n      }}\n      title={\u003cTitleForm initialValues={{ title }} isLoading={isLoading} /\u003e}\n      width={586}\n      footer={\n        \u003cDeleteButton\n          type=\"link\"\n          onSuccess={() =\u003e {\n            list(\"tasks\", \"replace\");\n          }}\n        \u003e\n          Delete card\n        \u003c/DeleteButton\u003e\n      }\n    \u003e\n      {/* Render the stage form */}\n      \u003cStageForm isLoading={isLoading} /\u003e\n\n      {/* Render the description form inside an accordion */}\n      \u003cAccordion\n        accordionKey=\"description\"\n        activeKey={activeKey}\n        setActive={setActiveKey}\n        fallback={\u003cDescriptionHeader description={description} /\u003e}\n        isLoading={isLoading}\n        icon={\u003cAlignLeftOutlined /\u003e}\n        label=\"Description\"\n      \u003e\n        \u003cDescriptionForm\n          initialValues={{ description }}\n          cancelForm={() =\u003e setActiveKey(undefined)}\n        /\u003e\n      \u003c/Accordion\u003e\n\n      {/* Render the due date form inside an accordion */}\n      \u003cAccordion\n        accordionKey=\"due-date\"\n        activeKey={activeKey}\n        setActive={setActiveKey}\n        fallback={\u003cDueDateHeader dueData={dueDate} /\u003e}\n        isLoading={isLoading}\n        icon={\u003cFieldTimeOutlined /\u003e}\n        label=\"Due date\"\n      \u003e\n        \u003cDueDateForm\n          initialValues={{ dueDate: dueDate ?? undefined }}\n          cancelForm={() =\u003e setActiveKey(undefined)}\n        /\u003e\n      \u003c/Accordion\u003e\n\n      {/* Render the users form inside an accordion */}\n      \u003cAccordion\n        accordionKey=\"users\"\n        activeKey={activeKey}\n        setActive={setActiveKey}\n        fallback={\u003cUsersHeader users={users} /\u003e}\n        isLoading={isLoading}\n        icon={\u003cUsergroupAddOutlined /\u003e}\n        label=\"Users\"\n      \u003e\n        \u003cUsersForm\n          initialValues={{\n            userIds: users?.map((user) =\u003e ({\n              label: user.name,\n              value: user.id,\n            })),\n          }}\n          cancelForm={() =\u003e setActiveKey(undefined)}\n        /\u003e\n      \u003c/Accordion\u003e\n    \u003c/Modal\u003e\n  );\n};\n\nexport default TasksEditPage;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003ecomponents/accordion.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport { AccordionHeaderSkeleton } from \"@/components\";\nimport { Text } from \"./text\";\n\ntype Props = React.PropsWithChildren\u003c{\n  accordionKey: string;\n  activeKey?: string;\n  setActive: (key?: string) =\u003e void;\n  fallback: string | React.ReactNode;\n  isLoading?: boolean;\n  icon: React.ReactNode;\n  label: string;\n}\u003e;\n\n/**\n * when activeKey is equal to accordionKey, the children will be rendered. Otherwise, the fallback will be rendered\n * when isLoading is true, the \u003cAccordionHeaderSkeleton /\u003e will be rendered\n * when Accordion is clicked, setActive will be called with the accordionKey\n */\nexport const Accordion = ({\n  accordionKey,\n  activeKey,\n  setActive,\n  fallback,\n  icon,\n  label,\n  children,\n  isLoading,\n}: Props) =\u003e {\n  if (isLoading) return \u003cAccordionHeaderSkeleton /\u003e;\n\n  const isActive = activeKey === accordionKey;\n\n  const toggleAccordion = () =\u003e {\n    if (isActive) {\n      setActive(undefined);\n    } else {\n      setActive(accordionKey);\n    }\n  };\n\n  return (\n    \u003cdiv\n      style={{\n        display: \"flex\",\n        padding: \"12px 24px\",\n        gap: \"12px\",\n        alignItems: \"start\",\n        borderBottom: \"1px solid #d9d9d9\",\n      }}\n    \u003e\n      \u003cdiv style={{ marginTop: \"1px\", flexShrink: 0 }}\u003e{icon}\u003c/div\u003e\n      {isActive ? (\n        \u003cdiv\n          style={{\n            display: \"flex\",\n            flexDirection: \"column\",\n            gap: \"12px\",\n            flex: 1,\n          }}\n        \u003e\n          \u003cText strong onClick={toggleAccordion} style={{ cursor: \"pointer\" }}\u003e\n            {label}\n          \u003c/Text\u003e\n          {children}\n        \u003c/div\u003e\n      ) : (\n        \u003cdiv onClick={toggleAccordion} style={{ cursor: \"pointer\", flex: 1 }}\u003e\n          {fallback}\n        \u003c/div\u003e\n      )}\n    \u003c/div\u003e\n  );\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003ecomponents/tags/user-tag.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport { Space, Tag } from \"antd\";\n\nimport { User } from \"@/graphql/schema.types\";\nimport CustomAvatar from \"../custom-avatar\";\n\ntype Props = {\n  user: User;\n};\n\n// display a user's avatar and name in a tag\nexport const UserTag = ({ user }: Props) =\u003e {\n  return (\n    \u003cTag\n      key={user.id}\n      style={{\n        padding: 2,\n        paddingRight: 8,\n        borderRadius: 24,\n        lineHeight: \"unset\",\n        marginRight: \"unset\",\n      }}\n    \u003e\n      \u003cSpace size={4}\u003e\n        \u003cCustomAvatar\n          src={user.avatarUrl}\n          name={user.name}\n          style={{ display: \"inline-flex\" }}\n        /\u003e\n        {user.name}\n      \u003c/Space\u003e\n    \u003c/Tag\u003e\n  );\n};\n```\n\n\u003c/details\u003e\n\n## \u003ca name=\"links\"\u003e🔗 Links\u003c/a\u003e\n\nOther components (Kanban Edit Forms, Skeletons and utilities) used in the project can be found [here](https://drive.google.com/drive/folders/1oDFoI-a8qSJqHde6doUtM9NCHlYpWdNW?usp=sharing)\n\n## \u003ca name=\"more\"\u003e🚀 More\u003c/a\u003e\n\n**Advance your skills with Next.js 14 Pro Course**\n\nEnjoyed creating this project? Dive deeper into our PRO courses for a richer learning adventure. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go!\n\n\u003ca href=\"https://jsmastery.pro/next14\" target=\"_blank\"\u003e\n\u003cimg src=\"https://github.com/sujatagunale/EasyRead/assets/151519281/557837ce-f612-4530-ab24-189e75133c71\" alt=\"Project Banner\"\u003e\n\u003c/a\u003e\n\n\u003cbr /\u003e\n\u003cbr /\u003e\n\n**Accelerate your professional journey with the Expert Training program**\n\nAnd if you're hungry for more than just a course and want to understand how we learn and tackle tech challenges, hop into our personalized masterclass. We cover best practices, different web skills, and offer mentorship to boost your confidence. Let's learn and grow together!\n\n\u003ca href=\"https://www.jsmastery.pro/masterclass\" target=\"_blank\"\u003e\n\u003cimg src=\"https://github.com/sujatagunale/EasyRead/assets/151519281/fed352ad-f27b-400d-9b8f-c7fe628acb84\" alt=\"Project Banner\"\u003e\n\u003c/a\u003e\n\n#\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadrianhajdin%2Frefine_dashboard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadrianhajdin%2Frefine_dashboard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadrianhajdin%2Frefine_dashboard/lists"}