Compare commits

..

175 Commits

Author SHA1 Message Date
RustamRu
52fec13377 0.7.2
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-09 12:40:31 +03:00
RustamRu
1d4b796a39 Merge branch 'main' of ssh://85.143.175.152:222/dry_wash_inc/dry-wash-pl
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-09 11:31:52 +03:00
RustamRu
142067e9ce 0.7.1 2025-02-09 11:31:36 +03:00
3c291baa0e Merge pull request 'fix: create order request body (#81)' (#84) from feature/order-form-upgrade into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #84
2025-02-09 11:30:58 +03:00
RustamRu
3cdc47b91b fix: create order request body (#81)
Some checks are pending
it-academy/dry-wash-pl/pipeline/pr-main Build started...
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-09 11:28:15 +03:00
RustamRu
901d8d78a1 0.7.0
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-09 11:17:24 +03:00
RustamRu
7736592830 fix: e2e tests
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-09 11:02:27 +03:00
RustamRu
8d447c9461 0.6.0
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-09 10:20:21 +03:00
6506b89dc7 Merge pull request 'feat: add playwright' (#83) from feature/playwright into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #83
2025-02-09 10:12:42 +03:00
89d432b360 Merge pull request 'fear: change requests to RTK query' (#78) from feature/rtk-query-arm into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #78
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2025-02-09 10:10:40 +03:00
f6cc2efb86 Merge pull request 'feature/order-form-upgrade' (#82) from feature/order-form-upgrade into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #82
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2025-02-09 10:09:45 +03:00
859fa4f2e1 feat: add playwright
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2025-02-09 09:57:04 +03:00
RustamRu
0ec9e146b9 feat: order creation animation (#77)
Some checks are pending
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main Build started...
2025-02-09 09:03:24 +03:00
RustamRu
1669f01879 feat: use RTK Query to create order deails (#81)
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-09 08:04:21 +03:00
658e23d4e3 fix: place orders in the model
Some checks are pending
it-academy/dry-wash-pl/pipeline/pr-main Build started...
2025-02-08 22:36:30 +03:00
ed8ae95436 feat: add hook useShowToast.ts
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2025-02-08 22:18:32 +03:00
a00aaff29d fear: change requests to RTK query
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2025-02-08 21:21:57 +03:00
ad1f948641 Merge pull request 'feat: add tests' (#79) from feature/test into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #79
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2025-02-04 19:41:58 +03:00
bd66f00b81 Merge pull request 'feat: rewrite the form to react-hook-form and add validation' (#80) from feature/react-hook-form into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #80
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2025-02-04 19:38:09 +03:00
9a20c9f098 feat: rewrite the form to react-hook-form and add validation
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2025-02-03 00:06:16 +03:00
d48f8011c1 fix: update package-lock.json
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2025-02-02 14:33:31 +03:00
f2319542e7 feat: add tests
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
2025-02-02 14:28:51 +03:00
3fb01392f0 fix: change place 2025-02-02 14:28:51 +03:00
1dd14caed2 feat: add tests 2025-02-02 14:28:43 +03:00
12982cdf9e Merge pull request 'feature/order-view-upgrade' (#75) from feature/order-view-upgrade into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #75
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2025-02-02 13:34:10 +03:00
RustamRu
31b440656c feat: order view status polling feature (#74)
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2025-01-26 19:28:23 +03:00
RustamRu
45c4ca16c8 feat: use RTK Query to get order deails (#73) 2025-01-26 19:20:36 +03:00
e3d316c418 Merge pull request 'fix: fill location from autocomplete (#71)' (#72) from feature/order-form-upgrade into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #72
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2025-01-26 15:59:57 +03:00
RustamRu
4c26928d23 fix: fill location from autocomplete (#71)
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2025-01-26 15:47:53 +03:00
1471bf94dd Merge pull request 'fix: option select (#55)' (#70) from feature/graphic-car-body-select into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #70
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2025-01-26 15:26:03 +03:00
RustamRu
d3ffaa97f8 fix: option select (#55)
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2025-01-26 15:06:11 +03:00
acd09d427c fix: change endpoint order
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-01-26 12:48:54 +03:00
34bf557a6f fix: change endpoint orders
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-01-26 12:43:40 +03:00
5e0ac9f7a5 fix: change endpoint
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-01-26 12:36:42 +03:00
9a2ef18925 Merge pull request 'feat: add RTK for master' (#69) from feature/RTK into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #69
2025-01-26 12:25:44 +03:00
d15bd6f7d2 feat: add RTK for master
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2025-01-26 12:21:34 +03:00
adce5392a1 Merge pull request 'feat: add update status and masters (#67)' (#68) from feature/updateOrders into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #68
2025-01-26 12:15:45 +03:00
1b01c0ba4f fix: add :id
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2025-01-26 11:08:43 +03:00
54ced18ea2 fix: eslint checks (#67)
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2025-01-25 17:15:41 +03:00
0ddcaa5d52 feat: add update status and masters (#67)
Some checks failed
it-academy/dry-wash-pl/pipeline/head There was a failure building this commit
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
2025-01-25 17:12:02 +03:00
85d96b930c Merge pull request 'feat/create-order-stubs' (#66) from feat/create-order-stubs into main
Reviewed-on: #66
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2025-01-21 17:32:41 +03:00
RustamRu
adb812280d feat: create order stubs (#65) 2025-01-21 17:29:56 +03:00
RustamRu
3382ae3ada 0.5.0
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-01-19 11:06:34 +03:00
RustamRu
5ed023866e fix: washing dates format
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-01-19 11:04:44 +03:00
RustamRu
e73773f359 Merge branch 'main' of ssh://85.143.175.152:222/dry_wash_inc/dry-wash-pl
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-01-19 10:55:56 +03:00
RustamRu
a9a9b3cadd feat: create order api 2025-01-19 10:55:44 +03:00
06abc15c9a feat: add const for time
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-01-19 10:50:28 +03:00
6ca7de9467 feat: add start time and end time
Some checks failed
it-academy/dry-wash-pl/pipeline/head There was a failure building this commit
2025-01-19 10:45:40 +03:00
939f107d1c Merge pull request 'feat: add a name and phone change from the Masters (#62)' (#63) from feature/update-master into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #63
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2025-01-19 10:14:40 +03:00
4f92125e6d feat: add a name and phone change from the Masters (#62)
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2025-01-18 16:06:27 +03:00
3ea501161c fix: add schedule fix
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-01-12 11:16:58 +03:00
e8634c396f 0.4.0
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-01-12 10:15:20 +03:00
edddf6d857 Merge pull request 'feat: add image to car body style select option (#55)' (#61) from feature/graphic-car-body-select into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #61
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2025-01-12 10:13:19 +03:00
1276b13fec Merge pull request 'feat: add fetch for multi-stub (#59)' (#60) from feature/arm-masters-back into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #60
2025-01-12 10:13:04 +03:00
2aa361e3db Merge pull request 'feat: add region code to car number (#54)' (#58) from feature/car-number-region-code into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #58
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2025-01-12 10:12:42 +03:00
RustamRu
4cda998bd7 feat: add image to car body style select option (#55)
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-01-12 09:05:36 +03:00
9b0bda3cda fix: add schedule for masters
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2025-01-12 00:34:56 +03:00
cad31605d9 fix: eslint fix 2025-01-12 00:34:37 +03:00
1dee68b65d fix: place the spinner in the center
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
2025-01-11 22:43:55 +03:00
6096cfc15c feat: add fetch for multi-stub (#59)
Some checks failed
it-academy/dry-wash-pl/pipeline/head There was a failure building this commit
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
2025-01-11 22:40:01 +03:00
fc699e7890 Merge pull request 'feat: Add a color scheme for the status of orders (#56)' (#57) from feat/colors-status into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #57
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2025-01-11 21:56:00 +03:00
RustamRu
3e0e570ff6 feat: add region code to car number (#54)
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2025-01-11 18:27:12 +03:00
7b24804498 feat: Add a color scheme for the status of orders (#56)
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2024-12-29 22:02:55 +03:00
5498122109 Merge pull request 'feat: add order-view (#9)' (#51) from feature/order-view into main
Some checks are pending
it-academy/dry-wash-pl/pipeline/head Build queued...
Reviewed-on: #51
2024-12-22 19:25:59 +03:00
ef90173b80 0.3.0
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-12-22 12:44:44 +03:00
c7162d0045 Merge pull request 'feat: add keycloak-js for arm' (#52) from feature/arm-keycloack into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #52
2024-12-22 12:41:54 +03:00
885ad16782 feat: add keycloak-js for arm
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2024-12-22 12:36:58 +03:00
RustamRu
5e24537919 fix: import order-details (#9)
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2024-12-22 11:30:30 +03:00
707a77d7ad Merge pull request 'feat: use react-yandex-maps for location input (#8)' (#50) from feature/order-form into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #50
2024-12-22 11:21:01 +03:00
RustamRu
8a0dff682b feat: add order-view (#9)
Some checks failed
it-academy/dry-wash-pl/pipeline/head There was a failure building this commit
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
2024-12-22 11:15:50 +03:00
RustamRu
b8606ac05f feat: use react-yandex-maps for location input (#8)
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2024-12-22 08:52:07 +03:00
6218b6f5d8 Merge pull request 'feature/change-days' (#49) from feature/change-days into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #49
2024-12-15 17:47:34 +03:00
8cc8391a09 feat: add a component for changing days (#48)
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2024-12-14 22:27:07 +03:00
e2c65fd39c fix: change the paths for localization (#47)
Some checks failed
it-academy/dry-wash-pl/pipeline/head There was a failure building this commit
2024-12-14 18:27:21 +03:00
9f530204fa 0.2.0
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-12-08 11:57:46 +03:00
fb3ca156fb Merge pull request 'feature/order-form' (#46) from feature/order-form into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #46
2024-12-08 11:31:17 +03:00
48d076a829 Merge pull request 'feature/successJson' (#45) from feature/successJson into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #45
2024-12-08 11:18:35 +03:00
e4b9aefe57 fix: add spaces
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2024-12-08 11:00:43 +03:00
RustamRu
77ea7d301e feat: order form page (#8)
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2024-12-08 09:57:46 +03:00
RustamRu
93adc1fb7d add order form components (#8) 2024-12-08 09:57:18 +03:00
RustamRu
0457bf7ffc install react-hook-form, react-phone-number-input (#8) 2024-12-08 09:57:01 +03:00
RustamRu
11fe40b86d override Input, Select components styles (#8) 2024-12-08 09:56:34 +03:00
RustamRu
ab40b587ba add order form page locales (#8) 2024-12-08 09:55:59 +03:00
RustamRu
b79c957779 add order form page models, reorder landing models (#8) 2024-12-08 09:55:13 +03:00
RustamRu
440e87ef76 fix landing route names (#8) 2024-12-08 09:54:17 +03:00
0b2dae79ac feat: add stub fetch admin (#44)
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
it-academy/dry-wash-pl/pipeline/head There was a failure building this commit
2024-12-07 23:41:26 +03:00
RustamRu
7c157574c8 feat: extract admin (#43)
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-12-01 12:17:04 +03:00
5ef0d5953d feat: add stub admin (#43)
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-12-01 11:53:20 +03:00
RustamRu
6705e74ece 0.1.1
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-26 18:31:12 +03:00
52bb3790c7 Merge pull request 'feature/landing-stubs 0.1.0 (#33)' (#34) from feature/landing-stubs into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #34
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2024-11-26 18:27:39 +03:00
RustamRu
2cf60c7b16 fix: import landingSuccessStub (#33)
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-25 23:53:09 +03:00
RustamRu
d30202108c Merge branch 'feature/landing-stubs' of ssh://85.143.175.152:222/dry_wash_inc/dry-wash-pl into feature/landing-stubs
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2024-11-25 23:48:29 +03:00
RustamRu
012de3c670 feat: generate landing stub types outside (#33) 2024-11-25 23:48:25 +03:00
RustamRu
409c339b3c feat: create success stubs json with type generation (#33) 2024-11-25 23:48:25 +03:00
RustamRu
8611405edd fix: eslint imports order 2024-11-25 23:48:25 +03:00
RustamRu
407da721af feat: apply success stubs to landing content (#33) 2024-11-25 23:48:24 +03:00
RustamRu
409473413a feat: create success stubs json with type generation (#33) 2024-11-25 23:48:06 +03:00
RustamRu
823e192c51 feat: move i18n type utils to lib and describe (#33) 2024-11-25 23:48:06 +03:00
RustamRu
d4965b7aba feat: generate landing stub types outside (#33)
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
2024-11-25 23:39:39 +03:00
RustamRu
16b4627166 feat: create success stubs json with type generation (#33)
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
2024-11-25 21:30:14 +03:00
7025c1a31b Merge pull request 'feat: add sideBar active(#36)' (#41) from feature/sideBar-active into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #41
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2024-11-25 10:11:52 +03:00
2d55f26de5 Merge remote-tracking branch 'origin/feature/sideBar-active' into feature/sideBar-active
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
# Conflicts:
#	src/components/Sidebar/Sidebar.tsx
2024-11-24 22:40:44 +03:00
da244adee0 fix: Update URL references and conditional checks (#36) 2024-11-24 22:37:19 +03:00
5d1a679c68 feat: add sideBar active(#36) 2024-11-24 22:31:21 +03:00
a9738c7d8a Merge pull request 'feature/stubsArm' (#40) from feature/stubsArm into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #40
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2024-11-24 19:57:51 +03:00
6442f1f407 Merge pull request 'feat: add dynamic routing (#25)' (#42) from feature/router-arm-dynamic into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #42
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2024-11-24 19:54:27 +03:00
2130532dfd feat: separation of imports MasterItem.tsx (#40)
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-24 14:59:38 +03:00
3b4f5a8486 feat: separation of imports (#40)
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
2024-11-24 14:57:36 +03:00
b5f3838536 feat: separation of imports (#25)
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-24 14:54:01 +03:00
dee3a04310 feat: add dynamic routing (#25)
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
it-academy/dry-wash-pl/pipeline/head There was a failure building this commit
2024-11-24 14:50:51 +03:00
acd39b1e32 feat: add format date(#40)
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
2024-11-24 12:08:45 +03:00
c2fad8a3ff Merge pull request 'feat: add multilingualism (#35)' (#39) from feature/i18next-multilingualism into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #39
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2024-11-24 11:22:09 +03:00
9eb0080d13 feat: add sideBar active(#36)
Some checks failed
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
2024-11-23 18:34:53 +03:00
920f4440e9 fix: divide the imports(#37)
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2024-11-23 15:59:21 +03:00
e374cbfdd3 feat: add stub success(#37)
Some checks failed
it-academy/dry-wash-pl/pipeline/head There was a failure building this commit
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
2024-11-23 15:56:40 +03:00
0b30ce6571 feat: divide imports by types (#35)
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2024-11-23 15:37:06 +03:00
3b47e1dcdf Merge remote-tracking branch 'origin/feature/i18next-multilingualism' into feature/i18next-multilingualism
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
2024-11-23 15:28:44 +03:00
4e8bc2fca5 feat: add multilingualism (#35) 2024-11-23 15:27:53 +03:00
0c9663fcef feat: add multilingualism (#35)
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-23 15:19:44 +03:00
RustamRu
181198c0cb fix: eslint imports order 2024-11-17 18:39:31 +03:00
RustamRu
5ca3fd2613 feat: apply success stubs to landing content (#33) 2024-11-17 18:38:32 +03:00
RustamRu
0cd7ec8b22 feat: create success stubs json with type generation (#33) 2024-11-17 18:36:57 +03:00
RustamRu
66c1323f00 feat: move i18n type utils to lib and describe (#33) 2024-11-17 18:36:26 +03:00
a63304b5e4 Merge pull request 'feature/eslint-import-order' (#32) from feature/eslint-import-order into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #32
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2024-11-17 13:56:21 +03:00
RustamRu
effd9dae2d Merge branch 'feature/eslint-import-order' of ssh://85.143.175.152:222/dry_wash_inc/dry-wash-pl into feature/eslint-import-order
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2024-11-17 13:46:28 +03:00
RustamRu
1787e94f8e feat: update eslint import order rule (#31) 2024-11-17 13:46:24 +03:00
RustamRu
855ec2f138 feat: update eslint import order rule
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-17 13:43:41 +03:00
RustamRu
d15d761f1a 0.1.0
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-feat/landing This commit looks good
2024-11-17 11:44:54 +03:00
05303092e2 Merge pull request 'feat/landing' (#30) from feat/landing into main
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-feat/landing This commit looks good
Reviewed-on: #30
2024-11-17 11:39:24 +03:00
RustamRu
98181ff354 update brojs/cli
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2024-11-17 11:28:04 +03:00
RustamRu
5822f5f7b0 Merge remote-tracking branch 'origin/main' into feat/landing
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2024-11-17 11:22:38 +03:00
RustamRu
e72736e692 Merge branch 'feat/landing' of ssh://85.143.175.152:222/dry_wash_inc/dry-wash-pl into feat/landing
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-17 11:18:16 +03:00
RustamRu
3cdb9a997e feat: add translations to landing (#18) 2024-11-17 11:18:12 +03:00
RustamRu
425a47b33c feat: add localized review mock files (#18) 2024-11-17 11:18:12 +03:00
RustamRu
862f2ef5f5 feat: add landing translation files (#18) 2024-11-17 11:18:12 +03:00
RustamRu
7e18d765bc feat: init i18n with type hints (#18) 2024-11-17 11:18:12 +03:00
edc329c154 Merge pull request 'feature/page404' (#27) from feature/page404 into main
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-feat/landing There was a failure building this commit
Reviewed-on: #27
2024-11-17 11:17:51 +03:00
5d208b1fc5 feat: delete word test(#22)
Some checks are pending
it-academy/dry-wash-pl/pipeline/pr-main Build started...
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-17 11:16:23 +03:00
1839136c9f feat: add not found page (#22) 2024-11-17 11:16:16 +03:00
a6ec94785a feat: test (#22) 2024-11-17 11:14:09 +03:00
18ca96bfe9 Merge pull request 'feat: add ErrorBoundary page (#23)' (#28) from feature/errorBoundary into main
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-feat/landing There was a failure building this commit
Reviewed-on: #28
2024-11-17 11:07:38 +03:00
db918ba7c7 Merge pull request 'feat: add router to the arm page (#14)' (#20) from feature/router-arm into main
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-feat/landing There was a failure building this commit
Reviewed-on: #20
2024-11-17 11:02:45 +03:00
RustamRu
c92c879b31 feat: add translations to landing (#18)
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-16 22:43:51 +03:00
RustamRu
752fe72786 feat: add localized review mock files (#18) 2024-11-16 22:43:28 +03:00
RustamRu
36200382a8 feat: add landing translation files (#18) 2024-11-16 22:43:08 +03:00
RustamRu
8328ee8028 feat: init i18n with type hints (#18) 2024-11-16 22:41:32 +03:00
d1d92c63e8 feat: add ErrorBoundary page (#23)
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2024-11-16 20:09:02 +03:00
RustamRu
95d26e58a3 Merge branch 'feat/landing' of ssh://85.143.175.152:222/dry_wash_inc/dry-wash-pl into feat/landing
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-16 19:38:06 +03:00
RustamRu
bc629eabdf feat: add navigation keys and routing check (#24) 2024-11-16 19:37:50 +03:00
dedc7e1608 feat: convert link to button (#14)
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-16 18:24:51 +03:00
1c8348dee5 Merge remote-tracking branch 'origin/feature/router-arm' into feature/router-arm
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
# Conflicts:
#	src/components/Sidebar/Sidebar.tsx
#	src/routes.tsx
2024-11-16 17:26:22 +03:00
04e4d011ff feat: add router to the arm page (#14) 2024-11-16 17:05:21 +03:00
RustamRu
cba5beda72 feat: add navigation keys and routing check (#24)
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-16 15:41:14 +03:00
RustamRu
3ade6f83cc Merge branch 'feat/landing' of ssh://85.143.175.152:222/dry_wash_inc/dry-wash-pl into feat/landing
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-16 14:24:05 +03:00
RustamRu
897602c471 fix: align footer to bottom (#7) 2024-11-16 14:24:02 +03:00
RustamRu
d2b74a1467 fix: align footer to bottom (#7)
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-10 12:34:01 +03:00
e4969938c3 Merge pull request 'feat: add i18next for arm (#17)' (#19) from feature/i18next-arm into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-feat/landing This commit looks good
Reviewed-on: #19
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2024-11-10 12:17:54 +03:00
5fa37ad2db fix: update bro cli and delete @brojs/i18nextreactconfig (#17)
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-10 12:04:15 +03:00
d7da2b5618 feat: add i18next for arm (#17)
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2024-11-10 11:54:40 +03:00
413181617b Merge pull request 'feat: landing (#7)' (#21) from feat/landing into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #21
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2024-11-10 11:50:19 +03:00
RustamRu
10854c836b feat: move demo video to remote assets (#7)
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-10 11:35:39 +03:00
RustamRu
98e11704e6 rename Jenkins config file
All checks were successful
it-ac-2024/dry_wash_inc/pipeline/pr-main This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2024-11-10 11:14:45 +03:00
RustamRu
1d8e9c7e90 Merge branch 'feat/landing' of ssh://85.143.175.152:222/dry_wash_inc/dry-wash-pl into feat/landing 2024-11-10 11:08:33 +03:00
RustamRu
1930333d64 add Landing page (#7) 2024-11-10 11:07:50 +03:00
RustamRu
0b47fb4a2a add Jenkins config 2024-11-10 11:04:14 +03:00
RustamRu
83f0dc77e4 add Landing routing 2024-11-10 02:48:06 +03:00
RustamRu
d713f074d9 combine Landing page 2024-11-10 02:47:48 +03:00
RustamRu
915e402647 add landing components 2024-11-10 02:47:19 +03:00
RustamRu
1c8054f7e8 add mocks 2024-11-10 02:46:56 +03:00
RustamRu
374d2d2ebe add assets 2024-11-10 02:46:08 +03:00
RustamRu
4cb3fba14a add theme config 2024-11-10 02:45:54 +03:00
116e883e91 feat: add router to the arm page (#14) 2024-11-09 19:34:12 +03:00
9b3a204657 Merge pull request 'feat: add i18next (#11)' (#16) from feature/i18next into main
Reviewed-on: #16
Reviewed-by: Rustam <kagapov.rustam@yandex.ru>
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2024-11-06 13:44:40 +03:00
9a490bd993 feat: add i18next (#11) 2024-11-03 12:53:20 +03:00
7ff8a99505 Merge pull request 'feat: add eslint (#12)' (#15) from feature/eslint into main
Reviewed-on: #15
2024-11-03 12:28:50 +03:00
190 changed files with 11704 additions and 2608 deletions

8
.gitignore vendored
View File

@ -130,4 +130,10 @@ dist
.yarn/install-state.gz
.pnp.*
.idea
.idea
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

57
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,57 @@
pipeline {
agent {
docker {
image 'node:20'
}
}
stages {
stage('install (ci)') {
steps {
sh 'node -v'
sh 'npm -v'
script {
String tag = sh(returnStdout: true, script: 'git tag --contains').trim()
String branchName = sh(returnStdout: true, script: 'git rev-parse --abbrev-ref HEAD').trim()
String commit = sh(returnStdout: true, script: 'git log -1 --oneline').trim()
String commitMsg = commit.substring(commit.indexOf(' ')).trim()
if (tag) {
currentBuild.displayName = "#${BUILD_NUMBER}, tag ${tag}"
} else {
currentBuild.displayName = "#${BUILD_NUMBER}, branch ${branchName}"
}
String author = sh(returnStdout: true, script: "git log -1 --pretty=format:'%an'").trim()
currentBuild.description = "${author}<br />${commitMsg}"
echo 'starting installing'
sh 'npm ci'
}
}
}
stage('checks') {
parallel {
stage('eslint') {
steps {
sh 'npm run eslint'
}
}
stage('build') {
steps {
sh 'npm run build'
}
}
}
}
stage('clean-all') {
steps {
sh 'rm -rf .[!.]*'
sh 'rm -rf ./*'
sh 'ls -a'
}
}
}
}

View File

@ -45,10 +45,12 @@
### MVP1
**1. Landing**
- преимущества сервиса
- оставить заявку (редирект на Страницу оформления заказа)
**2. Страница для оформления заказа**
- форма
- номер машины (mask input)
- цвет машины
@ -58,10 +60,12 @@
- после заполнения редирект на страницу с деталями заказа
**3. Страница с деталями заказа**
- описание заказа
- детали заказа (id, статус)
**3. АРМ оператора**
- список заказов (RUD)
- id заказа
- статус заказа (готово / не готово)
@ -72,7 +76,6 @@
- кнопка "Добавить"
- кнопка "Удалить"
### Built With
[![React][React.js]][React-url]
@ -103,6 +106,14 @@
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## Instructions
### Stubs types generation
1. generate types with json-literal-typer (should be installed globally)
```sh
npx json-literal-typer -i <path to json> -o <path to output ts-file>
```
2. export default type from output file
<!-- PARTICIPANTS -->
## Participants

1
__mocks__/file-mock.ts Normal file
View File

@ -0,0 +1 @@
module.exports = 'file';

View File

@ -0,0 +1,9 @@
import localeRu from '../locales/ru.json';
module.exports = {
useTranslation: (_, { keyPrefix }) => {
return {
t: (key: string) => localeRu[`${keyPrefix}.${key}`],
};
}
};

7
babel.config.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-typescript',
['@babel/preset-react', { runtime: 'automatic' }],
],
};

View File

@ -1,9 +1,9 @@
/* eslint-disable no-undef */
/* eslint-disable @typescript-eslint/no-require-imports */
const pkg = require("./package");
const pkg = require('./package');
module.exports = {
apiPath: "stubs/api",
apiPath: 'stubs/api',
webpackConfig: {
output: {
publicPath: `/static/${pkg.name}/${process.env.VERSION || pkg.version}/`,
@ -11,17 +11,20 @@ module.exports = {
},
/* use https://admin.bro-js.ru/ to create config, navigations and features */
navigations: {
"dry-wash.main": "/dry-wash",
"dry-wash.create": "/order",
"dry-wash.view.order": "/order/:orderId",
"dry-wash.arm": "/arm",
'dry-wash.main': '/dry-wash',
'dry-wash.order.create': '/order',
'dry-wash.order.view': '/order/:orderId',
'dry-wash.arm.master': 'master',
'dry-wash.arm.order': 'order',
'dry-wash.arm': '/arm/*',
},
features: {
"dry-wash-pl": {
'dry-wash-pl': {
// add your features here in the format [featureName]: { value: string }
'order-view-status-polling': { value: '3000' }
},
},
config: {
"dry-wash-pl.api": "/api",
'dry-wash.api': '/api',
},
};

26
e2e/example.spec.ts Normal file
View File

@ -0,0 +1,26 @@
import { test, expect } from '@playwright/test';
test.beforeEach('check server is up', async ({ page }) => {
try {
await page.goto('http://localhost:8099/dry-wash');
const makeOrderText = page.getByText('Сделать заказ', { exact: true });
await expect(makeOrderText).toBeVisible();
} catch (error) {
console.error('server not up');
test.skip();
}
});
test('login', async ({ page }) => {
await page.goto('http://localhost:8099/dry-wash/arm');
await page.getByRole('textbox', { name: 'Username or email' }).click();
await page
.getByRole('textbox', { name: 'Username or email' })
.fill('237x237');
await page.getByRole('textbox', { name: 'Password' }).click();
await page.getByRole('textbox', { name: 'Password' }).fill('');
await page.getByRole('button', { name: 'Sign In' }).click();
await page.getByRole('heading', { name: 'Заказы' }).click();
await page.getByRole('link', { name: 'Мастера' }).click();
await page.getByRole('link', { name: 'Заказы' }).click();
});

View File

@ -1,39 +1,52 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
import globals from 'globals';
import pluginJs from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginReact from 'eslint-plugin-react';
import stylistic from '@stylistic/eslint-plugin';
import pluginImport from 'eslint-plugin-import';
export default [
{ files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"] },
{ files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'] },
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
{
plugins: {
'@stylistic': stylistic
'@stylistic': stylistic,
import: pluginImport,
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"warn", // or "error"
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'warn', // or "error"
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
"sort-imports": ["error", {
"ignoreCase": false,
"ignoreDeclarationSort": true,
"ignoreMemberSort": true,
"memberSyntaxSortOrder": ["none", "all", "multiple", "single"],
"allowSeparatedGroups": true
}],
semi: ["error", "always"],
'sort-imports': ['off'],
'import/order': [
'error',
{
groups: [
'builtin',
'external',
'internal',
'parent',
['sibling', 'index'],
],
'newlines-between': 'always',
},
],
semi: ['error', 'always'],
'@stylistic/indent': ['error', 2],
'react/prop-types': 'off'
'react/prop-types': 'off',
},
}
},
{
ignores: ['babel.config.js'],
},
];

View File

@ -0,0 +1,19 @@
module.exports = {
transform: {
'^.+\\.tsx?$': 'babel-jest',
},
coverageProvider: 'v8',
coverageDirectory: 'coverage',
collectCoverageFrom: ['**/src/**/*.{ts,tsx}', '!**/src/app.tsx'],
collectCoverage: true,
clearMocks: true,
moduleNameMapper: {
'\\.(svg|webp)$': '<rootDir>/__mocks__/file',
'react-i18next': '<rootDir>/__mocks__/react-i18next',
},
testEnvironmentOptions: {
customExportConditions: [''],
},
testEnvironment: 'jest-fixed-jsdom',
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/e2e'],
};

109
locales/en.json Normal file
View File

@ -0,0 +1,109 @@
{
"dry-wash.landing.benefits-section.description": "Discover the benefits of our car dry cleaning services",
"dry-wash.landing.benefits-section.heading": "Advantages of an eco-friendly car wash",
"dry-wash.landing.benefits-section.list.0": "Environmentally friendly products",
"dry-wash.landing.benefits-section.list.1": "Fast and efficient service",
"dry-wash.landing.benefits-section.list.2": "Convenient mobile access",
"dry-wash.landing.benefits-section.list.3": "Reliable and trustworthy",
"dry-wash.landing.footer.copyright": "\u00A9 {{currentYear}} DryMaster. All rights reserved",
"dry-wash.landing.footer.links.faq": "FAQ",
"dry-wash.landing.footer.links.privacy-policy": "Privacy policy",
"dry-wash.landing.footer.links.service-terms": "Terms of service",
"dry-wash.landing.hero-section.description": "Experience the ultimate convenience of a dry car wash, designed for a cleaner planet.",
"dry-wash.landing.hero-section.headline": "Revitalize Your Ride with Eco-Friendly Care!",
"dry-wash.landing.make-order-button": "Make order",
"dry-wash.landing.site-logo": "The logo of the \"Dry Master\" company",
"dry-wash.landing.social-proof-section.heading": "We are being chosen",
"dry-wash.order-create.title": "Order a car wash",
"dry-wash.order-create.form.field.validation.required": "This field is required",
"dry-wash.order-create.form.phone-field.label": "Phone number",
"dry-wash.order-create.form.phone-field.invalid": "Enter the valid phone number",
"dry-wash.order-create.form.car-number-field.label": "Car number",
"dry-wash.order-create.form.car-number-field.invalid": "Enter the valid vehicle number",
"dry-wash.order-create.form.car-color-field.label": "The color of the car",
"dry-wash.order-create.form.car-body-field.label": "Car body type",
"dry-wash.order-create.form.washing-datetime-field.label": "What time is the car available?",
"dry-wash.order-create.form.washing-location-field.label": "Where is the car located?",
"dry-wash.order-create.form.washing-location-field.placeholder": "Enter the address or select on the map",
"dry-wash.order-create.form.washing-location-field.help": "For example, 55.754364, 48.743295 Universitetskaya Street, 1, Innopolis, Verkhneuslonsky district, Republic of Tatarstan (Tatarstan), 420500",
"dry-wash.order-create.car-body-select.placeholder": "Not specified",
"dry-wash.order-create.car-body-select.options.sedan": "Sedan",
"dry-wash.order-create.car-body-select.options.hatchback" : "Hatchback",
"dry-wash.order-create.car-body-select.options.crossover" : "Crossover",
"dry-wash.order-create.car-body-select.options.suv" : "Sport utility vehicle",
"dry-wash.order-create.car-body-select.options.station-wagon" : "Station wagon",
"dry-wash.order-create.car-body-select.options.coupe" : "Coupe",
"dry-wash.order-create.car-body-select.options.minivan" : "Minivan",
"dry-wash.order-create.car-body-select.options.pickup" : "Pickup",
"dry-wash.order-create.car-body-select.options.liftback" : "Liftback",
"dry-wash.order-create.car-body-select.options.sports-car" : "Sports-car",
"dry-wash.order-create.car-body-select.options.other": "Other",
"dry-wash.order-create.form.submit-button.label": "Submit",
"dry-wash.order-create.order-creation-title": "Creating order ...",
"dry-wash.order-create.create-order-query.success.title": "The order is successfully created",
"dry-wash.order-create.create-order-query.error.title": "Failed to create an order",
"dry-wash.order-view.title": "Your order",
"dry-wash.order-view.get-order-query.error.title": "Failed to fetch the details of order",
"dry-wash.order-view.details.title": "Order #{{number}}",
"dry-wash.order-view.details.owner": "Owner",
"dry-wash.order-view.details.car": "Car",
"dry-wash.order-view.details.location": "Where",
"dry-wash.order-view.details.datetime-range": "When",
"dry-wash.order-view.details.alert": "The operator will contact you about the payment at the specified phone number",
"dry-wash.arm.master.add": "Add",
"dry-wash.arm.order.title": "Orders",
"dry-wash.arm.order.table.empty": "Table empty",
"dry-wash.arm.order.error.title": "Error loading data",
"dry-wash.arm.order.status.progress": "In Progress",
"dry-wash.arm.order.status.complete": "Completed",
"dry-wash.arm.order.status.pending": "Pending",
"dry-wash.arm.order.status.working": "Working",
"dry-wash.arm.order.status.canceled": "Canceled",
"dry-wash.arm.order.status.placeholder": "Select status",
"dry-wash.arm.order.master.placeholder": "Select master",
"dry-wash.arm.order.table.header.carNumber": "Car Number",
"dry-wash.arm.order.table.header.washingTime": "Washing Time",
"dry-wash.arm.order.table.header.orderDate": "Order Date",
"dry-wash.arm.order.table.header.status": "Status",
"dry-wash.arm.order.table.header.masters": "Master",
"dry-wash.arm.order.table.header.telephone": "Telephone",
"dry-wash.arm.order.table.header.location": "Location",
"dry-wash.arm.master.title": "Masters",
"dry-wash.arm.master.table.header.name": "Name",
"dry-wash.arm.master.table.empty": "Table empty",
"dry-wash.arm.master.error.title": "Error loading data",
"dry-wash.arm.master.table.header.currentJob": "Current Job",
"dry-wash.arm.master.table.header.phone": "Phone",
"dry-wash.arm.master.table.header.actions": "Actions",
"dry-wash.arm.master.table.actionsMenu.delete": "Delete Master",
"dry-wash.arm.master.schedule.empty": "free",
"dry-wash.arm.master.editable.aria.cancel": "Undo changes",
"dry-wash.arm.master.editable.aria.save": "Save changes ",
"dry-wash.arm.master.editable.aria.edit": "Edit",
"dry-wash.arm.master.drawer.title": "Add New Master",
"dry-wash.arm.master.drawer.inputName.label": "Full Name",
"dry-wash.arm.master.drawer.inputName.placeholder": "Enter Full Name",
"dry-wash.arm.master.drawer.inputPhone.label": "Phone Number",
"dry-wash.arm.master.drawer.inputPhone.placeholder": "Enter Phone Number",
"dry-wash.arm.master.drawer.button.save": "Save",
"dry-wash.arm.master.drawer.button.cancel": "Cancel",
"dry-wash.arm.master.drawer.toast.create-master": "Master created",
"dry-wash.arm.master.drawer.toast.error.empty-fields": "Fields cannot be empty",
"dry-wash.arm.master.drawer.toast.error.base": "Error",
"dry-wash.arm.master.drawer.toast.error.create-master": "Error creating master",
"dry-wash.arm.master.drawer.toast.error.create-master-details": "Failed to add master. Please try again",
"dry-wash.arm.master.drawer.form.name.required": "Master name is required",
"dry-wash.arm.master.drawer.form.phone.required": "Phone number is required",
"dry-wash.arm.master.drawer.form.phone.pattern": "Invalid phone number",
"dry-wash.arm.master.drawer.form.name.minLength": "Name must contain at least 2 characters",
"dry-wash.arm.master.sideBar.orders": "Orders",
"dry-wash.arm.master.sideBar.master": "Masters",
"dry-wash.arm.master.sideBar.title": "Dry Master",
"dry-wash.notFound.title": "Page Not Found",
"dry-wash.notFound.description": "Unfortunately, the page you are looking for does not exist.",
"dry-wash.notFound.button.back": "Back to Home",
"dry-wash.errorBoundary.title": "Something went wrong",
"dry-wash.errorBoundary.description": "We are already working on fixing the issue",
"dry-wash.errorBoundary.button.reload": "Reload Page",
"dry-wash.washTime.timeSlot": "{{start}} - {{end}}"
}

115
locales/ru.json Normal file
View File

@ -0,0 +1,115 @@
{
"dry-wash.arm.master.add": "Добавить",
"dry-wash.arm.order.title": "Заказы",
"dry-wash.arm.order.status.progress": "Выполняется",
"dry-wash.arm.order.status.complete": "Завершено",
"dry-wash.arm.order.status.pending": "В ожидании",
"dry-wash.arm.order.status.working": "В работе",
"dry-wash.arm.order.status.canceled": "Отменено",
"dry-wash.arm.order.status.placeholder": "Выберите статус",
"dry-wash.arm.order.master.placeholder": "Выберите мастера",
"dry-wash.arm.order.table.header.carNumber": "Номер машины",
"dry-wash.arm.order.table.header.washingTime": "Время мойки",
"dry-wash.arm.order.table.header.orderDate": "Дата заказа",
"dry-wash.arm.order.table.header.status": "Статус",
"dry-wash.arm.order.table.header.masters": "Мастер",
"dry-wash.arm.order.table.header.telephone": "Телефон",
"dry-wash.arm.order.table.header.location": "Расположение",
"dry-wash.arm.order.table.empty": "Список пуст",
"dry-wash.arm.order.error.title": "Ошибка при загрузке данных",
"dry-wash.arm.master.title": "Мастера",
"dry-wash.arm.master.table.empty": "Список пуст",
"dry-wash.arm.master.error.title": "Ошибка при загрузке данных",
"dry-wash.arm.master.table.header.name": "Имя",
"dry-wash.arm.master.table.header.currentJob": "Актуальная занятость",
"dry-wash.arm.master.table.header.phone": "Телефон",
"dry-wash.arm.master.table.header.actions": "Действия",
"dry-wash.arm.master.table.actionsMenu.delete": "Удалить мастера",
"dry-wash.arm.master.table.actionsMenu.toast.success": "Мастер удалён",
"dry-wash.arm.master.table.actionsMenu.toast.error.title": "Ошибка!",
"dry-wash.arm.master.table.actionsMenu.toast.error.description": "Не удалось удалить мастера. Попробуйте ещё раз.",
"dry-wash.arm.master.schedule.empty": "Свободен",
"dry-wash.arm.master.editable.aria.cancel": "Отменить изменения",
"dry-wash.arm.master.editable.aria.save": "Сохранить изменения",
"dry-wash.arm.master.editable.aria.edit": "Редактировать",
"dry-wash.arm.master.editable.toast.success": "Успешно!",
"dry-wash.arm.master.editable.toast.error.description": "Не удалось обновить данные",
"dry-wash.arm.master.editable.toast.error.title": "Ошибка!",
"dry-wash.arm.master.drawer.title": "Добавить нового мастера",
"dry-wash.arm.master.drawer.inputName.label": "ФИО",
"dry-wash.arm.master.drawer.inputName.placeholder": "Введите ФИО",
"dry-wash.arm.master.drawer.inputPhone.label": "Номер телефона",
"dry-wash.arm.master.drawer.inputPhone.placeholder": "Введите номер телефона",
"dry-wash.arm.master.drawer.button.save": "Сохранить",
"dry-wash.arm.master.drawer.button.cancel": "Отменить",
"dry-wash.arm.master.drawer.toast.create-master": "Мастер создан",
"dry-wash.arm.master.drawer.toast.error.empty-fields": "Поля не могут быть пустыми",
"dry-wash.arm.master.drawer.toast.error.base": "Ошибка",
"dry-wash.arm.master.drawer.toast.error.create-master": "Ошибка при создании мастера",
"dry-wash.arm.master.drawer.toast.error.create-master-details": "Не удалось добавить мастера. Попробуйте еще раз",
"dry-wash.arm.master.drawer.form.name.required": "Имя мастера обязательно",
"dry-wash.arm.master.drawer.form.phone.required": "Телефон обязателен",
"dry-wash.arm.master.drawer.form.phone.pattern": "Некорректный номер телефона",
"dry-wash.arm.master.drawer.form.name.minLength": "Имя должно содержать минимум 2 символа",
"dry-wash.arm.master.sideBar.orders": "Заказы",
"dry-wash.arm.master.sideBar.master": "Мастера",
"dry-wash.arm.master.sideBar.title": "Сухой мастер",
"dry-wash.landing.benefits-section.description": "Откройте для себя преимущества наших услуг по химчистке автомобилей",
"dry-wash.landing.benefits-section.heading": "Преимущества экологичной автомойки",
"dry-wash.landing.benefits-section.list.0": "Экологически безопасные продукты",
"dry-wash.landing.benefits-section.list.1": "Быстрое и эффективное обслуживание",
"dry-wash.landing.benefits-section.list.2": "Удобный мобильный доступ",
"dry-wash.landing.benefits-section.list.3": "Надежный и заслуживающий доверия",
"dry-wash.landing.footer.copyright": "\u00A9 {{currentYear}} DryMaster. Все права защищены",
"dry-wash.landing.footer.links.faq": "FAQ",
"dry-wash.landing.footer.links.privacy-policy": "Политика конфиденциальности",
"dry-wash.landing.footer.links.service-terms": "Условия обслуживания",
"dry-wash.landing.hero-section.description": "Ощутите максимальное удобство сухой мойки автомобилей, созданной для того, чтобы планета стала чище",
"dry-wash.landing.hero-section.headline": "Оживите свою поездку с помощью экологически чистого ухода!",
"dry-wash.landing.make-order-button": "Сделать заказ",
"dry-wash.landing.site-logo": "Логотип компании \u00ABDry Master\u00BB",
"dry-wash.landing.social-proof-section.heading": "Нас выбирают",
"dry-wash.order-create.title": "Заказать мойку",
"dry-wash.order-create.form.field.validation.required": "Это поле обязательно для заполнения",
"dry-wash.order-create.form.phone-field.label": "Номер телефона",
"dry-wash.order-create.form.phone-field.invalid": "Введите корректный номер телефона",
"dry-wash.order-create.form.car-number-field.label": "Номер автомобиля",
"dry-wash.order-create.form.car-number-field.invalid": "Введите корректный номер автомобиля",
"dry-wash.order-create.form.car-color-field.label": "Цвет автомобиля",
"dry-wash.order-create.form.car-body-field.label": "Тип кузова автомобиля",
"dry-wash.order-create.form.washing-datetime-field.label": "В какое время автомобиль доступен?",
"dry-wash.order-create.form.washing-location-field.label": "Где находится автомобиль?",
"dry-wash.order-create.form.washing-location-field.placeholder": "Введите адрес или выберите на карте",
"dry-wash.order-create.form.washing-location-field.help": "Например, 55.754364, 48.743295 Университетская улица, 1, Иннополис, Верхнеуслонский район, Республика Татарстан (Татарстан), 420500",
"dry-wash.order-create.car-body-select.placeholder": "Не указан",
"dry-wash.order-create.car-body-select.options.sedan": "Седан",
"dry-wash.order-create.car-body-select.options.hatchback": "Хэтчбек",
"dry-wash.order-create.car-body-select.options.crossover": "Кроссовер",
"dry-wash.order-create.car-body-select.options.suv": "Внедорожник",
"dry-wash.order-create.car-body-select.options.station-wagon": "Универсал",
"dry-wash.order-create.car-body-select.options.coupe": "Купе",
"dry-wash.order-create.car-body-select.options.minivan": "Минивэн",
"dry-wash.order-create.car-body-select.options.pickup": "Пикап",
"dry-wash.order-create.car-body-select.options.liftback": "Лифтбек",
"dry-wash.order-create.car-body-select.options.sports-car": "Спорткар",
"dry-wash.order-create.car-body-select.options.other": "Другой",
"dry-wash.order-create.form.submit-button.label": "Отправить",
"dry-wash.order-create.order-creation-title": "Создаем заказ ...",
"dry-wash.order-create.create-order-query.success.title": "Заказ успешно создан",
"dry-wash.order-create.create-order-query.error.title": "Не удалось создать заказ",
"dry-wash.order-view.title": "Ваш заказ",
"dry-wash.order-view.get-order-query.error.title": "Не удалось загрузить детали заказа",
"dry-wash.order-view.details.title": "Заказ №{{number}}",
"dry-wash.order-view.details.owner": "Владелец",
"dry-wash.order-view.details.car": "Автомобиль",
"dry-wash.order-view.details.location": "Где",
"dry-wash.order-view.details.datetime-range": "Когда",
"dry-wash.order-view.details.alert": "С вами свяжется оператор насчет оплаты по указанному номеру телефона",
"dry-wash.notFound.title": "Страница не найдена",
"dry-wash.notFound.description": "К сожалению, запрашиваемая вами страница не существует.",
"dry-wash.notFound.button.back": "Вернуться на главную",
"dry-wash.errorBoundary.title": "Что-то пошло не так",
"dry-wash.errorBoundary.description": "Мы уже работаем над исправлением проблемы",
"dry-wash.errorBoundary.button.reload": "Перезагрузить страницу",
"dry-wash.washTime.timeSlot": "{{start}} - {{end}}"
}

8836
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +1,74 @@
{
"name": "dry-wash",
"version": "0.0.0",
"description": "<a id=\"readme-top\"></a>",
"main": "./src/index.tsx",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "brojs server --port=8099 --with-open-browser",
"build": "npm run clean && brojs build --dev",
"build:prod": "npm run clean && brojs build",
"clean": "rimraf dist",
"eslint": "npx eslint .",
"eslint:fix": "npx eslint . --fix",
"preversion": "npm run eslint"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@brojs/cli": "^1.3.0",
"@chakra-ui/icons": "^2.2.4",
"@chakra-ui/react": "^2.4.2",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@types/react": "^18.3.12",
"express": "^4.21.1",
"framer-motion": "^6.2.8",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.27.0"
},
"devDependencies": {
"@eslint/js": "^9.14.0",
"@stylistic/eslint-plugin": "^2.10.1",
"@types/react-dom": "^18.3.1",
"eslint": "^9.14.0",
"eslint-plugin-react": "^7.37.2",
"globals": "^15.11.0",
"prettier": "3.3.3",
"typescript-eslint": "^8.12.2"
}
"name": "dry-wash",
"version": "0.7.2",
"description": "<a id=\"readme-top\"></a>",
"main": "./src/index.tsx",
"scripts": {
"test": "jest -u",
"start": "brojs server --port=8099 --with-open-browser",
"build": "npm run clean && brojs build --dev",
"build:prod": "npm run clean && brojs build",
"clean": "rimraf dist",
"eslint": "npx eslint .",
"eslint:fix": "npx eslint . --fix",
"preversion": "npm run eslint"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@babel/core": "^7.26.7",
"@babel/preset-env": "^7.26.7",
"@babel/preset-react": "^7.26.3",
"@babel/preset-typescript": "^7.26.0",
"@brojs/cli": "^1.8.4",
"@chakra-ui/icons": "^2.2.4",
"@chakra-ui/react": "^2.10.5",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@fontsource/open-sans": "^5.1.0",
"@lottiefiles/react-lottie-player": "^3.5.4",
"@pbe/react-yandex-maps": "^1.2.5",
"@reduxjs/toolkit": "^2.5.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/react": "^18.3.12",
"babel-jest": "^29.7.0",
"dayjs": "^1.11.13",
"express": "^4.21.1",
"framer-motion": "^6.2.8",
"i18next": "^23.16.4",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-fixed-jsdom": "^0.0.9",
"keycloak-js": "^23.0.7",
"msw": "^2.7.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.2",
"react-i18next": "^15.1.1",
"react-icons": "^5.3.0",
"react-phone-number-input": "^3.4.9",
"react-redux": "^9.2.0",
"react-router-dom": "^6.27.0",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2"
},
"devDependencies": {
"@eslint/js": "^9.14.0",
"@playwright/test": "^1.50.1",
"@stylistic/eslint-plugin": "^2.10.1",
"@types/node": "^22.13.1",
"@types/react-dom": "^18.3.1",
"eslint": "^9.14.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-react": "^7.37.2",
"globals": "^15.11.0",
"prettier": "3.3.3",
"typescript": "^5.7.3",
"typescript-eslint": "^8.12.2"
},
"jest": {
"preset": "./jest-preset-it/jest-preset.ts"
}
}

79
playwright.config.ts Normal file
View File

@ -0,0 +1,79 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});

BIN
remote-assets/demo.mp4 Normal file

Binary file not shown.

15
src/__data__/features.ts Normal file
View File

@ -0,0 +1,15 @@
import { getFeatures } from "@brojs/cli";
const features = getFeatures('dry-wash-pl');
export const FEATURE = {
orderViewStatusPolling: {
isOn: Boolean(features['order-view-status-polling']),
getValue: () => {
const interval = parseInt(features['order-view-status-polling'].value);
if (!Number.isNaN(interval)) {
return interval;
}
}
}
};

View File

@ -0,0 +1,84 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { getConfigValue } from '@brojs/cli';
import dayjs from 'dayjs';
import { Master, OrderArm } from '../../models/api';
import { extractBodyFromResponse } from './utils';
export type UpdateMasterPayload = Required<Pick<Master, 'id'>> &
Partial<Omit<Master, 'id'>>;
type UpdateOrderProps = Required<Pick<OrderArm, 'id'>> &
Partial<Pick<OrderArm, 'status' | 'notes'>> & {
master?: string;
};
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: new URL(getConfigValue('dry-wash.api'), location.origin).href,
}),
tagTypes: ['Masters', 'Orders'],
endpoints: (builder) => ({
getMasters: builder.query<Master[], void>({
query: () => ({ url: '/arm/masters' }),
transformResponse: extractBodyFromResponse<Master[]>,
providesTags: ['Masters'],
}),
updateOrders: builder.mutation<void, UpdateOrderProps>({
query: ({ id, status, notes, master }) => ({
url: `/order/${id}`,
method: 'PATCH',
body: { status, notes, master },
}),
invalidatesTags: ['Orders'],
}),
getOrders: builder.query<OrderArm[], { date: Date }>({
query: ({ date }) => {
const startDate = dayjs(date).startOf('day').toISOString();
const endDate = dayjs(date).endOf('day').toISOString();
return {
url: '/arm/orders',
method: 'POST',
body: { startDate, endDate },
};
},
transformResponse: extractBodyFromResponse<OrderArm[]>,
providesTags: ['Orders'],
}),
addMaster: builder.mutation<void, Pick<Master, 'name' | 'phone'>>({
query: (master) => ({
url: '/arm/masters',
method: 'POST',
body: master,
}),
invalidatesTags: ['Masters'],
}),
deleteMaster: builder.mutation<void, { id: string }>({
query: ({ id }) => ({
url: `/arm/masters/${id}`,
method: 'DELETE',
}),
invalidatesTags: ['Masters'],
}),
updateMaster: builder.mutation<void, UpdateMasterPayload>({
query: ({ id, name, phone }) => ({
url: `/arm/masters/${id}`,
method: 'PATCH',
body: { name, phone },
}),
invalidatesTags: ['Masters'],
}),
}),
});
export const {
useGetMastersQuery,
useAddMasterMutation,
useDeleteMasterMutation,
useUpdateMasterMutation,
useGetOrdersQuery,
useUpdateOrdersMutation,
} = api;

View File

@ -0,0 +1,23 @@
import { GetOrder, CreateOrder } from "../../models/api";
import { api } from "./api";
import { extractBodyFromResponse, extractErrorMessageFromResponse } from "./utils";
export const landingApi = api.injectEndpoints({
endpoints: ({ mutation, query }) => ({
getOrder: query<GetOrder.Response, GetOrder.Params>({
query: ({ orderId }) => `/order/${orderId}`,
transformResponse: extractBodyFromResponse<GetOrder.Response>,
transformErrorResponse: extractErrorMessageFromResponse,
}),
createOrder: mutation<CreateOrder.Response, CreateOrder.Params>({
query: ({ body }) => ({
url: `/order/create`,
body,
method: 'POST'
}),
transformResponse: extractBodyFromResponse<CreateOrder.Response>,
transformErrorResponse: extractErrorMessageFromResponse,
}),
})
});

View File

@ -0,0 +1,15 @@
import { FetchBaseQueryError } from "@reduxjs/toolkit/query";
import { BaseResponse } from "../../models/api";
export const extractBodyFromResponse = <Body>(response: BaseResponse<Body>) => {
if (response.success) {
return response.body;
}
};
export const extractErrorMessageFromResponse = ({ data }: FetchBaseQueryError) => {
if (typeof data === 'object' && 'message' in data && typeof data.message === 'string') {
return data.message;
}
};

14
src/__data__/store.ts Normal file
View File

@ -0,0 +1,14 @@
import { configureStore } from '@reduxjs/toolkit';
import { api } from './service/api';
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

40
src/__data__/urls.ts Normal file
View File

@ -0,0 +1,40 @@
import { generatePath } from 'react-router-dom';
import { getNavigationValue } from '@brojs/cli';
import { Order } from '../models/landing';
const getFullUrls = (url: string) =>
`${getNavigationValue('dry-wash.main')}${url}`;
export const URLs = {
landing: {
url: getNavigationValue('dry-wash.main'),
getUrl() {
return this.url;
},
},
orderCreate: {
url: getFullUrls(getNavigationValue('dry-wash.order.create')),
getUrl() {
return this.url;
},
},
orderView: {
url: getFullUrls(getNavigationValue('dry-wash.order.view')),
getUrl(orderId: Order.Id) {
return generatePath(this.url, { orderId });
},
},
armMaster: {
url: getNavigationValue('dry-wash.arm.master'),
isOn: Boolean(getNavigationValue('dry-wash.arm.master')),
},
armOrder: {
url: getNavigationValue('dry-wash.arm.order'),
isOn: Boolean(getNavigationValue('dry-wash.arm.order')),
},
armBase: {
url: getFullUrls(getNavigationValue('dry-wash.arm')),
isOn: Boolean(getNavigationValue('dry-wash.arm')),
},
};

View File

@ -1,15 +1,23 @@
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import Routers from './routes';
import { ChakraProvider, theme as chakraTheme } from '@chakra-ui/react';
import { Provider } from 'react-redux';
import Routers from './routes';
import ErrorBoundary from './components/ErrorBoundary';
import { store } from './__data__/store';
const App = () => {
return (
<ChakraProvider theme={chakraTheme}>
<BrowserRouter>
<Routers></Routers>
</BrowserRouter>
</ChakraProvider>
<Provider store={store}>
<ChakraProvider theme={chakraTheme}>
<ErrorBoundary>
<BrowserRouter>
<Routers />
</BrowserRouter>
</ErrorBoundary>
</ChakraProvider>
</Provider>
);
};

View File

@ -0,0 +1 @@
export { default as OrderCreationAnimation } from './order-creation.json';

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1 @@
export { default as LogoSvg } from './dry-master-logo.svg';

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,11 @@
export { default as CoupeImg } from './coupe.webp';
export { default as CrossoverImg } from './crossover.webp';
export { default as HatchbackImg } from './hatchback.webp';
export { default as LiftbackImg } from './liftback.webp';
export { default as MinivanImg } from './minivan.webp';
export { default as OtherImg } from './other.webp';
export { default as PickupImg } from './pickup.webp';
export { default as SedanImg } from './sedan.webp';
export { default as SportsCarImg } from './sports-car.webp';
export { default as StationWagonImg } from './station-wagon.webp';
export { default as SuvImg } from './suv.webp';

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -0,0 +1,2 @@
export * from './car-body-type';
export { default as DemoVideoPosterImg } from './demo-video-poster.webp';

View File

@ -0,0 +1,31 @@
import React from 'react';
import { Box, Button, Text } from '@chakra-ui/react';
import { ArrowBackIcon, ArrowForwardIcon } from '@chakra-ui/icons';
interface DateNavigatorProps {
currentDate: Date;
onPreviousDate: () => void;
onNextDate: () => void;
}
const DateNavigator = ({
currentDate,
onPreviousDate,
onNextDate,
}: DateNavigatorProps) => {
return (
<Box display='flex' alignItems='center' justifyContent='flex-start' mb='5'>
<Button onClick={onPreviousDate}>
<ArrowBackIcon />
</Button>
<Text mx='4' fontSize='lg' fontWeight='bold'>
{currentDate.toLocaleDateString()}
</Text>
<Button onClick={onNextDate}>
<ArrowForwardIcon />
</Button>
</Box>
);
};
export default DateNavigator;

View File

@ -0,0 +1 @@
export { default } from './DateNavigator';

View File

@ -0,0 +1,107 @@
import React, { useEffect, useState } from 'react';
import {
Editable,
EditableInput,
EditablePreview,
Flex,
IconButton,
Input,
useEditableControls,
ButtonGroup,
Stack,
} from '@chakra-ui/react';
import { CheckIcon, CloseIcon, EditIcon } from '@chakra-ui/icons';
import { useTranslation } from 'react-i18next';
import { useUpdateMasterMutation } from '../../__data__/service/api';
import useShowToast from '../../hooks/useShowToast';
interface EditableWrapperProps {
value: string;
fieldName: 'phone' | 'name';
id: string;
}
const EditableWrapper = ({ value, fieldName, id }: EditableWrapperProps) => {
const [updateMaster, { isError, isSuccess, error }] =
useUpdateMasterMutation();
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.master.editable',
});
const showToast = useShowToast();
const [currentValue, setCurrentValue] = useState<string>(value);
const handleSubmit = async (newValue: string) => {
if (currentValue === newValue) return;
await updateMaster({ id, [fieldName]: newValue });
setCurrentValue(newValue);
};
useEffect(() => {
if (isSuccess) {
showToast(t('toast.success'), 'success');
}
}, [isSuccess]);
useEffect(() => {
if (isError) {
showToast(t('toast.error.title'), 'error', t('toast.error.description'));
console.error(t('toast.error.description'), error);
}
}, [isError, error]);
function EditableControls() {
const {
isEditing,
getSubmitButtonProps,
getCancelButtonProps,
getEditButtonProps,
} = useEditableControls();
return isEditing ? (
<ButtonGroup justifyContent='center' size='sm'>
<IconButton
aria-label={t('aria.save')}
icon={<CheckIcon />}
{...getSubmitButtonProps()}
/>
<IconButton
aria-label={t('aria.cancel')}
icon={<CloseIcon />}
{...getCancelButtonProps()}
/>
</ButtonGroup>
) : (
<Flex justifyContent='center'>
<IconButton
aria-label={t('aria.edit')}
size='sm'
icon={<EditIcon />}
{...getEditButtonProps()}
/>
</Flex>
);
}
return (
<Editable
textAlign='center'
defaultValue={currentValue}
fontSize='2xl'
isPreviewFocusable={false}
onSubmit={handleSubmit}
>
<Stack direction={['column', 'row']} spacing='15px'>
<EditablePreview />
<Input as={EditableInput} />
<EditableControls />
</Stack>
</Editable>
);
};
export default EditableWrapper;

View File

@ -0,0 +1,65 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { Heading, Text, Button, Center, VStack } from '@chakra-ui/react';
import i18next from 'i18next';
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
interface ErrorBoundaryProps {
children: ReactNode;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(): Partial<ErrorBoundaryState> {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.error('Error caught by ErrorBoundary:', error, errorInfo);
this.setState({ error, errorInfo });
}
render() {
const { hasError } = this.state;
//TODO: добавить анимацию после залива 404 страницы
//TODO: может сделать обертку для хука, чтоб язык менялся без перезагрузки
if (hasError) {
return (
<Center minH='100vh'>
<VStack spacing={4} textAlign='center'>
<Heading as='h1' size='2xl'>
{i18next.t('~:dry-wash.errorBoundary.title')}
</Heading>
<Text fontSize='lg'>
{i18next.t('~:dry-wash.errorBoundary.description')}
</Text>
<Button
colorScheme='teal'
size='lg'
variant='outline'
onClick={() => window.location.reload()}
>
{i18next.t('~:dry-wash.errorBoundary.button.reload')}
</Button>
</VStack>
</Center>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@ -0,0 +1 @@
export { default } from './ErrorBoundary';

View File

@ -1,17 +1,37 @@
import { Box, Flex } from '@chakra-ui/react';
import React from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import Sidebar from '../Sidebar';
import Orders from '../Orders';
import Masters from '../Masters';
import React from 'react';
import { URLs } from '../../__data__/urls';
const LayoutArm = ({ currentPage, onSelectPage }) => (
<Flex h='100vh'>
<Sidebar onSelectPage={onSelectPage} />
<Box flex='1' bg='gray.50'>
{currentPage === 'orders' && <Orders />}
{currentPage === 'masters' && <Masters />}
</Box>
</Flex>
);
const LayoutArm = () => {
let defaultRedirect = null;
if (URLs.armOrder.isOn) {
defaultRedirect = URLs.armOrder.url;
} else if (URLs.armMaster.isOn) {
defaultRedirect = URLs.armMaster.url;
}
return (
<Flex h='100vh'>
<Sidebar />
<Box flex='1' bg='gray.50'>
<Routes>
<Route index element={<Navigate to={defaultRedirect} replace />} />
{URLs.armOrder.isOn && (
<Route path={URLs.armOrder.url} element={<Orders />} />
)}
{URLs.armMaster.isOn && (
<Route path={URLs.armMaster.url} element={<Masters />} />
)}
</Routes>
</Box>
</Flex>
);
};
export default LayoutArm;

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import {
Menu,
MenuButton,
@ -7,15 +7,49 @@ import {
IconButton,
} from '@chakra-ui/react';
import { EditIcon } from '@chakra-ui/icons';
import { useTranslation } from 'react-i18next';
import { useDeleteMasterMutation } from '../../__data__/service/api';
import useShowToast from '../../hooks/useShowToast';
interface MasterActionsMenu {
id: string;
}
const MasterActionsMenu = ({ id }: MasterActionsMenu) => {
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.master.table.actionsMenu',
});
const [deleteMaster, { isSuccess, isError, error, isLoading }] =
useDeleteMasterMutation();
const showToast = useShowToast();
const handleClickDelete = async () => {
await deleteMaster({ id });
};
useEffect(() => {
if (isSuccess) {
showToast(t('toast.success'), 'success');
}
}, [isSuccess]);
useEffect(() => {
if (isError) {
showToast(t('toast.error.title'), 'error', t('toast.error.description'));
console.error(error);
}
}, [isError]);
const MasterActionsMenu = () => {
return (
<Menu>
<MenuButton icon={<EditIcon />} as={IconButton} variant='outline' />
<MenuList>
<MenuItem>Посмотреть профиль</MenuItem>
<MenuItem>Изменить расписание</MenuItem>
<MenuItem>Удалить мастера</MenuItem>
<MenuItem onClick={handleClickDelete} isDisabled={isLoading}>
{t('delete')}
</MenuItem>
</MenuList>
</Menu>
);

View File

@ -0,0 +1,142 @@
import React, { useEffect } from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';
import {
Button,
FormControl,
FormLabel,
Input,
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerOverlay,
InputGroup,
InputLeftElement,
FormErrorMessage,
} from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { PhoneIcon } from '@chakra-ui/icons';
import { useAddMasterMutation } from '../../__data__/service/api';
import { DrawerInputs } from '../../models/arm/form';
import useShowToast from '../../hooks/useShowToast';
interface MasterDrawerProps {
isOpen: boolean;
onClose: () => void;
}
const MasterDrawer = ({ isOpen, onClose }: MasterDrawerProps) => {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<DrawerInputs>();
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.master.drawer',
});
const onSubmit: SubmitHandler<DrawerInputs> = async (data) => {
const trimMaster = {
name: data.name.trim(),
phone: data.phone.trim(),
};
const isEmptyFields = trimMaster.name === '' || trimMaster.phone === '';
if (isEmptyFields) {
showToast(t('toast.error.base'), 'error', t('toast.error.empty-fields'));
return;
}
await addMaster(trimMaster);
};
const [addMaster, { error, isSuccess }] = useAddMasterMutation();
const showToast = useShowToast();
useEffect(() => {
if (isSuccess) {
showToast(t('toast.create-master'), 'success');
reset();
onClose();
}
}, [isSuccess]);
useEffect(() => {
if (error) {
showToast(
t('toast.error.create-master'),
'error',
t('toast.error.create-master-details'),
);
console.error(error);
}
}, [error]);
return (
<Drawer isOpen={isOpen} onClose={onClose} size='md'>
<DrawerOverlay />
<DrawerContent>
<form onSubmit={handleSubmit(onSubmit)}>
<DrawerCloseButton />
<DrawerHeader>{t('title')}</DrawerHeader>
<DrawerBody>
<FormControl mb='4' isInvalid={!!errors.name}>
<FormLabel>{t('inputName.label')}</FormLabel>
<Input
{...register('name', {
required: t('form.name.required'),
minLength: {
value: 2,
message: t('form.name.minLength'),
},
})}
placeholder={t('inputName.placeholder')}
/>
<FormErrorMessage>
{errors.name && errors.name.message}
</FormErrorMessage>
</FormControl>
<FormControl isInvalid={!!errors.phone}>
<FormLabel>{t('inputPhone.label')}</FormLabel>
<InputGroup>
<InputLeftElement pointerEvents='none'>
<PhoneIcon color='gray.300' />
</InputLeftElement>
<Input
{...register('phone', {
required: t('form.phone.required'),
pattern: {
value: /^(\+7|8)\d{10}$/,
message: t('form.phone.pattern'),
},
setValueAs: (value) => value.replace(/[^\d+]/g, ''),
})}
placeholder={t('inputPhone.placeholder')}
/>
</InputGroup>
<FormErrorMessage>
{errors.phone && errors.phone.message}
</FormErrorMessage>
</FormControl>
</DrawerBody>
<DrawerFooter>
<Button colorScheme='teal' mr={3} type='submit'>
{t('button.save')}
</Button>
<Button variant='ghost' onClick={onClose}>
{t('button.cancel')}
</Button>
</DrawerFooter>
</form>
</DrawerContent>
</Drawer>
);
};
export default MasterDrawer;

View File

@ -1,25 +1,39 @@
import React from 'react';
import { Badge, Link, Stack, Td, Tr } from '@chakra-ui/react';
import MasterActionsMenu from '../MasterActionsMenu';
import { Badge, Stack, Td, Tr, Text } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import MasterActionsMenu from '../MasterActionsMenu';
import { getTimeSlot } from '../../lib';
import EditableWrapper from '../Editable/Editable';
const MasterItem = ({ name, phone, id, schedule }) => {
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.master',
});
const MasterItem = ({ name, schedule, phone }) => {
return (
<Tr>
<Td>{name}</Td>
<Td>
<Stack direction='row'>
{schedule.map((time, index) => (
<Badge colorScheme={'green'} key={index}>
{time}
</Badge>
))}
</Stack>
<EditableWrapper id={id} fieldName={'name'} value={name} />
</Td>
<Td>
<Link href='tel:'>{phone}</Link>
{schedule?.length > 0 ? (
<Stack direction='row'>
{schedule?.map(({ startWashTime, endWashTime }, index: number) => (
<Badge colorScheme={'green'} key={index}>
{getTimeSlot(startWashTime, endWashTime)}
</Badge>
))}
</Stack>
) : (
<Text color='gray.500'>{t('schedule.empty')}</Text>
)}
</Td>
<Td>
<MasterActionsMenu />
<EditableWrapper id={id} fieldName={'phone'} value={phone} />
</Td>
<Td>
<MasterActionsMenu id={id} />
</Td>
</Tr>
);

View File

@ -1,65 +0,0 @@
import React, { useState } from 'react';
import {
Button,
FormControl,
FormLabel,
Input,
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerOverlay,
} from '@chakra-ui/react';
const MasterDrawer = ({ isOpen, onClose }) => {
const [newMaster, setNewMaster] = useState({ name: '', phone: '' });
const handleSave = () => {
console.log(`Сохранение мастера: ${newMaster}`);
onClose();
};
return (
<Drawer isOpen={isOpen} onClose={onClose} size='md'>
<DrawerOverlay />
<DrawerContent>
<DrawerCloseButton />
<DrawerHeader>Добавить нового мастера</DrawerHeader>
<DrawerBody>
<FormControl mb='4'>
<FormLabel>ФИО</FormLabel>
<Input
value={newMaster.name}
onChange={(e) =>
setNewMaster({ ...newMaster, name: e.target.value })
}
placeholder='Введите ФИО'
/>
</FormControl>
<FormControl>
<FormLabel>Номер телефона</FormLabel>
<Input
value={newMaster.phone}
onChange={(e) =>
setNewMaster({ ...newMaster, phone: e.target.value })
}
placeholder='Введите номер телефона'
/>
</FormControl>
</DrawerBody>
<DrawerFooter>
<Button colorScheme='teal' mr={3} onClick={handleSave}>
Сохранить
</Button>
<Button variant='ghost' onClick={onClose}>
Отменить
</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
};
export default MasterDrawer;

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import {
Box,
Heading,
@ -10,36 +10,75 @@ import {
Button,
useDisclosure,
Flex,
Td,
Text,
Spinner,
} from '@chakra-ui/react';
import { mastersData } from '../../mocks';
import MasterItem from '../MasterItem';
import MasterDrawer from '../MasterModal';
import { useTranslation } from 'react-i18next';
const TABLE_HEADERS = ['Имя', 'Актуальная занятость', 'Телефон', 'Действия'];
import MasterItem from '../MasterItem';
import MasterDrawer from '../MasterDrawer';
import { useGetMastersQuery } from '../../__data__/service/api';
import useShowToast from '../../hooks/useShowToast';
const TABLE_HEADERS = [
'name' as const,
'currentJob' as const,
'phone' as const,
'actions' as const,
];
const Masters = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
const showToast = useShowToast();
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.master',
});
const { data: masters, error, isLoading, isSuccess } = useGetMastersQuery();
useEffect(() => {
if (error) {
showToast(t('error.title'), 'error');
}
}, [error]);
return (
<Box p='8'>
<Flex justifyContent='space-between' alignItems='center' mb='5'>
<Heading size='lg'>Мастера</Heading>
<Heading size='lg'> {t('title')}</Heading>
<Button colorScheme='green' onClick={onOpen}>
+ Добавить
+ {t('add')}
</Button>
</Flex>
<Table variant='simple' colorScheme='blackAlpha'>
<Thead>
<Tr>
{TABLE_HEADERS.map((name) => (
<Th key={name}>{name}</Th>
<Th key={name}>{t(`table.header.${name}`)}</Th>
))}
</Tr>
</Thead>
<Tbody>
{mastersData.map((master, index) => (
<MasterItem key={index} {...master} />
))}
{isLoading && (
<Tr>
<Td colSpan={TABLE_HEADERS.length} textAlign='center' py='8'>
<Spinner size='lg' />
</Td>
</Tr>
)}
{isSuccess && masters.length === 0 && (
<Tr>
<Td colSpan={TABLE_HEADERS.length}>
<Text>{t('table.empty')}</Text>
</Td>
</Tr>
)}
{isSuccess &&
masters.map((master, index) => (
<MasterItem key={index} {...master} />
))}
</Tbody>
</Table>
<MasterDrawer isOpen={isOpen} onClose={onClose} />

View File

@ -1,32 +1,93 @@
import React, { useState } from 'react';
import React, { ChangeEvent, useState } from 'react';
import { Td, Tr, Link, Select } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import { getTimeSlot } from '../../lib';
import { useUpdateOrdersMutation } from '../../__data__/service/api';
import { OrderArm, Status, statuses } from '../../models/api';
const statusColors: Record<Status, string> = {
pending: 'yellow.100',
progress: 'blue.100',
working: 'orange.100',
canceled: 'red.100',
complete: 'green.100',
};
const OrderItem = ({
carNumber,
washTime,
startWashTime,
endWashTime,
orderDate,
status,
phone,
location,
}) => {
master,
allMasters,
id,
}: OrderArm) => {
const [updateOrders] = useUpdateOrdersMutation();
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.order',
});
const [statusSelect, setStatus] = useState(status);
const bgColor = statusColors[statusSelect];
const [masterSelect, setMaster] = useState(master?.name);
const handelChangeMasters = (e: ChangeEvent<HTMLSelectElement>) => {
const masterName = e.target.value;
const selectedMaster = allMasters.find(
(master) => master.name === masterName,
);
if (selectedMaster) {
setMaster(masterName);
updateOrders({ id, master: selectedMaster.id });
} else {
console.error('Master not found');
}
};
const handeChangeStatus = (e: ChangeEvent<HTMLSelectElement>) => {
const status = e.target.value as OrderArm['status'];
updateOrders({ id, status });
setStatus(status);
};
return (
<Tr>
<Td>{carNumber}</Td>
<Td>{washTime}</Td>
<Td>{orderDate}</Td>
<Td>
{dayjs(orderDate).format('DD.MM.YYYY')} <br />
{getTimeSlot(startWashTime, endWashTime)}
</Td>
<Td>
<Select
value={statusSelect}
onChange={(e) => setStatus(e.target.value)}
placeholder='Выберите статус'
onChange={handeChangeStatus}
placeholder={t(`status.placeholder`)}
bg={bgColor}
>
<option value='в ожидании'>в ожидании</option>
<option value='В процессе'>в процессе</option>
<option value='в работе'>в работе</option>
<option value='отменил'>отменил</option>
<option value='Завершено'>Завершено</option>
{statuses.map((status) => (
<option key={status} value={status}>
{t(`status.${status}`)}
</option>
))}
</Select>
</Td>
<Td>
<Select
value={masterSelect}
onChange={handelChangeMasters}
placeholder={t(`master.placeholder`)}
>
{allMasters.map((item) => (
<option key={item.id} value={item.name}>
{item.name}
</option>
))}
</Select>
</Td>
<Td>

View File

@ -1,34 +1,120 @@
import { Box, Heading, Table, Thead, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import { ordersData } from '../../mocks';
import React, { useEffect, useState } from 'react';
import {
Box,
Heading,
Table,
Thead,
Tbody,
Tr,
Th,
Spinner,
Text,
Td,
} from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import OrderItem from '../OrderItem';
import { OrderProps } from '../OrderItem/OrderItem';
import DateNavigator from '../DateNavigator';
import {
useGetMastersQuery,
useGetOrdersQuery,
} from '../../__data__/service/api';
import useShowToast from '../../hooks/useShowToast';
const TABLE_HEADERS = [
'carNumber' as const,
'orderDate' as const,
'status' as const,
'masters' as const,
'telephone' as const,
'location' as const,
];
const Orders = () => {
const TABLE_HEADERS = [
'Номер машины',
'Время мойки',
'Дата заказа',
'Статус',
'Телефон',
'Расположение',
];
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.order',
});
const showToast = useShowToast();
const [currentDate, setCurrentDate] = useState(new Date());
const {
data: orders,
isLoading: isOrdersLoading,
isSuccess: isOrdersSuccess,
isError: isOrdersError,
error: ordersError,
} = useGetOrdersQuery({ date: currentDate });
const {
data: masters,
isLoading: isMastersLoading,
isSuccess: isMastersSuccess,
isError: isMastersError,
error: mastersError,
} = useGetMastersQuery();
const isLoading = isOrdersLoading || isMastersLoading;
const isSuccess = isOrdersSuccess && isMastersSuccess;
const isError = isOrdersError || isMastersError;
useEffect(() => {
if (isError) {
showToast(t('error.title'), 'error');
}
}, [isError, ordersError, mastersError, t]);
return (
<Box p='8'>
<Heading size='lg' mb='5'>
Заказы
{t('title')}
</Heading>
<DateNavigator
currentDate={currentDate}
onPreviousDate={() =>
setCurrentDate((prevDate) =>
dayjs(prevDate).subtract(1, 'day').toDate(),
)
}
onNextDate={() =>
setCurrentDate((prevDate) => dayjs(prevDate).add(1, 'day').toDate())
}
/>
<Table variant='simple' colorScheme='blackAlpha'>
<Thead>
<Tr>
{TABLE_HEADERS.map((name, key) => (
<Th key={key}>{name}</Th>
<Th key={key}>{t(`table.header.${name}`)}</Th>
))}
</Tr>
</Thead>
<Tbody>
{ordersData.map((order, index) => (
<OrderItem key={index} {...order} />
))}
{isLoading && (
<Tr>
<Td colSpan={TABLE_HEADERS.length} textAlign='center' py='8'>
<Spinner size='lg' />
</Td>
</Tr>
)}
{isSuccess && orders.length === 0 && (
<Tr>
<Td colSpan={TABLE_HEADERS.length}>
<Text>{t('table.empty')}</Text>
</Td>
</Tr>
)}
{isSuccess &&
orders.map((order, index) => (
<OrderItem
allMasters={masters}
key={index}
{...order}
status={order.status as OrderProps['status']}
/>
))}
</Tbody>
</Table>
</Box>

View File

@ -0,0 +1,16 @@
import React, { FC } from 'react';
import { Flex, Spinner } from '@chakra-ui/react';
export const PageSpinner: FC = () => {
return (
<Flex w='full' h='100vh' justifyContent='center' alignItems='center'>
<Spinner
thickness='5px'
speed='0.65s'
emptyColor='gray.200'
color='green.500'
size='xl'
/>
</Flex>
);
};

View File

@ -0,0 +1 @@
export { PageSpinner } from './PageSpinner';

View File

@ -1,42 +1,60 @@
import { Box, Button, Heading, VStack } from '@chakra-ui/react';
import { Box, Button, Heading, VStack, Divider } from '@chakra-ui/react';
import React from 'react';
import { Divider } from '@chakra-ui/react';
import { useLocation, Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
const Sidebar = ({ onSelectPage }) => (
<Box
borderRight='1px solid black'
bg='gray.50'
color='white'
w='250px'
p='5'
pt='8'
>
<Heading color='green' size='lg' mb='5'>
Сухой мастер
</Heading>
import { URLs } from '../../__data__/urls';
<VStack align='start' spacing='4'>
<Divider />
<Button
onClick={() => onSelectPage('orders')}
w='100%'
colorScheme='green'
variant='ghost'
>
Заказы
</Button>
<Divider />
<Button
onClick={() => onSelectPage('masters')}
w='100%'
colorScheme='green'
variant='ghost'
>
Мастера
</Button>
<Divider />
</VStack>
</Box>
);
const Sidebar = () => {
const location = useLocation();
const isActive = (keyword: string) => location.pathname.includes(keyword);
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.master.sideBar',
});
return (
<Box
borderRight='1px solid black'
bg='gray.50'
color='white'
w='250px'
p='5'
pt='8'
>
<Heading color='green' size='lg' mb='5'>
{t('title')}
</Heading>
<VStack align='start' spacing='4'>
<Divider />
{URLs.armOrder.isOn && (
<Button
as={Link}
to={URLs.armOrder.url}
w='100%'
colorScheme={isActive(URLs.armOrder.url) ? 'green' : 'blue'}
variant={isActive(URLs.armOrder.url) ? 'solid' : 'ghost'}
>
{t('orders')}
</Button>
)}
<Divider />
{URLs.armMaster.isOn && (
<Button
as={Link}
to={URLs.armMaster.url}
w='100%'
colorScheme={isActive(URLs.armMaster.url) ? 'green' : 'blue'}
variant={isActive(URLs.armMaster.url) ? 'solid' : 'ghost'}
>
{t('master')}
</Button>
)}
<Divider />
</VStack>
</Box>
);
};
export default Sidebar;

1
src/components/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './PageSpinner';

View File

@ -0,0 +1,34 @@
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { Heading, HStack, List, Text, VStack } from '@chakra-ui/react';
import { CtaButton, PageSection } from '../';
import { ListItem } from './ListItem';
import { BenefitsSectionProps } from './types';
import { iconsMap } from './helper';
export const BenefitsSection: FC<BenefitsSectionProps> = ({
data: { heading, description, list } = {},
}) => {
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' });
return (
<PageSection>
<VStack w='full' spacing={2}>
<Heading as='h2'>{t(heading)}</Heading>
<Text>{t(description)}</Text>
</VStack>
<List display='flex' flexDirection='column' spacing={3}>
{list.map((itemKey, i) => (
<ListItem key={i} Icon={iconsMap[itemKey]}>
{t(itemKey)}
</ListItem>
))}
</List>
<HStack w='full' justify='flex-end'>
<CtaButton />
</HStack>
</PageSection>
);
};

View File

@ -0,0 +1,16 @@
import React, { FC, PropsWithChildren } from 'react';
import { ListIcon, ListItem as ChakraListItem } from '@chakra-ui/react';
import { IconType } from 'react-icons';
export type ListItemProps = PropsWithChildren & {
Icon: IconType;
};
export const ListItem: FC<ListItemProps> = ({ Icon, children }) => {
return (
<ChakraListItem display='inline-flex'>
<ListIcon as={Icon} color='primary.500' boxSize='6' />
{children}
</ChakraListItem>
);
};

View File

@ -0,0 +1 @@
export { type ListItemProps, ListItem } from './ListItem';

View File

@ -0,0 +1,13 @@
import { IconType } from "react-icons";
import { MdEco, MdMiscellaneousServices, MdPlace, MdHandshake } from "react-icons/md";
import { ArrElement } from "../../../lib";
import { BenefitsList } from "./types";
export const iconsMap: Record<ArrElement<BenefitsList>, IconType> = {
"benefits-section.list.0": MdEco,
"benefits-section.list.1": MdMiscellaneousServices,
"benefits-section.list.2": MdPlace,
"benefits-section.list.3": MdHandshake,
};

View File

@ -0,0 +1,2 @@
export type { BenefitsSectionProps } from './types';
export { BenefitsSection } from './BenefitsSection';

View File

@ -0,0 +1,14 @@
export type BenefitsList = [
'benefits-section.list.0',
'benefits-section.list.1',
'benefits-section.list.2',
'benefits-section.list.3',
];
export type BenefitsSectionProps = {
data: {
heading: 'benefits-section.heading';
description: 'benefits-section.description';
list: BenefitsList;
};
};

View File

@ -0,0 +1,21 @@
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { Link as RouterLink } from 'react-router-dom';
import { ButtonProps, Button } from '@chakra-ui/react';
import { URLs } from '../../../__data__/urls';
export const CtaButton: FC<ButtonProps> = (props) => {
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' });
return (
<Button
as={RouterLink}
to={URLs.orderCreate.getUrl()}
colorScheme='primary'
{...props}
>
{t('make-order-button')}
</Button>
);
};

View File

@ -0,0 +1 @@
export { CtaButton } from './CtaButton';

View File

@ -0,0 +1,11 @@
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { Text } from '@chakra-ui/react';
const currentYear = new Date().getFullYear();
export const Copyright: FC = () => {
const { t } = useTranslation();
return <Text color='whiteAlpha.500'>{t('dry-wash.landing.footer.copyright', { currentYear })}</Text>;
};

View File

@ -0,0 +1 @@
export { Copyright } from './Copyright';

View File

@ -0,0 +1,36 @@
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, List, ListItem } from '@chakra-ui/react';
import { Link as RouterLink } from 'react-router-dom';
import { SiteLogo, PageSection } from '../';
import { Copyright } from './Copyright';
export const Footer: FC = () => {
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.landing.footer.links',
});
const listData = [
{ to: '#', label: t('privacy-policy') },
{ to: '#', label: t('service-terms') },
{ to: '#', label: t('faq') },
];
return (
<PageSection as='footer' py={5} bg='gray.700' color='white'>
<SiteLogo />
<Copyright />
<List spacing={2}>
{listData.map(({ to, label }, i) => (
<ListItem key={i}>
<Link as={RouterLink} to={to}>
{label}
</Link>
</ListItem>
))}
</List>
</PageSection>
);
};

View File

@ -0,0 +1 @@
export { Footer } from './Footer';

View File

@ -0,0 +1,65 @@
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Heading, Text, Center, VStack } from '@chakra-ui/react';
import { DemoVideoPosterImg } from '../../../assets/images';
import { CtaButton, SiteLogo, PageSection } from '../';
import { HeroSectionProps } from './types';
export const HeroSection: FC<HeroSectionProps> = ({
data: { headline, description, video } = {},
flexShrink,
}) => {
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' });
return (
<Box flexShrink={flexShrink} as='header' pos='relative' zIndex={0}>
<Box
as='video'
src={`${__webpack_public_path__}/remote-assets/${video}`}
poster={DemoVideoPosterImg}
autoPlay
loop
muted
w='full'
h='full'
pos='absolute'
objectFit='cover'
filter='brightness(50%)'
zIndex={-1}
/>
<PageSection
h='full'
minH='375px'
maxH='1000px'
py={10}
justifyContent='center'
alignItems='center'
spacing={8}
>
<Center>
<SiteLogo />
</Center>
<VStack spacing={4}>
<Heading
as='h1'
textAlign='center'
color='white'
__css={{ textWrap: 'balance' }}
>
{t(headline)}
</Heading>
<Text
textAlign='center'
__css={{ textWrap: 'balance' }}
color='white'
>
{t(description)}
</Text>
</VStack>
<CtaButton size='lg' />
</PageSection>
</Box>
);
};

View File

@ -0,0 +1 @@
export { HeroSection } from './HeroSection';

View File

@ -0,0 +1,9 @@
import { BoxProps } from "@chakra-ui/react";
export type HeroSectionProps = {
data: {
headline: 'hero-section.headline';
description: 'hero-section.description';
video: string;
};
} & Pick<BoxProps, 'flexShrink'>;

View File

@ -0,0 +1,12 @@
import React, { FC, PropsWithChildren } from 'react';
import { StackProps, VStack } from '@chakra-ui/react';
type PageSectionProps = StackProps & PropsWithChildren;
export const PageSection: FC<PageSectionProps> = ({ children, ...restProps }) => {
return (
<VStack as='section' w='full' px={5} py={5} spacing={6} alignItems='flex-start' {...restProps}>
{children}
</VStack>
);
};

View File

@ -0,0 +1 @@
export { PageSection } from './PageSection';

View File

@ -0,0 +1,13 @@
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { Image } from '@chakra-ui/react';
import { LogoSvg } from '../../../assets/icons';
export const SiteLogo: FC = () => {
const { t } = useTranslation();
return <Image src={LogoSvg} alt={t('~:dry-wash.landing.site-logo')} w={40} />;
};
// todo: replace Image by SVG React component

View File

@ -0,0 +1 @@
export { SiteLogo } from './SiteLogo';

View File

@ -0,0 +1,37 @@
import React, { FC } from 'react';
import { Card, Avatar, Text } from '@chakra-ui/react';
type ReviewCardProps = {
firstname: string;
lastname: string;
picture: string;
text: string;
};
export const ReviewCard: FC<ReviewCardProps> = ({
firstname,
lastname,
picture,
text,
}) => {
const name = [firstname, lastname].join(' ');
return (
<Card p={4} gap={2} alignItems='center' variant='elevated'>
<Avatar
name={name}
src={picture}
size='xl'
boxShadow='2px 2px 0 1px var(--chakra-colors-secondary-500)'
/>
<Text
as='q'
fontSize='sm'
textAlign='center'
__css={{ textWrap: 'balance' }}
>
{text}
</Text>
</Card>
);
};

View File

@ -0,0 +1 @@
export { ReviewCard } from './ReviewCard';

View File

@ -0,0 +1,80 @@
import React, { FC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
Text,
} from '@chakra-ui/react';
import { mockReview } from '../../../../mocks/landing';
import { Review } from '../../../../models';
import { ReviewCard } from './ReviewCard';
const SLIDE_CHANGE_INTERVAL = 5000;
export const ReviewsSlider: FC = () => {
const { i18n } = useTranslation();
const mockReviews: Review.View[] = mockReview.getReviewsByLocale(i18n.language);
const reviewsCount = mockReviews.length;
const [activeTab, setActiveTab] = useState(0);
const [isSlideShowStopped, setIsSlideShowStopped] = useState(false);
useEffect(() => {
const timer = setInterval(() => {
const newActiveTab = (activeTab + 1) % reviewsCount;
setActiveTab(newActiveTab);
}, SLIDE_CHANGE_INTERVAL);
if (isSlideShowStopped) {
clearInterval(timer);
}
return () => {
clearInterval(timer);
};
}, [activeTab]);
return (
<Tabs
index={activeTab}
onChange={(selectedTab) => {
setIsSlideShowStopped(true);
setActiveTab(selectedTab);
}}
display='flex'
flexDir='column'
alignItems='center'
variant='soft-rounded'
colorScheme='gray'
>
<TabList gap={2}>
{mockReviews.map(({ id }, i) => (
<Tab
key={id}
w={4}
h={4}
p={0}
bg='gray.100'
_selected={{
bg: 'secondary.100',
}}
>
<Text visibility='hidden'>{i}</Text>
</Tab>
))}
</TabList>
<TabPanels>
{mockReviews.map(({ id, ...reviewProps }) => (
<TabPanel key={id}>
<ReviewCard {...reviewProps} />
</TabPanel>
))}
</TabPanels>
</Tabs>
);
};

View File

@ -0,0 +1 @@
export { ReviewsSlider } from './ReviewsSlider';

View File

@ -0,0 +1,24 @@
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { Heading, HStack } from '@chakra-ui/react';
import { CtaButton, PageSection } from '../';
import { ReviewsSlider } from './ReviewsSlider';
import { SocialProofSectionProps } from './types';
export const SocialProofSection: FC<SocialProofSectionProps> = ({
data: { heading } = {},
}) => {
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' });
return (
<PageSection>
<Heading as='h2'>{t(heading)}</Heading>
<ReviewsSlider />
<HStack w='full' justify='flex-end'>
<CtaButton />
</HStack>
</PageSection>
);
};

View File

@ -0,0 +1,2 @@
export type { SocialProofSectionProps } from './types';
export { SocialProofSection } from './SocialProofSection';

View File

@ -0,0 +1,5 @@
export type SocialProofSectionProps = {
data: {
heading: 'social-proof-section.heading';
};
};

View File

@ -0,0 +1,7 @@
export * from './CtaButton';
export * from './PageSection';
export * from './BenefitsSection'; // CtaButton, PageSection
export * from './SocialProofSection'; // CtaButton, PageSection
export * from './SiteLogo';
export * from './Footer'; // PageSection, SiteLogo
export * from './HeroSection'; // CtaButton, PageSection, SiteLogo

View File

@ -0,0 +1,82 @@
import React, { forwardRef } from 'react';
import {
Input,
Box,
Popover,
PopoverAnchor,
PopoverContent,
PopoverBody,
useRadioGroup,
Grid,
GridItem,
UseRadioGroupProps,
useDisclosure,
} from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { carBodySelectOptions, getInputValue } from './helper';
import { CarBodyOption } from './option';
import { CarBodySelectProps } from './types';
export const CarBodySelect = forwardRef<HTMLInputElement, CarBodySelectProps>(
function CarBodySelect(props, ref) {
const handleOptionClick: UseRadioGroupProps['onChange'] = (value) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
props.onChange(value);
};
const { value, getRadioProps, getRootProps } = useRadioGroup({
defaultValue: props.value,
value: props.value,
onChange: handleOptionClick,
});
const { isOpen, onOpen, onClose } = useDisclosure();
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.order-create.car-body-select',
});
return (
<Box width='100%'>
<Popover
isOpen={isOpen}
autoFocus={false}
placement='bottom-start'
matchWidth
>
<PopoverAnchor>
<Input
{...props}
ref={ref}
value={getInputValue(Number(value), t) ?? props.value}
readOnly
onClick={onOpen}
onBlur={onClose}
placeholder={t('placeholder')}
/>
</PopoverAnchor>
<PopoverContent width='100%' maxWidth='100%'>
<PopoverBody border='1px' borderColor='gray.300' p={0}>
<Grid
templateColumns='repeat(auto-fit, minmax(150px, 1fr))'
{...getRootProps()}
>
{carBodySelectOptions.map(({ value, img, labelTKey }) => (
<GridItem key={value}>
<CarBodyOption
image={img}
label={t(`options.${labelTKey}`)}
{...getRadioProps({ value: String(value) })}
/>
</GridItem>
))}
</Grid>
</PopoverBody>
</PopoverContent>
</Popover>
</Box>
);
},
);

View File

@ -0,0 +1,87 @@
import { TFunction } from "i18next";
import { InputProps } from "@chakra-ui/react";
import {
CoupeImg,
CrossoverImg,
HatchbackImg,
LiftbackImg,
MinivanImg,
PickupImg,
SedanImg,
SportsCarImg,
StationWagonImg,
SuvImg,
OtherImg
} from "../../../../assets/images";
import { Car } from "../../../../models/landing";
import { CarBodySelectOption } from "./types";
export const carBodySelectOptions: CarBodySelectOption[] = [
{
value: Car.BodyStyle.SEDAN,
labelTKey: 'sedan',
img: SedanImg
},
{
value: Car.BodyStyle.HATCHBACK,
labelTKey: 'hatchback',
img: HatchbackImg
},
{
value: Car.BodyStyle.CROSSOVER,
labelTKey: 'crossover',
img: CrossoverImg
},
{
value: Car.BodyStyle.SUV,
labelTKey: 'suv',
img: SuvImg
},
{
value: Car.BodyStyle.STATION_WAGON,
labelTKey: 'station-wagon',
img: StationWagonImg
},
{
value: Car.BodyStyle.COUPE,
labelTKey: 'coupe',
img: CoupeImg
},
{
value: Car.BodyStyle.MINIVAN,
labelTKey: 'minivan',
img: MinivanImg
},
{
value: Car.BodyStyle.PICKUP,
labelTKey: 'pickup',
img: PickupImg
},
{
value: Car.BodyStyle.LIFTBACK,
labelTKey: 'liftback',
img: LiftbackImg
},
{
value: Car.BodyStyle.SPORTS_CAR,
labelTKey: 'sports-car',
img: SportsCarImg
},
{
value: Car.BodyStyle.OTHER,
labelTKey: 'other',
img: OtherImg
},
];
export const getInputValue = (value: Car.BodyStyle, t: TFunction<"~", "dry-wash.order-create.car-body-select">): InputProps['value'] => {
const { labelTKey } = carBodySelectOptions.find((option) => value === option.value) ?? {};
if (labelTKey) {
return t(`options.${labelTKey}`);
}
return;
};

View File

@ -0,0 +1 @@
export { CarBodySelect } from './car-body-select';

View File

@ -0,0 +1,39 @@
import React from 'react';
import {
ImageProps,
StackProps,
Image,
useRadio,
chakra,
Box,
UseRadioProps,
Flex,
} from '@chakra-ui/react';
import { getPropsByState } from './helper';
type CarBodyOptionProps = {
image: ImageProps['src'];
label: StackProps['children'];
} & UseRadioProps;
export const CarBodyOption = ({
image,
label,
...radioProps
}: CarBodyOptionProps) => {
const { state, getInputProps, getRadioProps, htmlProps, getLabelProps } =
useRadio(radioProps);
return (
<chakra.label {...htmlProps} cursor='pointer'>
<input {...getInputProps({})} hidden />
<Box {...getRadioProps()} p={2} {...getPropsByState(state)}>
<Flex direction='column' alignItems='center' {...getLabelProps()}>
<Image src={image} rounded={4} />
{label}
</Flex>
</Box>
</chakra.label>
);
};

View File

@ -0,0 +1,19 @@
import {
BoxProps,
} from '@chakra-ui/react';
import { RadioState } from '@chakra-ui/react/dist/types/radio/use-radio';
export const getPropsByState = ({ isChecked }: RadioState): BoxProps => {
if (isChecked) {
return {
bgColor: 'primary.200',
_hover: { bgColor: 'primary.100' },
_active: { bgColor: 'primary.300' },
};
}
return {
_hover: { bgColor: 'primary.50' },
_active: { bgColor: 'primary.100' },
};
};

View File

@ -0,0 +1 @@
export { CarBodyOption } from './car-body-option';

View File

@ -0,0 +1,24 @@
import { InputProps } from "@chakra-ui/react";
import { Car } from "../../../../models/landing";
export type CarBodySelectOption = {
value: Car.BodyStyle;
labelTKey:
'sedan' |
'hatchback' |
'crossover' |
'suv' |
'station-wagon' |
'coupe' |
'minivan' |
'pickup' |
'liftback' |
'sports-car' |
'other';
img?: string;
};
export type CarBodySelectProps = {
value?: string;
} & Pick<InputProps, 'onChange'>;

View File

@ -0,0 +1,23 @@
import React, { forwardRef, useId } from 'react';
import { Input, InputProps } from '@chakra-ui/react';
import { CAR_COLORS } from './helper';
export const CarColorInput = forwardRef<HTMLInputElement, InputProps>(
function CarColorInput(props, ref) {
const listId = useId();
return (
<>
<Input ref={ref} list={listId} {...props} />
<datalist id={listId}>
{CAR_COLORS.map(({ code, name }) => (
<option key={code} label={name} value={code}>{name}</option>
))}
</datalist>
</>
);
},
);
// todo: add option color visual indication

View File

@ -0,0 +1,34 @@
export const CAR_COLORS: Record<'name' | 'code', string>[] = [
{
name: 'white',
code: '#ffffff'
},
{
name: 'black',
code: '#000000'
},
{
name: 'silver',
code: '#c0c0c0'
},
{
name: 'gray',
code: '#808080'
},
{
name: 'beige-brown',
code: '#796745'
},
{
name: 'red',
code: '#b90000'
},
{
name: 'blue',
code: '#003B62'
},
{
name: 'green',
code: '#078d51'
},
];

View File

@ -0,0 +1 @@
export { CarColorInput } from './car-color-input';

View File

@ -0,0 +1,22 @@
import React, { forwardRef } from 'react';
import { Input, InputProps } from '@chakra-ui/react';
import { handleInputChange } from './helper';
export const CarNumberInput = forwardRef<HTMLInputElement, InputProps>(
function CarNumberInput({ onChange, ...props }, ref) {
return (
<Input
{...props}
ref={ref}
onChange={(e) => {
const formattedValue = handleInputChange(e.target.value);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
onChange?.(formattedValue);
}}
maxLength={12}
/>
);
},
);

View File

@ -0,0 +1,33 @@
const VALID_LETTER = 'а|в|е|к|м|н|о|р|с|т|у|х';
const invalidCharsRe = new RegExp(`[^(${VALID_LETTER})0-9]`, 'gi');
const cleanValue = (value: string) => value.replace(invalidCharsRe, '');
const validCarNumberInputRe = new RegExp(`^([${VALID_LETTER}]{1}|$)((?:[0-9]|$)(?:[0-9]|$)(?:[0-9]|$))([${VALID_LETTER}]{1,2}|$)((?:[0-9]|$)(?:[0-9]|$)(?:[0-9]|$))$`, 'gi');
const isValidInput = (cleanedValue: string) => validCarNumberInputRe.test(cleanedValue);
const formatAsCarNumber = (cleanedValue: string) => {
return cleanedValue.replace(validCarNumberInputRe, (_, p1, p2, p3, p4) => [p1, p2, p3, p4].join(' ')).toUpperCase();
};
const getWithoutLastChar = (value: string) => value.substring(0, value.length - 1);
export const handleInputChange = (value: string | undefined | null) => {
const cleanedValue = cleanValue(value);
if (!cleanedValue) {
return '';
}
if (isValidInput(cleanedValue)) {
return formatAsCarNumber(cleanedValue).trim();
}
return getWithoutLastChar(value).trim();
};
const validCarNumberRe = new RegExp(`^[${VALID_LETTER}][0-9]{3}[${VALID_LETTER}]{2}[0-9]{2,3}$`, 'i');
export const isValidCarNumber = (value: string) => {
const cleanedValue = cleanValue(value);
return validCarNumberRe.test(cleanedValue);
};

View File

@ -0,0 +1,2 @@
export { CarNumberInput } from './car-number-input';
export { isValidCarNumber } from './helper';

Some files were not shown because too many files have changed in this diff Show More