diff --git a/.pnp.cjs b/.pnp.cjs index 23326a13..ac7f3e3d 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -3004,6 +3004,7 @@ const RAW_RUNTIME_STATE = ["linkify-react", "virtual:9ef42ff9c873460955cc48cd9b15127324f3d1f83a4bea8e6327df0101bb993bef095b175f8d10a3f0d23ee47f702ca3ef7272cba815f708e8609d03d84b96a2#npm:4.1.3"],\ ["linkifyjs", "npm:4.1.3"],\ ["nanoid", "npm:5.0.4"],\ + ["rdndmb-html5-to-touch", "npm:8.0.3"],\ ["react", "npm:18.2.0"],\ ["react-dom", "virtual:de80dc576383b2386358abc0e9fe49c00e3397fe355a0337462b73ab3115c2e557eb85784ee0fe776394cc11dd020b4e84dbbd75acf72ee6d54415d82d21f5c5#npm:18.2.0"],\ ["react-hot-toast", "virtual:9ef42ff9c873460955cc48cd9b15127324f3d1f83a4bea8e6327df0101bb993bef095b175f8d10a3f0d23ee47f702ca3ef7272cba815f708e8609d03d84b96a2#npm:2.4.1"],\ @@ -3040,6 +3041,137 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@dnd-kit/accessibility", [\ + ["npm:3.1.0", {\ + "packageLocation": "./.yarn/cache/@dnd-kit-accessibility-npm-3.1.0-c746ff31d6-4f9d24e801.zip/node_modules/@dnd-kit/accessibility/",\ + "packageDependencies": [\ + ["@dnd-kit/accessibility", "npm:3.1.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:6bf0421413cf10005f0e036c5f66c498340579f89c81403e024ace4de57497d9986177f4ebaa4711134cf1b8244d8a9d60fcb63b1f424ef415fe226687b0cf9b#npm:3.1.0", {\ + "packageLocation": "./.yarn/__virtual__/@dnd-kit-accessibility-virtual-d39a470f5f/0/cache/@dnd-kit-accessibility-npm-3.1.0-c746ff31d6-4f9d24e801.zip/node_modules/@dnd-kit/accessibility/",\ + "packageDependencies": [\ + ["@dnd-kit/accessibility", "virtual:6bf0421413cf10005f0e036c5f66c498340579f89c81403e024ace4de57497d9986177f4ebaa4711134cf1b8244d8a9d60fcb63b1f424ef415fe226687b0cf9b#npm:3.1.0"],\ + ["@types/react", "npm:18.2.48"],\ + ["react", "npm:18.2.0"],\ + ["tslib", "npm:2.6.2"]\ + ],\ + "packagePeers": [\ + "@types/react",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@dnd-kit/core", [\ + ["npm:6.1.0", {\ + "packageLocation": "./.yarn/cache/@dnd-kit-core-npm-6.1.0-13c1618df7-c793eb97cb.zip/node_modules/@dnd-kit/core/",\ + "packageDependencies": [\ + ["@dnd-kit/core", "npm:6.1.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:6.1.0", {\ + "packageLocation": "./.yarn/__virtual__/@dnd-kit-core-virtual-6bf0421413/0/cache/@dnd-kit-core-npm-6.1.0-13c1618df7-c793eb97cb.zip/node_modules/@dnd-kit/core/",\ + "packageDependencies": [\ + ["@dnd-kit/core", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:6.1.0"],\ + ["@dnd-kit/accessibility", "virtual:6bf0421413cf10005f0e036c5f66c498340579f89c81403e024ace4de57497d9986177f4ebaa4711134cf1b8244d8a9d60fcb63b1f424ef415fe226687b0cf9b#npm:3.1.0"],\ + ["@dnd-kit/utilities", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:3.2.2"],\ + ["@types/react", "npm:18.2.48"],\ + ["@types/react-dom", "npm:18.2.18"],\ + ["react", "npm:18.2.0"],\ + ["react-dom", "virtual:de80dc576383b2386358abc0e9fe49c00e3397fe355a0337462b73ab3115c2e557eb85784ee0fe776394cc11dd020b4e84dbbd75acf72ee6d54415d82d21f5c5#npm:18.2.0"],\ + ["tslib", "npm:2.6.2"]\ + ],\ + "packagePeers": [\ + "@types/react-dom",\ + "@types/react",\ + "react-dom",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@dnd-kit/modifiers", [\ + ["npm:7.0.0", {\ + "packageLocation": "./.yarn/cache/@dnd-kit-modifiers-npm-7.0.0-a682febbbe-542e1d2b61.zip/node_modules/@dnd-kit/modifiers/",\ + "packageDependencies": [\ + ["@dnd-kit/modifiers", "npm:7.0.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:7.0.0", {\ + "packageLocation": "./.yarn/__virtual__/@dnd-kit-modifiers-virtual-3427504d72/0/cache/@dnd-kit-modifiers-npm-7.0.0-a682febbbe-542e1d2b61.zip/node_modules/@dnd-kit/modifiers/",\ + "packageDependencies": [\ + ["@dnd-kit/modifiers", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:7.0.0"],\ + ["@dnd-kit/core", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:6.1.0"],\ + ["@dnd-kit/utilities", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:3.2.2"],\ + ["@types/dnd-kit__core", null],\ + ["@types/react", "npm:18.2.48"],\ + ["react", "npm:18.2.0"],\ + ["tslib", "npm:2.6.2"]\ + ],\ + "packagePeers": [\ + "@dnd-kit/core",\ + "@types/dnd-kit__core",\ + "@types/react",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@dnd-kit/sortable", [\ + ["npm:8.0.0", {\ + "packageLocation": "./.yarn/cache/@dnd-kit-sortable-npm-8.0.0-2d428bbda3-a6066c652b.zip/node_modules/@dnd-kit/sortable/",\ + "packageDependencies": [\ + ["@dnd-kit/sortable", "npm:8.0.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:8.0.0", {\ + "packageLocation": "./.yarn/__virtual__/@dnd-kit-sortable-virtual-13c752f3a0/0/cache/@dnd-kit-sortable-npm-8.0.0-2d428bbda3-a6066c652b.zip/node_modules/@dnd-kit/sortable/",\ + "packageDependencies": [\ + ["@dnd-kit/sortable", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:8.0.0"],\ + ["@dnd-kit/core", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:6.1.0"],\ + ["@dnd-kit/utilities", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:3.2.2"],\ + ["@types/dnd-kit__core", null],\ + ["@types/react", "npm:18.2.48"],\ + ["react", "npm:18.2.0"],\ + ["tslib", "npm:2.6.2"]\ + ],\ + "packagePeers": [\ + "@dnd-kit/core",\ + "@types/dnd-kit__core",\ + "@types/react",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@dnd-kit/utilities", [\ + ["npm:3.2.2", {\ + "packageLocation": "./.yarn/cache/@dnd-kit-utilities-npm-3.2.2-3fe8307947-9aa90526f3.zip/node_modules/@dnd-kit/utilities/",\ + "packageDependencies": [\ + ["@dnd-kit/utilities", "npm:3.2.2"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:3.2.2", {\ + "packageLocation": "./.yarn/__virtual__/@dnd-kit-utilities-virtual-a3a66ba2b4/0/cache/@dnd-kit-utilities-npm-3.2.2-3fe8307947-9aa90526f3.zip/node_modules/@dnd-kit/utilities/",\ + "packageDependencies": [\ + ["@dnd-kit/utilities", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:3.2.2"],\ + ["@types/react", "npm:18.2.48"],\ + ["react", "npm:18.2.0"],\ + ["tslib", "npm:2.6.2"]\ + ],\ + "packagePeers": [\ + "@types/react",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@emotion/babel-plugin", [\ ["npm:11.11.0", {\ "packageLocation": "./.yarn/cache/@emotion-babel-plugin-npm-11.11.0-c1dcc4c884-89cbb6ec0e.zip/node_modules/@emotion/babel-plugin/",\ @@ -5654,6 +5786,24 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@react-dnd/asap", [\ + ["npm:5.0.2", {\ + "packageLocation": "./.yarn/cache/@react-dnd-asap-npm-5.0.2-66021d3d61-0063db616d.zip/node_modules/@react-dnd/asap/",\ + "packageDependencies": [\ + ["@react-dnd/asap", "npm:5.0.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@react-dnd/invariant", [\ + ["npm:4.0.2", {\ + "packageLocation": "./.yarn/cache/@react-dnd-invariant-npm-4.0.2-826eacc1ea-b303cc53fc.zip/node_modules/@react-dnd/invariant/",\ + "packageDependencies": [\ + ["@react-dnd/invariant", "npm:4.0.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@react-pdf/fns", [\ ["npm:2.2.1", {\ "packageLocation": "./.yarn/cache/@react-pdf-fns-npm-2.2.1-77536ed89f-457bdff57e.zip/node_modules/@react-pdf/fns/",\ @@ -8541,6 +8691,10 @@ const RAW_RUNTIME_STATE = ["@boolti/icon", "workspace:packages/icon"],\ ["@boolti/typescript-config", "workspace:packages/config-typescript"],\ ["@boolti/ui", "workspace:packages/ui"],\ + ["@dnd-kit/core", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:6.1.0"],\ + ["@dnd-kit/modifiers", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:7.0.0"],\ + ["@dnd-kit/sortable", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:8.0.0"],\ + ["@dnd-kit/utilities", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:3.2.2"],\ ["@emotion/babel-plugin", "npm:11.11.0"],\ ["@emotion/react", "virtual:de80dc576383b2386358abc0e9fe49c00e3397fe355a0337462b73ab3115c2e557eb85784ee0fe776394cc11dd020b4e84dbbd75acf72ee6d54415d82d21f5c5#npm:11.11.3"],\ ["@emotion/styled", "virtual:85869d3eba7afdb6f94c001c9503942ddc4354e881daf63c24e9d58366ea9f25c6bac2df65ae0f5266c54cd36fe68f0d9568da3a1ab62446405c98ac852f4431#npm:11.11.0"],\ @@ -10326,6 +10480,40 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["dnd-core", [\ + ["npm:16.0.1", {\ + "packageLocation": "./.yarn/cache/dnd-core-npm-16.0.1-552224cee0-6b852c576c.zip/node_modules/dnd-core/",\ + "packageDependencies": [\ + ["dnd-core", "npm:16.0.1"],\ + ["@react-dnd/asap", "npm:5.0.2"],\ + ["@react-dnd/invariant", "npm:4.0.2"],\ + ["redux", "npm:4.2.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["dnd-multi-backend", [\ + ["npm:8.0.3", {\ + "packageLocation": "./.yarn/cache/dnd-multi-backend-npm-8.0.3-77b57f51ee-4395fd8278.zip/node_modules/dnd-multi-backend/",\ + "packageDependencies": [\ + ["dnd-multi-backend", "npm:8.0.3"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:1586002902c50f74c93bfc4432d5e9af4b44f1b1eb4b98750b8199eba8f53742e35a69aa3f476fc38f5ef4b29df79e58cd9bc0e0b0fe33fa0debed3793886ce3#npm:8.0.3", {\ + "packageLocation": "./.yarn/__virtual__/dnd-multi-backend-virtual-a4e91b52b7/0/cache/dnd-multi-backend-npm-8.0.3-77b57f51ee-4395fd8278.zip/node_modules/dnd-multi-backend/",\ + "packageDependencies": [\ + ["dnd-multi-backend", "virtual:1586002902c50f74c93bfc4432d5e9af4b44f1b1eb4b98750b8199eba8f53742e35a69aa3f476fc38f5ef4b29df79e58cd9bc0e0b0fe33fa0debed3793886ce3#npm:8.0.3"],\ + ["@types/dnd-core", null],\ + ["dnd-core", null]\ + ],\ + "packagePeers": [\ + "@types/dnd-core",\ + "dnd-core"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["doctrine", [\ ["npm:2.1.0", {\ "packageLocation": "./.yarn/cache/doctrine-npm-2.1.0-ac15d049b7-b6416aaff1.zip/node_modules/doctrine/",\ @@ -16136,6 +16324,18 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["rdndmb-html5-to-touch", [\ + ["npm:8.0.3", {\ + "packageLocation": "./.yarn/cache/rdndmb-html5-to-touch-npm-8.0.3-1586002902-85aa39516e.zip/node_modules/rdndmb-html5-to-touch/",\ + "packageDependencies": [\ + ["rdndmb-html5-to-touch", "npm:8.0.3"],\ + ["dnd-multi-backend", "virtual:1586002902c50f74c93bfc4432d5e9af4b44f1b1eb4b98750b8199eba8f53742e35a69aa3f476fc38f5ef4b29df79e58cd9bc0e0b0fe33fa0debed3793886ce3#npm:8.0.3"],\ + ["react-dnd-html5-backend", "npm:16.0.1"],\ + ["react-dnd-touch-backend", "npm:16.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["react", [\ ["npm:18.2.0", {\ "packageLocation": "./.yarn/cache/react-npm-18.2.0-1eae08fee2-b562d9b569.zip/node_modules/react/",\ @@ -16234,6 +16434,27 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["react-dnd-html5-backend", [\ + ["npm:16.0.1", {\ + "packageLocation": "./.yarn/cache/react-dnd-html5-backend-npm-16.0.1-754940d855-6e4b632a11.zip/node_modules/react-dnd-html5-backend/",\ + "packageDependencies": [\ + ["react-dnd-html5-backend", "npm:16.0.1"],\ + ["dnd-core", "npm:16.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["react-dnd-touch-backend", [\ + ["npm:16.0.1", {\ + "packageLocation": "./.yarn/cache/react-dnd-touch-backend-npm-16.0.1-2b96ba84be-8809614693.zip/node_modules/react-dnd-touch-backend/",\ + "packageDependencies": [\ + ["react-dnd-touch-backend", "npm:16.0.1"],\ + ["@react-dnd/invariant", "npm:4.0.2"],\ + ["dnd-core", "npm:16.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["react-docgen", [\ ["npm:7.0.3", {\ "packageLocation": "./.yarn/cache/react-docgen-npm-7.0.3-ea0f679a0f-74622750e6.zip/node_modules/react-docgen/",\ @@ -16964,6 +17185,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["redux", [\ + ["npm:4.2.1", {\ + "packageLocation": "./.yarn/cache/redux-npm-4.2.1-e7e2cf2e37-136d98b3d5.zip/node_modules/redux/",\ + "packageDependencies": [\ + ["redux", "npm:4.2.1"],\ + ["@babel/runtime", "npm:7.23.9"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["regenerate", [\ ["npm:1.4.2", {\ "packageLocation": "./.yarn/cache/regenerate-npm-1.4.2-b296c5b63a-f73c9eba5d.zip/node_modules/regenerate/",\ diff --git a/apps/admin/package.json b/apps/admin/package.json index 9432d0e0..cc43cfd1 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -15,6 +15,10 @@ "@boolti/api": "*", "@boolti/icon": "*", "@boolti/ui": "*", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/modifiers": "^7.0.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@react-pdf/renderer": "^3.4.4", diff --git a/apps/admin/src/components/EnteranceTable/EnteranceTable.styles.ts b/apps/admin/src/components/EnteranceTable/EnteranceTable.styles.ts index 796262a2..e1bdd7a7 100644 --- a/apps/admin/src/components/EnteranceTable/EnteranceTable.styles.ts +++ b/apps/admin/src/components/EnteranceTable/EnteranceTable.styles.ts @@ -1,102 +1,56 @@ import { Button } from '@boolti/ui'; import styled from '@emotion/styled'; -const Container = styled.div` - display: flex; - flex-direction: column; - margin: 16px 0; +const Container = styled.table` + table-layout: auto; + margin-bottom: 16px; width: ${({ theme }) => theme.breakpoint.desktop}; - height: 547px; `; -const HeaderRow = styled.div` - display: flex; - flex: 0 0 auto; - flex-wrap: nowrap; +const HeaderRow = styled.tr` + width: 100%; padding-left: 8px; background-color: ${({ theme }) => theme.palette.grey.g00}; border-top: 1px solid ${({ theme }) => theme.palette.grey.g20}; border-bottom: 1px solid ${({ theme }) => theme.palette.grey.g20}; `; -const HeaderItem = styled.span` - display: block; - flex: 0 0 auto; +const HeaderItem = styled.th` + white-space: nowrap; text-align: left; padding: 12px; ${({ theme }) => theme.typo.b2}; color: ${({ theme }) => theme.palette.grey.g60}; - &:not(:last-of-type) { - margin-right: 12px; - } - &:nth-of-type(1) { - width: 108px; - } - &:nth-of-type(2) { - width: 80px; - } - &:nth-of-type(3) { - width: 180px; - } - &:nth-of-type(4) { - width: 100px; - } - &:nth-of-type(5) { - width: 140px; - } - &:nth-of-type(6) { - width: 80px; - } - &:nth-of-type(7) { - width: 148px; + &:last-child { + width: 100% !important; } `; -const Row = styled.div` - display: flex; - flex-wrap: nowrap; +const Row = styled.tr` + width: 100%; padding-left: 8px; border-bottom: 1px solid ${({ theme }) => theme.palette.grey.g20}; `; -const Item = styled.span` - display: block; - flex: 0 0 auto; - padding: 14px 12px; +const Item = styled.td` + padding: 14px; text-align: left; ${({ theme }) => theme.typo.b2}; color: ${({ theme }) => theme.palette.grey.g90}; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; & strong { background-color: ${({ theme }) => theme.palette.primary.o0}; } - &:not(:last-of-type) { - margin-right: 12px; - } - &:nth-of-type(1) { - width: 108px; - } - &:nth-of-type(2) { - width: 80px; - } - &:nth-of-type(3) { - width: 180px; - } - &:nth-of-type(4) { - width: 100px; - } - &:nth-of-type(5) { - width: 140px; - } - &:nth-of-type(6) { - width: 80px; - } - &:nth-of-type(7) { - width: 148px; - } `; const Empty = styled.div` + position: absolute; + top: 50%; + left: 50%; display: flex; + transform: translateX(-50%) translateY(46px); flex-direction: column; flex: 1; justify-content: center; @@ -112,6 +66,16 @@ const ResetButton = styled(Button)` margin-top: 18px; `; +const DisabledText = styled.span` + ${({ theme }) => theme.typo.b2}; + color: ${({ theme }) => theme.palette.grey.g30}; +`; + +const SearchResult = styled.span` + white-space: nowrap; + overflow: hidden; +`; + export default { Container, HeaderItem, @@ -120,4 +84,6 @@ export default { Item, Empty, ResetButton, + DisabledText, + SearchResult, }; diff --git a/apps/admin/src/components/EnteranceTable/index.tsx b/apps/admin/src/components/EnteranceTable/index.tsx index 8a134eda..8dcf380b 100644 --- a/apps/admin/src/components/EnteranceTable/index.tsx +++ b/apps/admin/src/components/EnteranceTable/index.tsx @@ -1,4 +1,3 @@ -import { EntranceResponse } from '@boolti/api'; import { createColumnHelper, flexRender, @@ -11,55 +10,69 @@ import { boldText } from '~/utils/boldText'; import { formatPhoneNumber } from '~/utils/format'; import Styled from './EnteranceTable.styles'; +import { TicketWithReservationResponse } from '@boolti/api/src/types/adminTicket'; -const columnHelper = createColumnHelper(); +const columnHelper = createColumnHelper(); const columns = [ columnHelper.accessor('csTicketId', { header: '티켓 번호', + minSize: 120, }), - columnHelper.accessor('ticketType', { - header: '티켓 종류', - cell: (props) => `${props.getValue() === 'INVITE' ? '초청' : '일반'}티켓`, - }), - columnHelper.accessor('ticketName', { - header: '티켓 이름', - }), - columnHelper.accessor('reservationName', { - header: '방문자 이름', + columnHelper.accessor('reservation.reservationHolder.name', { + header: '방문자명', cell: (props) => { const { searchText = '' } = (props.table.options.meta ?? {}) as { searchText: string }; return ( - + ); }, + size: 80, + maxSize: 80, }), - columnHelper.accessor('reservationPhoneNumber', { + columnHelper.accessor('reservation.reservationHolder.phoneNumber', { header: '연락처', cell: (props) => { const { searchText = '' } = (props.table.options.meta ?? {}) as { searchText: string }; return ( - + /> ); }, + size: 140, + maxSize: 140, }), - columnHelper.accessor('entered', { - header: '상태', - cell: (props) => (props.getValue() ? '입장 확인' : '미입장'), + columnHelper.accessor('salesTicketType.ticketType', { + header: '티켓 종류', + cell: (props) => `${props.getValue() === 'INVITE' ? '초청' : '일반'}티켓`, + size: 80, + maxSize: 80, }), - columnHelper.accessor('enteredAt', { - header: '입장 일시', - cell: (props) => (props.getValue() ? format(props.getValue(), 'yyyy/MM/dd HH:mm') : '-'), + columnHelper.accessor('salesTicketType.ticketName', { + header: '티켓명', + minSize: 80, + }), + columnHelper.accessor('usedAt', { + header: '방문 일시', + cell: (props) => { + const value = props.getValue(); + return value ? ( + format(value, 'yyyy.MM.dd HH:mm') + ) : ( + 아직 방문하지 않았습니다. + ); + }, }), ]; interface Props { - data: EntranceResponse[]; - isEnteredTicket: boolean; + data: TicketWithReservationResponse[]; + isEnteredTicket?: boolean; searchText: string; onClickReset?: VoidFunction; } @@ -70,23 +83,36 @@ const EnteranceTable = ({ searchText, data, isEnteredTicket, onClickReset }: Pro columns, data, getCoreRowModel: getCoreRowModel(), + defaultColumn: { + minSize: undefined, + maxSize: undefined, + }, meta: { searchText, }, }); return ( - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - - ))} - - ))} + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ))} + + ))} + {data.length === 0 ? ( {isSearchResult ? ( @@ -97,21 +123,23 @@ const EnteranceTable = ({ searchText, data, isEnteredTicket, onClickReset }: Pro ) : isEnteredTicket ? ( - '입장 관객이 없어요.' + '아직 방문자가 없어요.' ) : ( - '미입장 관객이 없어요.' + '미방문자가 없어요.' )} ) : ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + )} ); diff --git a/apps/admin/src/components/MobileCardList/MobileCardList.style.ts b/apps/admin/src/components/MobileCardList/MobileCardList.style.ts index 5c9f4aff..186233a1 100644 --- a/apps/admin/src/components/MobileCardList/MobileCardList.style.ts +++ b/apps/admin/src/components/MobileCardList/MobileCardList.style.ts @@ -48,18 +48,44 @@ const DateText = styled.div` `; const UserInfoText = styled.div` + width: 100%; + text-align: left; ${({ theme }) => theme.typo.sh1}; color: ${({ theme }) => theme.palette.grey.g90}; `; +const TicketDetailTextWrap = styled.div` + display: flex; + width: 100%; + justify-content: space-between; + align-items: center; +`; + const TicketInfoText = styled.div` ${({ theme }) => theme.typo.b1}; color: ${({ theme }) => theme.palette.grey.g70}; `; +const TicketStatusText = styled.div<{ type: 'DISABLED' | 'LINE_THROUGH' | 'NORMAL' }>` + ${({ theme }) => theme.typo.b1}; + color: ${({ theme, type }) => + type === 'NORMAL' ? theme.palette.grey.g90 : theme.palette.grey.g30}; + text-decoration: ${({ type }) => (type === 'LINE_THROUGH' ? 'line-through' : undefined)}; +`; + const ResetButton = styled.button` font-weight: 600; text-decoration: underline; `; -export default { Container, CardItem, Row, DateText, UserInfoText, TicketInfoText, ResetButton }; +export default { + Container, + CardItem, + Row, + DateText, + UserInfoText, + TicketInfoText, + ResetButton, + TicketDetailTextWrap, + TicketStatusText, +}; diff --git a/apps/admin/src/components/MobileCardList/index.tsx b/apps/admin/src/components/MobileCardList/index.tsx index dd9af206..67194fa1 100644 --- a/apps/admin/src/components/MobileCardList/index.tsx +++ b/apps/admin/src/components/MobileCardList/index.tsx @@ -8,12 +8,14 @@ import Styled from './MobileCardList.style'; type Item = { id: number; - badgeText: string; + badgeText?: string; name: string; phoneNumber: string; ticketName: string; + type: 'DISABLED' | 'LINE_THROUGH' | 'NORMAL'; + status: string; date?: string; - count: number; + count?: number; }; interface Props { @@ -29,19 +31,27 @@ function MobileCardList({ searchText, items, emptyText, onClickReset }: Props) { const Elements = items.map((item) => { return ( - - {item.badgeText} - {item.date && {format(item.date, 'yyyy/MM/dd HH:mm')}} - + {item.badgeText && ( + + {item.badgeText} + {item.date && ( + {format(item.date, 'yyyy/MM/dd HH:mm')} + )} + + )} - - {item.ticketName} · {item.count}매 - + + + {item.ticketName} + {item.count ? ` · ${item.count}매` : ''} + + {item.status} + ); diff --git a/apps/admin/src/components/Pagination/index.tsx b/apps/admin/src/components/Pagination/index.tsx index 8822a283..9b559cdb 100644 --- a/apps/admin/src/components/Pagination/index.tsx +++ b/apps/admin/src/components/Pagination/index.tsx @@ -1,6 +1,8 @@ import { ChevronLeftIcon, ChevronRightIcon } from '@boolti/icon'; import Styled from './Pagination.styles'; +import { useDeviceWidth } from '~/hooks/useDeviceWidth'; +import { useTheme } from '@emotion/react'; interface Props { totalPages: number; @@ -8,18 +10,20 @@ interface Props { onClickPage?: (page: number) => void; } -const SIZE_PER_PAGE = 10; - const Pagination = ({ totalPages, currentPage, onClickPage }: Props) => { - const start = Math.floor(currentPage / SIZE_PER_PAGE) * SIZE_PER_PAGE; - const end = start + SIZE_PER_PAGE; + const deviceWidth = useDeviceWidth(); + const theme = useTheme(); + const isMobile = deviceWidth < parseInt(theme.breakpoint.mobile, 10); + const sizePerPage = isMobile ? 5 : 10; + const start = Math.floor(currentPage / sizePerPage) * sizePerPage; + const end = start + sizePerPage; const pages = Array.from({ length: totalPages }, (_, i) => i).slice(start, end); return ( { - onClickPage?.(Math.max(currentPage - SIZE_PER_PAGE, 0)); + onClickPage?.(Math.max(start - sizePerPage, 0)); }} > @@ -38,7 +42,7 @@ const Pagination = ({ totalPages, currentPage, onClickPage }: Props) => { { - onClickPage?.(Math.min(currentPage + SIZE_PER_PAGE, totalPages - 1)); + onClickPage?.(Math.min(end, totalPages - 1)); }} > diff --git a/apps/admin/src/components/ReservationTable/ReservationTable.styles.ts b/apps/admin/src/components/ReservationTable/ReservationTable.styles.ts index 9e860eea..8c6289bf 100644 --- a/apps/admin/src/components/ReservationTable/ReservationTable.styles.ts +++ b/apps/admin/src/components/ReservationTable/ReservationTable.styles.ts @@ -1,70 +1,38 @@ import { Button } from '@boolti/ui'; import styled from '@emotion/styled'; -const Container = styled.div` - display: flex; - flex-direction: column; - margin: 16px 0; +const Container = styled.table` + table-layout: auto; + margin-bottom: 16px; width: ${({ theme }) => theme.breakpoint.desktop}; - height: 547px; `; -const HeaderRow = styled.div` - display: flex; - flex: 0 0 auto; - flex-wrap: nowrap; +const HeaderRow = styled.tr` + width: 100%; padding-left: 8px; background-color: ${({ theme }) => theme.palette.grey.g00}; border-top: 1px solid ${({ theme }) => theme.palette.grey.g20}; border-bottom: 1px solid ${({ theme }) => theme.palette.grey.g20}; `; -const HeaderItem = styled.span` - display: inline-block; - flex: 0 0 auto; +const HeaderItem = styled.th` padding: 12px; ${({ theme }) => theme.typo.b2}; color: ${({ theme }) => theme.palette.grey.g60}; - &:not(:last-of-type) { - margin-right: 12px; - } - &:nth-of-type(1) { - width: 88px; - } - &:nth-of-type(2) { - width: 80px; - } - &:nth-of-type(3) { - min-width: 100px; - } - &:nth-of-type(4) { - width: 100px; - } - &:nth-of-type(5) { - width: 140px; - } - &:nth-of-type(6) { - width: 96px; - } - &:nth-of-type(7) { + white-space: nowrap; + + &.ticket-price { text-align: right; - width: 92px; - } - &:nth-of-type(8) { - width: 92px; } `; -const Row = styled.div` - display: flex; - flex-wrap: nowrap; +const Row = styled.tr` + width: 100%; padding-left: 8px; border-bottom: 1px solid ${({ theme }) => theme.palette.grey.g20}; `; -const Item = styled.span` - display: block; - flex: 0 0 auto; +const Item = styled.td` padding: 14px 12px; white-space: nowrap; ${({ theme }) => theme.typo.b2}; @@ -72,41 +40,22 @@ const Item = styled.span` & strong { background-color: ${({ theme }) => theme.palette.primary.o0}; } - &:not(:last-of-type) { - margin-right: 12px; - } - &:nth-of-type(1) { - width: 88px; - } - &:nth-of-type(2) { - width: 80px; - } - &:nth-of-type(3) { - min-width: 100px; - } - &:nth-of-type(4) { - width: 100px; - } - &:nth-of-type(5) { - width: 140px; - } - &:nth-of-type(6) { - width: 96px; - } - &:nth-of-type(7) { + &.ticket-price { text-align: right; - width: 92px; - } - &:nth-of-type(8) { - width: 92px; - } - &:nth-of-type(9) { - width: 100px; } `; +const DisabledText = styled.span` + ${({ theme }) => theme.typo.b2}; + color: ${({ theme }) => theme.palette.grey.g30}; +`; + const Empty = styled.div` + position: absolute; + top: 50%; + left: 50%; display: flex; + transform: translateX(-50%) translateY(46px); flex-direction: column; flex: 1; justify-content: center; @@ -122,12 +71,43 @@ const ResetButton = styled(Button)` margin-top: 18px; `; +const TooltipItemColumn = styled.ul` + display: block; +`; + +const TooltipItemRow = styled.li` + display: flex; + justify-content: space-between; + align-items: center; + & > span { + ${({ theme }) => theme.typo.c1}; + color: ${({ theme }) => theme.palette.grey.w}; + } + & > span:first-of-type { + margin-right: 16px; + } + & > span:nth-child(2) { + min-width: 78px; + text-align: left; + } +`; + +const TooltipAnchor = styled.a` + &:hover { + color: ${({ theme }) => theme.palette.grey.g50}; + } +`; + export default { Container, HeaderItem, HeaderRow, Row, Item, + DisabledText, Empty, ResetButton, + TooltipItemColumn, + TooltipItemRow, + TooltipAnchor, }; diff --git a/apps/admin/src/components/ReservationTable/index.tsx b/apps/admin/src/components/ReservationTable/index.tsx index 30a64471..fb6d6475 100644 --- a/apps/admin/src/components/ReservationTable/index.tsx +++ b/apps/admin/src/components/ReservationTable/index.tsx @@ -1,4 +1,4 @@ -import { ReservationResponse, TicketStatus } from '@boolti/api'; +import { ReservationWithTicketsResponse, TicketStatus } from '@boolti/api'; import { createColumnHelper, flexRender, @@ -11,87 +11,152 @@ import { boldText } from '~/utils/boldText'; import { formatPhoneNumber } from '~/utils/format'; import Styled from './ReservationTable.styles'; +import { Tooltip } from 'react-tooltip'; +import { palette } from '@boolti/ui'; -const columnHelper = createColumnHelper(); +const columnHelper = createColumnHelper(); -const columns = [ - columnHelper.accessor('csTicketId', { - header: '티켓 번호', - }), - columnHelper.accessor('ticketType', { - header: '티켓 종류', - cell: (props) => `${props.getValue() === 'INVITE' ? '초청' : '일반'}티켓`, - }), - columnHelper.accessor('ticketName', { - header: '티켓 이름', +const getColumns = (ticketStatus: TicketStatus) => [ + columnHelper.accessor('csReservationId', { + header: '주문 번호', + size: 108, }), - columnHelper.accessor('reservationName', { - header: '방문자 이름', + columnHelper.accessor('paymentInfo', { + header: '결제자명', + id: 'payerName', cell: (props) => { const { searchText = '' } = (props.table.options.meta ?? {}) as { searchText: string }; return ( - + ); }, + size: 80, }), - columnHelper.accessor('reservationPhoneNumber', { + columnHelper.accessor('paymentInfo', { header: '연락처', + id: 'payerPhoneNumber', cell: (props) => { const { searchText = '' } = (props.table.options.meta ?? {}) as { searchText: string }; return ( ); }, + size: 140, }), - columnHelper.accessor('csReservationId', { - header: '주문 번호', + columnHelper.accessor('reservationHolderDetail', { + header: '방문자명', + id: 'reservationHolderDetailName', + cell: (props) => props.getValue()?.name ?? '-', + size: 80, }), - columnHelper.accessor('ticketPrice', { - header: (props) => - (props.table.options.meta as { ticketStatus: TicketStatus }).ticketStatus === 'CANCEL' - ? '환불 금액' - : '결제 금액', - cell: (props) => `${props.getValue().toLocaleString()}원`, + columnHelper.accessor('reservationHolderDetail', { + header: '연락처', + id: 'reservationHolderDetailNamePhoneNumber', + cell: (props) => formatPhoneNumber(props.getValue()?.phoneNumber) ?? '-', + size: 140, }), - columnHelper.accessor('means', { - header: (props) => - (props.table.options.meta as { ticketStatus: TicketStatus }).ticketStatus === 'CANCEL' - ? '환불 방법' - : '결제 방법', + columnHelper.accessor('salesTicketType.ticketType', { + header: '티켓종류', cell: (props) => { switch (props.getValue()) { - case 'ACCOUNT_TRANSFER': - return '계좌이체'; - case 'CARD': - return '카드'; - case 'FREE': - return '-'; - case 'SIMPLE_PAYMENT': - return '간편결제'; + case 'INVITE': + return '초청 티켓'; + case 'SALE': + return '일반 티켓'; } }, + size: 80, + }), + columnHelper.accessor('salesTicketType.ticketName', { + header: '티켓명', + minSize: 80, + }), + columnHelper.accessor('tickets', { + header: '매수', + cell: (props) => `${props.getValue().length}매`, + size: 50, }), + columnHelper.accessor((row) => (row.salesTicketType?.price ?? 0) * row.tickets.length, { + header: ticketStatus === 'CANCEL' ? '취소 금액' : '결제 금액', + cell: (props) => `${props.getValue().toLocaleString()}원`, + size: 92, + id: 'ticket-price', + }), + ...(ticketStatus === 'COMPLETE' + ? [ + columnHelper.accessor( + (row) => + row.tickets.length > 1 + ? `${row.tickets[0].csTicketId} 외 ${row.tickets.length - 1}개` + : row.tickets[0]?.csTicketId, + { + header: '티켓 번호', + cell: (props) => { + const useTooltip = props.row.original.tickets.length > 1; + if (useTooltip) { + return ( + content + ' ' + ticket.csTicketId, '') + .trim() + : '' + } + > + {props.getValue()} + + ); + } + return props.getValue(); + }, + minSize: 120, + }, + ), + ] + : []), columnHelper.accessor( - (props) => - props.ticketStatus === 'CANCEL' && props.canceledAt ? props.canceledAt : props.ticketIssuedAt, + (row) => { + if (ticketStatus === 'CANCEL' && row.cancelInfo) { + return { type: 'date', value: row.cancelInfo.canceledAt }; + } + + return row.gift && !row.gift.done + ? { type: 'text', value: '아직 선물이 등록되지 않았습니다.' } + : { type: 'date', value: row.createdAt }; + }, { - id: 'at', - header: (props) => - (props.table.options.meta as { ticketStatus: TicketStatus }).ticketStatus === 'CANCEL' - ? '환불일시' - : '발권일시', - cell: (props) => (props.getValue() ? format(props.getValue(), 'yyyy/MM/dd HH:mm') : '-'), + header: ticketStatus === 'CANCEL' ? '취소 일시' : '결제 일시', + cell: (props) => { + const { type, value } = props.getValue(); + switch (type) { + case 'date': + return format(value, 'yyy.MM.dd HH:mm'); + case 'text': + return {value}; + } + return '-'; + }, }, ), ]; interface Props { emptyText: string; - data: ReservationResponse[]; + data: ReservationWithTicketsResponse[]; selectedTicketStatus: TicketStatus; searchText: string; onClickReset?: VoidFunction; @@ -106,54 +171,93 @@ const ReservationTable = ({ }: Props) => { const isSearchResult = searchText !== ''; const table = useReactTable({ - columns, + columns: getColumns(selectedTicketStatus), data, getCoreRowModel: getCoreRowModel(), + defaultColumn: { + minSize: undefined, + }, meta: { - ticketStatus: selectedTicketStatus, searchText, }, }); return ( - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - - ))} - - ))} - {data.length === 0 ? ( - - {isSearchResult ? ( - <> - 검색 결과가 없어요.{'\n'}방문자 이름 또는 연락처를 변경해보세요. - - 검색 초기화 - - - ) : ( - emptyText - )} - - ) : ( - <> - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - + <> + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + ))} - + ))} - - )} - + + {data.length === 0 ? ( + + {isSearchResult ? ( + <> + 검색 결과가 없어요.{'\n'}방문자 이름 또는 연락처를 변경해보세요. + + 검색 초기화 + + + ) : ( + emptyText + )} + + ) : ( + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + + )} + + { + const ticketIds = (content ?? '').split(' '); + return ( + + {ticketIds.map((id, index) => ( + + No.{index + 1} + {id} + + ))} + + ); + }} + /> + ); }; diff --git a/apps/admin/src/components/ShowCastInfo/ShowCastInfo.styles.ts b/apps/admin/src/components/ShowCastInfo/ShowCastInfo.styles.ts index da058c2d..357e8a21 100644 --- a/apps/admin/src/components/ShowCastInfo/ShowCastInfo.styles.ts +++ b/apps/admin/src/components/ShowCastInfo/ShowCastInfo.styles.ts @@ -1,8 +1,9 @@ -import { Button } from '@boolti/ui'; +import { mq_lg } from '@boolti/ui'; import styled from '@emotion/styled'; import { m } from 'framer-motion'; const Container = styled.div` + position: relative; border-radius: 8px; background: ${({ theme }) => theme.palette.grey.w}; box-shadow: 0px 8px 14px 0px ${({ theme }) => theme.palette.shadow}; @@ -18,8 +19,6 @@ const Header = styled.div` align-items: center; border-radius: 8px 8px 0px 0px; border: 1px solid ${({ theme }) => theme.palette.grey.g20}; - color: ${({ theme }) => theme.palette.grey.g90}; - ${({ theme }) => theme.typo.sh2}; padding: 24px 28px; &:last-child { @@ -27,10 +26,37 @@ const Header = styled.div` } `; -const EditButton = styled(Button)` - padding: 13px 18px; - & > svg { - margin-right: 8px; +const HeaderNameWrapper = styled.div` + display: flex; + align-items: center; + gap: 12px; +` + +const Handle = styled.button` + display: inline-flex; + align-items: center; + justify-content: center; + color: ${({ theme }) => theme.palette.grey.g40}; + cursor: grab; + user-select: none; + user-zoom: none; +` + +const Name = styled.span` + color: ${({ theme }) => theme.palette.grey.g90}; + ${({ theme }) => theme.typo.sh2}; +`; + +const EditButtonWrapper = styled.div` + span { + display: none; + margin-left: 8px; + } + + ${mq_lg} { + span { + display: inline; + } } `; @@ -118,9 +144,12 @@ const CollapseButton = styled.button` export default { Container, Header, + HeaderNameWrapper, + Handle, + Name, Cast, CollapseButton, - EditButton, + EditButtonWrapper, CastItem, UserImage, Username, diff --git a/apps/admin/src/components/ShowCastInfo/index.tsx b/apps/admin/src/components/ShowCastInfo/index.tsx index 05d7ae62..d58a0a8e 100644 --- a/apps/admin/src/components/ShowCastInfo/index.tsx +++ b/apps/admin/src/components/ShowCastInfo/index.tsx @@ -1,7 +1,9 @@ -import { useDialog } from '@boolti/ui'; +import { TextButton, useDialog } from '@boolti/ui'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; import Styled from './ShowCastInfo.styles'; -import { EditIcon, ChevronDownIcon, ChevronUpIcon, UserIcon } from '@boolti/icon'; +import { EditIcon, ChevronDownIcon, ChevronUpIcon, UserIcon, MenuIcon } from '@boolti/icon'; import { useState } from 'react'; import ShowCastInfoFormDialogContent, { TempShowCastInfoFormInput, @@ -19,47 +21,75 @@ const ShowCastInfo = ({ showCastInfo, onSave, onDelete }: Props) => { const dialog = useDialog(); const [isOpen, setIsOpen] = useState(false); + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging + } = useSortable({ id: showCastInfo.id }); + + const style = { + transform: CSS.Translate.toString(transform), + transition, + backgroundColor: isDragging ? 'rgba(231, 234, 242, 0.5)' : undefined, + backdropFilter: isDragging ? 'blur(3px)' : undefined, + zIndex: isDragging ? 100 : 99, + cursor: isDragging ? 'grabbing' : undefined, + }; + const toggle = () => setIsOpen((prev) => !prev); return ( - + - {showCastInfo.name} - { - e.preventDefault(); - dialog.open({ - title: '출연진 정보 편집', - isAuto: true, - content: ( - { - try { - await onSave(castInfo); - dialog.close(); - } catch { - return new Promise((_, reject) => reject('저장 중 오류가 발생하였습니다.')); - } - }} - prevShowCastInfo={showCastInfo} - onDelete={async () => { - try { - await onDelete?.(); - dialog.close(); - } catch { - return new Promise((_, reject) => reject('삭제 중 오류가 발생하였습니다.')); - } - }} - /> - ), - }); - }} - > - - 편집하기 - + + + + + + {showCastInfo.name} + + + + { + e.preventDefault(); + dialog.open({ + title: '출연진 정보 편집', + isAuto: true, + content: ( + { + try { + await onSave(castInfo); + dialog.close(); + } catch { + return new Promise((_, reject) => reject('저장 중 오류가 발생하였습니다.')); + } + }} + prevShowCastInfo={showCastInfo} + onDelete={async () => { + try { + await onDelete?.(); + dialog.close(); + } catch { + return new Promise((_, reject) => reject('삭제 중 오류가 발생하였습니다.')); + } + }} + /> + ), + }); + }} + > + + 정보 편집 + + {memberLength > 0 && ( <> @@ -84,6 +114,7 @@ const ShowCastInfo = ({ showCastInfo, onSave, onDelete }: Props) => { ))} { e.preventDefault(); toggle(); @@ -94,7 +125,7 @@ const ShowCastInfo = ({ showCastInfo, onSave, onDelete }: Props) => { )} - + ); }; diff --git a/apps/admin/src/components/ShowCastInfoFormDialogContent/DraggableShowCastInfoMemberRow.tsx b/apps/admin/src/components/ShowCastInfoFormDialogContent/DraggableShowCastInfoMemberRow.tsx new file mode 100644 index 00000000..162f1188 --- /dev/null +++ b/apps/admin/src/components/ShowCastInfoFormDialogContent/DraggableShowCastInfoMemberRow.tsx @@ -0,0 +1,30 @@ +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from '@dnd-kit/utilities'; +import ShowCastInfoMemberRow, { ShowCastInfoMemberRowProps } from "./ShowCastInfoMemberRow"; + +interface DraggableShowCastInfoMemberRowProps extends ShowCastInfoMemberRowProps { + id: string +} + +const DraggableShowCastInfoMemberRow = ({ id, ...props }: DraggableShowCastInfoMemberRowProps) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging + } = useSortable({ id }); + + const style = { + transform: CSS.Translate.toString(transform), + transition, + opacity: isDragging ? 0 : 1 + }; + + return ( + + ) +} + +export default DraggableShowCastInfoMemberRow \ No newline at end of file diff --git a/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoFormDialogContent.styles.ts b/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoFormDialogContent.styles.ts index 1f95eee0..77d44bef 100644 --- a/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoFormDialogContent.styles.ts +++ b/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoFormDialogContent.styles.ts @@ -29,6 +29,9 @@ const ShowInfoFormLabel = styled.span` `; const MemberList = styled.div` + display: flex; + flex-direction: column; + gap: 20px; max-height: 364px; overflow-y: scroll; ::-webkit-scrollbar { @@ -59,10 +62,22 @@ const InputWrapper = styled.div` &:focus-within { border-color: ${({ theme, isError }) => - isError ? theme.palette.status.error : theme.palette.grey.g70}; + isError ? theme.palette.status.error : theme.palette.grey.g70}; } `; +const Handle = styled.button` + display: inline-flex; + align-items: center; + justify-content: center; + color: ${({ theme }) => theme.palette.grey.g40}; + margin-top: 12px; + margin-right: 8px; + cursor: grab; + user-select: none; + user-zoom: none; +` + const TextFieldWrap = styled.div` margin-bottom: 28px; @@ -90,7 +105,9 @@ const Row = styled.div` display: flex; justify-content: center; align-items: flex-start; - margin-bottom: 20px; + border-radius: 4px; + position: relative; + z-index: 99; `; const TrashCanButton = styled.button` @@ -171,9 +188,34 @@ const ErrorMessage = styled.span` color: ${({ theme }) => theme.palette.status.error}; `; +const DraggableShowCastInfoMemberRow = styled.div` + border-radius: 4px; + cursor: grabbing; + backdrop-filter: blur(1.5px); + z-index: 100; + + & > div > div > div { + background: none; + } + + &::after { + content: ''; + position: absolute; + top: -10px; + left: -10px; + width: calc(100% + 20px); + height: calc(100% + 20px); + background-color: rgba(231, 234, 242, 0.5); + border-radius: 4px; + z-index: -1; + } +` + + export default { ShowInfoFormLabel, InputWrapper, + Handle, HashTag, Input, Row, @@ -189,4 +231,5 @@ export default { DeleteButton, ErrorMessage, FieldWrap, + DraggableShowCastInfoMemberRow }; diff --git a/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoMemberRow.tsx b/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoMemberRow.tsx new file mode 100644 index 00000000..db016ab9 --- /dev/null +++ b/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoMemberRow.tsx @@ -0,0 +1,122 @@ +import { ClearIcon, MenuIcon, TrashIcon, UserIcon } from '@boolti/icon'; +import { Member } from '@boolti/api'; +import { replaceUserCode } from '~/utils/replace'; +import Styled from './ShowCastInfoFormDialogContent.styles'; +import { TempShowCastInfoFormInput } from "."; +import { Control, Controller } from "react-hook-form"; +import { forwardRef } from 'react'; + +export interface ShowCastInfoMemberRowProps { + control: Control; + field: Partial & { id: number, _id: string }; + index: number; + isFieldBlurred: { userCode: boolean; roleName: boolean }; + draggingStyle?: React.CSSProperties; + onSetUser?: (userCode: string) => void; + onResetUser?: () => void; + onBlurRoleName?: () => void; + onDelete?: () => void +} + +const ShowCastInfoMemberRow = forwardRef(({ control, field, index, isFieldBlurred, draggingStyle, onSetUser, onResetUser, onBlurRoleName, onDelete, ...props }, ref) => { + return ( + + + + + { + const value = field.userCode; + const isError = Boolean( + isFieldBlurred.userCode ? !value || !field.userNickname : false, + ); + return ( + + + {field.userNickname ? ( + <> + {field.userImgPath ? ( + + ) : ( + + )} + {field.userNickname} + { + onChange(undefined); + onResetUser?.(); + }} + > + + + + ) : ( + <> + # + { + const nextValue = replaceUserCode(e.target.value); + onChange(nextValue); + }} + onBlur={async (event) => { + onBlur(); + onSetUser?.(event.target.value); + }} + value={value ?? ''} + /> + + )} + + {isError && 필수 입력사항입니다.} + + ); + }} + name={`members.${index}.userCode`} + /> + { + const value = field.roleName; + const isError = isFieldBlurred.roleName && !value; + return ( + + + { + onBlur(); + onBlurRoleName?.(); + }} + value={value ?? ''} + /> + + {isError && 필수 입력사항입니다.} + + ); + }} + name={`members.${index}.roleName`} + /> + + + + + ); +}); + +export default ShowCastInfoMemberRow diff --git a/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx b/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx index ace1e584..e4deb806 100644 --- a/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx +++ b/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx @@ -1,15 +1,20 @@ import { TextField, useConfirm, useToast } from '@boolti/ui'; import { Controller, useFieldArray, useForm } from 'react-hook-form'; import Styled from './ShowCastInfoFormDialogContent.styles'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { useBodyScrollLock } from '~/hooks/useBodyScrollLock'; -import { ClearIcon, PlusIcon, TrashIcon, UserIcon } from '@boolti/icon'; +import { PlusIcon } from '@boolti/icon'; import { Member, queryKeys, useQueryClient } from '@boolti/api'; -import { replaceUserCode } from '~/utils/replace'; +import ShowCastInfoMemberRow from './ShowCastInfoMemberRow'; +import { DndContext, DragOverlay, DragStartEvent, DragOverEvent, UniqueIdentifier, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors, closestCenter } from '@dnd-kit/core'; +import { SortableContext, arrayMove, verticalListSortingStrategy, sortableKeyboardCoordinates } from '@dnd-kit/sortable'; +import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; +import DraggableShowCastInfoMemberRow from './DraggableShowCastInfoMemberRow'; export interface TempShowCastInfoFormInput { + id: number; name: string; - members?: Array>; + members?: Array & { id: number }>; } interface Props { @@ -18,7 +23,7 @@ interface Props { onSave: (value: TempShowCastInfoFormInput) => Promise; } -const ShowCastInfoFormDialogContent = ({ onDelete, prevShowCastInfo, onSave }: Props) => { +const ShowCastInfoFormDialogContent = ({ prevShowCastInfo, onDelete, onSave }: Props) => { const queryClient = useQueryClient(); const previousShowCastInfoMemberLength = prevShowCastInfo?.members?.length ?? 0; @@ -29,7 +34,7 @@ const ShowCastInfoFormDialogContent = ({ onDelete, prevShowCastInfo, onSave }: P const { control, getValues, watch, getFieldState } = useForm({ defaultValues, }); - const { fields, append, remove, update } = useFieldArray({ + const { fields, append, remove, update, replace } = useFieldArray({ control, name: 'members', keyName: '_id', @@ -66,6 +71,54 @@ const ShowCastInfoFormDialogContent = ({ onDelete, prevShowCastInfo, onSave }: P (!userNickname || !roleName), )); + const [draggingItemId, setDraggingItemId] = useState(null); + const draggingField = controlledFields.find(({ _id }) => _id === draggingItemId); + const draggingFieldIndex = controlledFields.findIndex(({ _id }) => _id === draggingItemId); + + const sensors = useSensors( + useSensor(MouseSensor, { + activationConstraint: { + distance: 10, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 0, + tolerance: 5, + }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const dragStartHandler = (event: DragStartEvent) => { + setDraggingItemId(event.active.id); + }; + + const dragEndHandler = useCallback((event: DragOverEvent) => { + const { active, over } = event; + + if (!(active && over && over.id !== active.id)) return; + + const oldIndex = controlledFields.findIndex(({ _id }) => _id === active.id); + const newIndex = controlledFields.findIndex(({ _id }) => _id === over.id); + + if (oldIndex === -1 || newIndex === -1) return; + + const nextFields = arrayMove(controlledFields, oldIndex, newIndex); + replace(nextFields); + + setIsMemberFieldBlurred((prev) => { + const nextMemberFieldBlurred = [...prev]; + nextMemberFieldBlurred.splice(oldIndex, 1); + nextMemberFieldBlurred.splice(newIndex, 0, prev[oldIndex]); + return nextMemberFieldBlurred; + }) + + setDraggingItemId(null); + }, [controlledFields, replace]); + return ( <> 팀명 @@ -95,152 +148,93 @@ const ShowCastInfoFormDialogContent = ({ onDelete, prevShowCastInfo, onSave }: P /> 팀원 - {controlledFields.map((field, index) => ( - - { - const value = field.userCode; - const isError = Boolean( - isMemberFieldBlurred[index].userCode ? !value || !field.userNickname : false, - ); - return ( - - - {field.userNickname ? ( - <> - {field.userImgPath ? ( - - ) : ( - - )} - {field.userNickname} - { - onChange(undefined); - setIsMemberFieldBlurred((prev) => { - const nextMemberFieldBlurred = [...prev]; - nextMemberFieldBlurred[index].userCode = true; - return nextMemberFieldBlurred; - }); - update(index, { - roleName: field.roleName, - }); - }} - > - - - - ) : ( - <> - # - { - const nextValue = replaceUserCode(e.target.value); - onChange(nextValue); - }} - onBlur={async (event) => { - onBlur(); - const userCode = event.target.value; - if (userCode !== '') { - try { - const { imgPath, nickname } = await queryClient.fetchQuery( - queryKeys.user.userCode(event.target.value), - ); - update(index, { - ...controlledFields[index], - userImgPath: imgPath, - userNickname: nickname, - }); - } catch { - toast.error( - '불티에 회원으로 등록된 식별 코드로만 등록이 가능합니다.' + - '\n' + - '식별 코드를 확인 후 다시 시도해 주세요.', - ); - } finally { - setIsMemberFieldBlurred((prev) => { - const nextMemberFieldBlurred = [...prev]; - nextMemberFieldBlurred[index].userCode = true; - return nextMemberFieldBlurred; - }); - } - } - }} - value={value ?? ''} - /> - - )} - - {isError && 필수 입력사항입니다.} - - ); - }} - name={`members.${index}.userCode`} - /> - { - const value = field.roleName; - const isError = isMemberFieldBlurred[index].roleName && !value; - return ( - - - { - onBlur(); - setIsMemberFieldBlurred((prev) => { - const nextMemberFieldBlurred = [...prev]; - nextMemberFieldBlurred[index].roleName = true; - return nextMemberFieldBlurred; - }); - }} - value={value ?? ''} - /> - - {isError && 필수 입력사항입니다.} - - ); - }} - name={`members.${index}.roleName`} - /> - { - const isConfirm = await confirm('팀원 정보를 삭제하시겠어요?', { - confirm: '삭제하기', - cancel: '취소하기', - }); + + field._id)} strategy={verticalListSortingStrategy}> + {controlledFields.map((field, index) => ( + { + if (userCode !== '') { + try { + const { imgPath, nickname } = await queryClient.fetchQuery( + queryKeys.user.userCode(userCode), + ); + update(index, { + ...controlledFields[index], + userImgPath: imgPath, + userNickname: nickname, + }); + } catch { + toast.error( + '불티에 회원으로 등록된 식별 코드로만 등록이 가능합니다.' + + '\n' + + '식별 코드를 확인 후 다시 시도해 주세요.', + ); + } finally { + setIsMemberFieldBlurred((prev) => { + const nextMemberFieldBlurred = [...prev]; + nextMemberFieldBlurred[index].userCode = true; + return nextMemberFieldBlurred; + }); + } + } + }} + onResetUser={() => { + setIsMemberFieldBlurred((prev) => { + const nextMemberFieldBlurred = [...prev]; + nextMemberFieldBlurred[index].userCode = true; + return nextMemberFieldBlurred; + }); + update(index, { + id: field.id, + roleName: field.roleName, + }); + }} + onBlurRoleName={() => { + setIsMemberFieldBlurred((prev) => { + const nextMemberFieldBlurred = [...prev]; + nextMemberFieldBlurred[index].roleName = true; + return nextMemberFieldBlurred; + }); + }} + onDelete={async () => { + const isConfirm = await confirm('팀원 정보를 삭제하시겠어요?', { + confirm: '삭제하기', + cancel: '취소하기', + }); - if (isConfirm) { - toast.success('팀원 정보를 삭제했습니다.'); - setIsMemberFieldBlurred((prev) => - prev.filter((_, blurredIndex) => blurredIndex !== index), - ); - remove(index); - } - }} - > - - - - ))} + if (isConfirm) { + toast.success('팀원 정보를 삭제했습니다.'); + setIsMemberFieldBlurred((prev) => + prev.filter((_, blurredIndex) => blurredIndex !== index), + ); + remove(index); + } + }} + /> + ))} + + + {(draggingItemId && draggingField && draggingFieldIndex > -1) ? ( + + + + ) : null} + + { - append({}); + append({ id: -Math.floor(Math.random() * 1000000) }); setIsMemberFieldBlurred((prev) => [...prev, { userCode: false, roleName: false }]); }} > @@ -278,13 +272,14 @@ const ShowCastInfoFormDialogContent = ({ onDelete, prevShowCastInfo, onSave }: P onClick={async (e) => { e.preventDefault(); + const id = prevShowCastInfo?.id ?? -Math.floor(Math.random() * 1000000); const name = getValues('name'); const members = (getValues('members') ?? []).filter( (member) => member.userNickname && member.roleName && member.userCode, ); try { - await onSave({ name, members }); + await onSave({ id, name, members }); toast.success( onDelete ? '출연진 정보를 수정했습니다.' : '출연진 정보를 생성했습니다.', ); diff --git a/apps/admin/src/components/ShowDetailLayout/index.tsx b/apps/admin/src/components/ShowDetailLayout/index.tsx index 99c9db48..f3b99cb5 100644 --- a/apps/admin/src/components/ShowDetailLayout/index.tsx +++ b/apps/admin/src/components/ShowDetailLayout/index.tsx @@ -62,8 +62,8 @@ const toTargets = { const label = { INFO: '공연 기본 정보', TICKET: '티켓 관리', - RESERVATION: '방문자 관리', - ENTRANCE: '입장 관리', + RESERVATION: '결제 관리', + ENTRANCE: '방문자 관리', SETTLEMENT: '정산 관리', }; diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx b/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx index 1c03fa55..03f5653d 100644 --- a/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx +++ b/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx @@ -36,7 +36,8 @@ const ShowBasicInfoFormContent = ({ const { getRootProps, getInputProps } = useDropzone({ accept: { - 'image/jpeg, image/png': [], + 'image/jpeg': [], + 'image/png': [], }, maxFiles: MAX_IMAGE_COUNT, onDrop: onDropImage, diff --git a/apps/admin/src/components/TicketNameFilter/TicketNameFilter.styles.ts b/apps/admin/src/components/TicketNameFilter/TicketNameFilter.styles.ts new file mode 100644 index 00000000..1fa311d1 --- /dev/null +++ b/apps/admin/src/components/TicketNameFilter/TicketNameFilter.styles.ts @@ -0,0 +1,112 @@ +import { mq_lg } from '@boolti/ui'; +import styled from '@emotion/styled'; + +const Container = styled.div` + position: relative; +`; + +const TicketFilterButton = styled.button<{ isActive?: boolean }>` + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + color: ${({ theme, isActive }) => (isActive ? theme.palette.primary.o1 : theme.palette.grey.g90)}; + border-radius: 4px; + ${({ theme }) => theme.typo.b3}; + padding: 9px 16px; + padding-left: 0px; + + & > svg { + margin-right: 8px; + } + + ${({ isActive, theme }) => + !isActive + ? ` + &:hover { + color: ${theme.palette.grey.g70}; + background-color: ${theme.palette.mobile.grey.g10}; + } + ` + : ''} + + ${mq_lg} { + padding-left: 9px; + } +`; + +const TicketOptions = styled.div` + padding-top: 16px; + white-space: nowrap; + border-radius: 6px; + background-color: ${({ theme }) => theme.palette.grey.w}; + ${mq_lg} { + left: 0; + position: absolute; + left: unset; + right: 0; + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; + box-shadow: 0px 8px 14px 0px rgba(172, 171, 171, 0.13); + margin-top: 4px; + padding: 16px 20px; + } +`; + +const TicketOptionTitle = styled.div` + ${({ theme }) => theme.typo.sh1}; + color: ${({ theme }) => theme.palette.grey.g90}; +`; + +const OptionList = styled.div` + margin: 8px 0 16px; + display: flex; + flex-direction: column; +`; + +const OptionItem = styled.button` + cursor: pointer; + padding: 8px 0; + display: flex; + justify-content: flex-start; + align-items: center; + ${({ theme }) => theme.typo.b1}; + color: ${({ theme }) => theme.palette.grey.g90}; + + & > svg { + margin-right: 8px; + } +`; + +const ButtonWrap = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: nowrap; + padding-bottom: 16px; + + & > button { + flex: 1 0 auto; + + &:first-of-type { + flex: 0 0 0; + justify-content: flex-start; + margin-right: 36px; + padding-left: 0; + ${({ theme }) => theme.typo.b1}; + } + } + + ${mq_lg} { + padding-bottom: 0; + } +`; + +export default { + Container, + TicketFilterButton, + TicketOptions, + TicketOptionTitle, + OptionList, + OptionItem, + ButtonWrap, +}; diff --git a/apps/admin/src/components/TicketNameFilter/index.tsx b/apps/admin/src/components/TicketNameFilter/index.tsx new file mode 100644 index 00000000..f90f4064 --- /dev/null +++ b/apps/admin/src/components/TicketNameFilter/index.tsx @@ -0,0 +1,114 @@ +import { FilterIcon, SquareCheckIcon } from '@boolti/icon'; +import Styled from './TicketNameFilter.styles'; +import { useRef, useState } from 'react'; +import { Button, TextButton, useDialog } from '@boolti/ui'; +import { useOnClickOutside } from '@boolti/ui/src/hooks/useOnClickOutside'; +import { useDeviceWidth } from '~/hooks/useDeviceWidth'; +import { useTheme } from '@emotion/react'; +import { useBodyScrollLock } from '~/hooks/useBodyScrollLock'; + +interface Option { + label: string; + value: string; +} + +interface Props { + updateSelectValues: (selectedValues: string[]) => void; + selectedValues: string[]; + options: Option[]; +} + +const TicketFilterOptions = ({ + selectedValues, + options, + updateSelectValues, + close, +}: Props & { close: VoidFunction }) => { + const [tempSelectedValues, setTempSelectedValues] = useState( + selectedValues.length === 0 ? options.map((option) => option.value) : selectedValues, + ); + const ref = useRef(null); + + useOnClickOutside(ref, close, { skipWhenParentElementSame: true }); + useBodyScrollLock(true); + + return ( + + 필터 + + {options.map((option) => ( + { + setTempSelectedValues((prev) => + prev.includes(option.value) + ? prev.filter((value) => value !== option.value) + : [...prev, option.value], + ); + }} + > + + {option.label} + + ))} + + + { + setTempSelectedValues(options.map((option) => option.value)); + }} + > + 전체 선택 + + + + + ); +}; + +const TicketNameFilter = (props: Props) => { + const deviceWidth = useDeviceWidth(); + const theme = useTheme(); + const isMobile = deviceWidth < parseInt(theme.breakpoint.mobile, 10); + const [isOpen, setIsOpen] = useState(false); + const { open, close } = useDialog(); + + const toggle = () => setIsOpen((prev) => !prev); + + return ( + + 0} + onClick={() => { + if (!isMobile) { + toggle(); + } else { + open({ + content: , + }); + } + }} + > + + 필터 + + {isOpen && !isMobile && setIsOpen(false)} />} + + ); +}; + +export default TicketNameFilter; diff --git a/apps/admin/src/components/TicketTypeSelect/TicketTypeSelect.styles.ts b/apps/admin/src/components/TicketTypeSelect/TicketTypeSelect.styles.ts new file mode 100644 index 00000000..2f396410 --- /dev/null +++ b/apps/admin/src/components/TicketTypeSelect/TicketTypeSelect.styles.ts @@ -0,0 +1,23 @@ +import styled from '@emotion/styled'; + +const MobileMenu = styled.div` + padding-bottom: 8px; +`; + +const Item = styled.button<{ isSelected: boolean }>` + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + height: 48px; + padding: 12px 0; + ${({ theme, isSelected }) => (isSelected ? theme.typo.sh1 : theme.typo.b3)}; + color: ${({ theme, isSelected }) => + isSelected ? theme.palette.grey.g90 : theme.palette.grey.g70}; + cursor: pointer; +`; + +export default { + MobileMenu, + Item, +}; diff --git a/apps/admin/src/components/TicketTypeSelect/index.tsx b/apps/admin/src/components/TicketTypeSelect/index.tsx index f313dd0a..ba60ec2b 100644 --- a/apps/admin/src/components/TicketTypeSelect/index.tsx +++ b/apps/admin/src/components/TicketTypeSelect/index.tsx @@ -1,6 +1,8 @@ -import { ChevronRightIcon } from '@boolti/icon'; -import { breakpoint } from '@boolti/ui'; +import { CheckIcon, ChevronRightIcon } from '@boolti/icon'; +import { breakpoint, useDialog } from '@boolti/ui'; import { useTheme } from '@emotion/react'; +import Styled from './TicketTypeSelect.styles'; + import Select from 'react-select'; import { useDeviceWidth } from '~/hooks/useDeviceWidth'; @@ -21,17 +23,43 @@ export const options = [ const TicketTypeSelect = ({ onChange, value }: Props) => { const theme = useTheme(); const width = useDeviceWidth(); + const isMobile = width < parseInt(theme.breakpoint.mobile, 10); + const { open, close } = useDialog(); return (