61 Commits

Author SHA1 Message Date
dc10fa6be4 chore: release v0.2.2
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 1m26s
2025-09-20 14:41:32 +05:00
39e84723b1 refactor: remove table captions from all tables 🔧 2025-09-20 14:40:55 +05:00
19043aa692 fix: set 100 MVR for default wallet topup amount 🔧 2025-09-20 14:25:48 +05:00
f2a17d522b fix: hide account information once the payment is verified/expired/cancelled 🔧 2025-09-20 14:17:48 +05:00
43b8e22196 fix: remove pagination if there is only 1 page 🔧 2025-09-20 14:09:12 +05:00
c041c2e7d7 chore: release v0.2.1
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 14m23s
2025-09-20 13:39:13 +05:00
e5298aa323 fix: add release script 🔧 2025-09-20 13:37:35 +05:00
741358e32f fix: add missing release script
Some checks are pending
Build and Push Docker Images / Build and Push Docker Images (push) Has started running
2025-09-20 13:31:15 +05:00
1399527ddb fix: added missing release script to package.json 🔧 2025-09-20 13:26:41 +05:00
950ac676f0 Show no agreements if there is no agreement in agreement page 🔨
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 8m19s
2025-09-20 11:16:06 +05:00
b32b61afe8 rename walet page to transaction history 🔨 2025-09-20 11:15:44 +05:00
1e2329e7f1 refactor(sidebar): re arrange sidebar links 🔨 2025-09-20 11:14:38 +05:00
6c5f848856 remove welcome banner after 4 seconds with animation 2025-09-20 11:14:11 +05:00
c67b8ade10 fix: verification response handling 🐛
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 12m34s
2025-09-15 22:00:57 +05:00
17aa65a686 feat(user-update-form): display field errors in user update form
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 7m52s
2025-07-28 10:03:17 +05:00
21938657ca fix(wallet-transactions): improve badge text color for transaction types (mobile)
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 8m44s
2025-07-27 22:23:37 +05:00
a717c3d242 feat(admin-topup): add description field to topup request payload
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 8m2s
fix(wallet-transactions-table): update badge colors for better visibility
2025-07-27 22:13:49 +05:00
171b1d4d7c feat(agreements): implement agreement fetching and display with error handling
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 13m34s
feat(agreement-card): create AgreementCard component for displaying user agreements
fix(devices-table): update no devices message for clarity based on parental control
fix(payments-table): update no payments message for consistency
fix(topups-table): update no topups message for consistency
feat(user): add agreement field to User interface
2025-07-27 19:43:01 +05:00
dcf58c4349 fix(sidebar): adjust transition durations for consistency 🐛 2025-07-27 19:41:50 +05:00
3da668a94a feat(admin-topup): add description field to topup form and validation for amount
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 8m56s
fix(verify-registration-otp): improve styling and structure of OTP verification form
2025-07-27 16:01:15 +05:00
76a4e3d66e fix(devices): fix payment amount generation by passing actual values 🐛
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 7m43s
2025-07-27 15:22:15 +05:00
db261ae0bd fix(signup-form): update header and welcome message for clarity
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 8m55s
2025-07-27 14:22:32 +05:00
644e4f730f feat(user): add admin topup functionality in user details page
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 6m16s
2025-07-27 12:37:38 +05:00
3943c0d6c8 fix(wallet): update styling for total debit/credit display in wallet transactions table
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 6m56s
2025-07-26 18:56:28 +05:00
e9af120c7b feat(wallet): update transaction type from CREDIT to TOPUP and display total debit/credit amounts
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 5m27s
2025-07-26 00:17:22 +05:00
e0b76bb865 feat(wallet): implement wallet transactions table and filtering options
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 7m19s
2025-07-25 16:01:20 +05:00
9b2f2c1528 add admin checks for admin pages and run biome formating 🔨
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 11m8s
2025-07-25 13:31:12 +05:00
aedf7cdf7d feat(topups): add support for fetching all topups in getTopups function
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 7m2s
2025-07-25 11:06:55 +05:00
a2425009ee update readme
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 5m14s
2025-07-25 10:59:54 +05:00
eadd790fe1 feat(verify-user): add link to view user agreement and improve layout for user information
Some checks are pending
Build and Push Docker Images / Build and Push Docker Images (push) Has started running
2025-07-25 10:58:39 +05:00
c2578f1c8f feat(user-agreement): implement user agreement upload functionality and update related components 2025-07-25 10:39:15 +05:00
5fda723653 chore(package-lock): update brace-expansion to version 1.1.12 and form-data to version 4.0.4
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 9m45s
2025-07-24 23:03:58 +05:00
1f6fe7db38 feat(user-topups): add user topups page with dynamic filtering and admin table integration
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Has been cancelled
feat(admin-devices-table): update admin check to use is_admin and clean up device display logic 
feat(admin-topup-table): create admin topups table with pagination and detail view links 
fix(user-payments-table): correct user data access and display payment amount with currency 
feat(app-sidebar): add link for user topups in the admin sidebar 
fix(backend-types): enhance Payment interface to include user details for better data handling 
2025-07-24 23:01:41 +05:00
d7b8e4ec64 chore(user-payments-table): remove unused imports 🔧
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 44s
2025-07-23 23:13:04 +05:00
c34285c66d feat(admin): add admin payment tables + filters
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Has been cancelled
2025-07-23 23:12:10 +05:00
dc3b5f9bf9 feat(user-verification): implement user verification functionality and update dialog UI
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 6m54s
2025-07-16 02:03:03 +05:00
7a02cb2415 chore(user-update-form): remove unused import for cleaner code 🔧
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 5m40s
2025-07-14 21:40:22 +05:00
9926de99d6 feat(filters): replace existing filters with dynamic filters for improved user and device management 2025-07-14 21:39:31 +05:00
8bcf4812be chore(README): update task list to reflect completed features in parental control and admin controls 🔧 2025-07-14 21:39:17 +05:00
8fac07bb60 feat(user): implement user update functionality and enhance verification page UI 2025-07-14 21:38:29 +05:00
780239dbbe feat(admin): add usertable and update next config
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 5m25s
2025-07-13 23:09:02 +05:00
Abdulla Aidhaan
d198a1bdcf Merge pull request #17 from i701/feat/user-verification-handling
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 8m20s
fix getProfile import in PaymentId 🐛
2025-07-13 22:58:17 +05:00
361a01d86c fix getProfile import in PaymentId 🐛 2025-07-13 22:57:20 +05:00
Abdulla Aidhaan
74b19ed5b9 Merge pull request #16 from i701/feat/user-verification-handling
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 1m43s
feat/user verification handling
2025-07-13 22:53:16 +05:00
255c03ae6a feat: implement user verification and rejection functionality with improved error handling 2025-07-13 22:51:46 +05:00
5809e26593 refactor: simplify FloatingInput and Textarea components by removing unnecessary forwardRef usage 🔨 2025-07-13 22:49:55 +05:00
b90a4afc73 fix: enhance error handling in handleApiResponse function for better clarity 🐛 2025-07-13 22:41:26 +05:00
Abdulla Aidhaan
644db022a1 Merge pull request #15 from i701/feat/user-profile
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 2m30s
feat/user profile 
2025-07-11 14:50:06 +05:00
9c1d1c5d2b feat: add floating label input component for enhanced form usability 2025-07-11 14:41:47 +05:00
78673e050c refactor: remove unused user verification function and clean up user type definitions 🔨 2025-07-11 14:41:30 +05:00
a20c939c91 feat: implement user profile page and integrate profile fetching logic in AccountPopover and ApplicationLayout components 2025-07-11 14:41:06 +05:00
d1fdcc873a fix: simplify condition for displaying pending payment link in ClickableRow component 🐛
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 18m1s
2025-07-11 09:59:29 +05:00
71f48b1270 fix: remove unnecessary JSON display of payment creation date and improve conditional styling in MobilePaymentDetails component 🐛
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 7m52s
2025-07-09 21:44:54 +05:00
27a0b5d4b3 bug: fix payment status display and force timezones in creation date in PaymentsTable and DevicesToPay components 🐛
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 7m27s
2025-07-09 21:14:49 +05:00
783d4b360d fix: pagination in PaymentsTable and TopupsTable components 🐛
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 7m35s
2025-07-08 22:40:06 +05:00
fa12cd74d7 refactor: use single seperate AccountInfomation component and integrate it into DevicesToPay and TopupToPay components 🔨
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 5m31s
2025-07-08 21:09:17 +05:00
8f9b4ba2e5 refactor: remove unused imports in TopupToPay component 🔨
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 12m18s
2025-07-08 19:54:48 +05:00
a61231cf6b refactor: improve state management and layout in Topup components 🔨
Some checks are pending
Build and Push Docker Images / Build and Push Docker Images (push) Has started running
2025-07-08 19:54:07 +05:00
9f9f2e4e91 feat: add disabled prop to DevicesToPay component and update button state management
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 5m36s
2025-07-06 23:19:12 +05:00
Abdulla Aidhaan
3ac57a9c07 Merge pull request #14 from i701/feat/device-payments
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 6m26s
refactor: implement device payment verification with useActionState hook 🔨
2025-07-06 23:10:04 +05:00
Abdulla Aidhaan
2232cdcf72 Merge pull request #13 from i701/feat/device-payments
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 5m22s
feat/device payments
2025-07-06 22:41:28 +05:00
148 changed files with 14004 additions and 8113 deletions

View File

@@ -1,9 +1,6 @@
{ {
"extends": [ "extends": ["next/core-web-vitals", "next/typescript"],
"next/core-web-vitals", "rules": {
"next/typescript" "@typescript-eslint/no-explicit-any": "error"
], }
"rules": {
"@typescript-eslint/no-explicit-any": "error"
}
} }

1
.husky/commit-msg Executable file
View File

@@ -0,0 +1 @@
npx commitlint --edit $1

1
.husky/pre-commit Executable file
View File

@@ -0,0 +1 @@
npm run lint

View File

@@ -1,3 +1,3 @@
{ {
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib"
} }

241
CHANGELOG.md Normal file
View File

@@ -0,0 +1,241 @@
# Changelog
## <small>0.2.2 (2025-09-20)</small>
* refactor: remove table captions from all tables 🔧 ([39e8472](https://github.com/i701/sarlink-portal/commit/39e8472))
* fix: hide account information once the payment is verified/expired/cancelled 🔧 ([f2a17d5](https://github.com/i701/sarlink-portal/commit/f2a17d5))
* fix: remove pagination if there is only 1 page 🔧 ([43b8e22](https://github.com/i701/sarlink-portal/commit/43b8e22))
* fix: set 100 MVR for default wallet topup amount 🔧 ([19043aa](https://github.com/i701/sarlink-portal/commit/19043aa))
## <small>0.2.1 (2025-09-20)</small>
* fix: add release script 🔧 ([e5298aa](https://github.com/i701/sarlink-portal/commit/e5298aa))
## 0.2.0 (2025-09-20)
* fix: add error message display for general errors in OTP verification form ([25404b6](https://github.com/i701/sarlink-portal/commit/25404b6))
* fix: add logging for user data and adjust verification check in signin function ([4127035](https://github.com/i701/sarlink-portal/commit/4127035))
* fix: add missing release script ([741358e](https://github.com/i701/sarlink-portal/commit/741358e))
* fix: added missing release script to package.json 🔧 ([1399527](https://github.com/i701/sarlink-portal/commit/1399527))
* fix: correct conditional expression for progress indicator color 🐛 ([0c7f34f](https://github.com/i701/sarlink-portal/commit/0c7f34f))
* fix: correct font variable usage and improve type handling in dynamic filter ([3f92a00](https://github.com/i701/sarlink-portal/commit/3f92a00))
* fix: correct OTP type in VerifyRegistrationOTP function ([de1e76f](https://github.com/i701/sarlink-portal/commit/de1e76f))
* fix: correct user verification logic in VerifyRegistrationOTP function ([9c67386](https://github.com/i701/sarlink-portal/commit/9c67386))
* fix: enhance error handling in handleApiResponse function for better clarity 🐛 ([b90a4af](https://github.com/i701/sarlink-portal/commit/b90a4af))
* fix: enhance verification feedback in VerifyRegistrationOTP function and update UI messages ([3703b3e](https://github.com/i701/sarlink-portal/commit/3703b3e))
* fix: ensure device list is refreshed after adding a new device ([ac11fee](https://github.com/i701/sarlink-portal/commit/ac11fee))
* fix: openssl ([b9cfc55](https://github.com/i701/sarlink-portal/commit/b9cfc55))
* fix: pagination in PaymentsTable and TopupsTable components 🐛 ([783d4b3](https://github.com/i701/sarlink-portal/commit/783d4b3))
* fix: remove unnecessary JSON display of payment creation date and improve conditional styling in Mob ([71f48b1](https://github.com/i701/sarlink-portal/commit/71f48b1))
* fix: remove unused authentication imports in PaymentPage component ([886b0b1](https://github.com/i701/sarlink-portal/commit/886b0b1))
* fix: remove unused components and fix build 🐛 ([383ffdd](https://github.com/i701/sarlink-portal/commit/383ffdd))
* fix: remove unused imports from authentication pages for cleaner code ([1ae529c](https://github.com/i701/sarlink-portal/commit/1ae529c))
* fix: remove unused session retrieval in PaymentPage component ([3d12786](https://github.com/i701/sarlink-portal/commit/3d12786))
* fix: remove unused type prop from BlockDeviceDialog component ([de1e842](https://github.com/i701/sarlink-portal/commit/de1e842))
* fix: reset form after successfully adding a device in AddDeviceDialog ([067acef](https://github.com/i701/sarlink-portal/commit/067acef))
* fix: revert wut ([ff70624](https://github.com/i701/sarlink-portal/commit/ff70624))
* fix: simplify condition for displaying pending payment link in ClickableRow component 🐛 ([d1fdcc8](https://github.com/i701/sarlink-portal/commit/d1fdcc8))
* fix: streamline redirect logic for verified OTP response ([322f526](https://github.com/i701/sarlink-portal/commit/322f526))
* fix: update error messages and statuses in VerifyRegistrationOTP function for clarity ([e0e3de0](https://github.com/i701/sarlink-portal/commit/e0e3de0))
* fix: update OTP verification status message in VerifyRegistrationOTP function ([6a078d9](https://github.com/i701/sarlink-portal/commit/6a078d9))
* fix: update signin function to handle user verification response correctly ([00705bc](https://github.com/i701/sarlink-portal/commit/00705bc))
* fix: update verification error message in VerifyRegistrationOTP function ([12f9811](https://github.com/i701/sarlink-portal/commit/12f9811))
* fix: update welcome message in ApplicationLayout and simplify payment verification logic in DevicesT ([a093ab1](https://github.com/i701/sarlink-portal/commit/a093ab1))
* fix: verification response handling 🐛 ([c67b8ad](https://github.com/i701/sarlink-portal/commit/c67b8ad))
* fix(devices): fix payment amount generation by passing actual values 🐛 ([76a4e3d](https://github.com/i701/sarlink-portal/commit/76a4e3d))
* fix(sidebar): adjust transition durations for consistency 🐛 ([dcf58c4](https://github.com/i701/sarlink-portal/commit/dcf58c4))
* fix(signup-form): update header and welcome message for clarity ([db261ae](https://github.com/i701/sarlink-portal/commit/db261ae))
* fix(wallet-transactions): improve badge text color for transaction types (mobile) ([2193865](https://github.com/i701/sarlink-portal/commit/2193865))
* fix(wallet): update styling for total debit/credit display in wallet transactions table ([3943c0d](https://github.com/i701/sarlink-portal/commit/3943c0d))
* add admin checks for admin pages and run biome formating 🔨 ([9b2f2c1](https://github.com/i701/sarlink-portal/commit/9b2f2c1))
* Add Agreements page, enhance Devices and Users components with sorting and filtering options, and im ([9021f01](https://github.com/i701/sarlink-portal/commit/9021f01))
* Add AlertDialog and Badge components for user verification ([8e6f802](https://github.com/i701/sarlink-portal/commit/8e6f802))
* add auto deploy ([02e1bcb](https://github.com/i701/sarlink-portal/commit/02e1bcb))
* add auto deploy ([5e1364e](https://github.com/i701/sarlink-portal/commit/5e1364e))
* Add filter, pagination, search, and user table components ([1b43c85](https://github.com/i701/sarlink-portal/commit/1b43c85))
* add mirror to gitea ([d60dd3a](https://github.com/i701/sarlink-portal/commit/d60dd3a))
* add mirror to gitea ([9e9fab5](https://github.com/i701/sarlink-portal/commit/9e9fab5))
* add non stretched favicon ([e9c71c1](https://github.com/i701/sarlink-portal/commit/e9c71c1))
* Add payment processing and device management features ([e815da4](https://github.com/i701/sarlink-portal/commit/e815da4))
* add seed script and use postgres for db ([a0d85b1](https://github.com/i701/sarlink-portal/commit/a0d85b1))
* Add title background styling to globals.css ([762939b](https://github.com/i701/sarlink-portal/commit/762939b))
* Add user rejection and device addition functionalities ([2cbf9fb](https://github.com/i701/sarlink-portal/commit/2cbf9fb))
* attempt 2 building with docker ([a5b18e8](https://github.com/i701/sarlink-portal/commit/a5b18e8))
* attempt 4 docker ([8e068f1](https://github.com/i701/sarlink-portal/commit/8e068f1))
* attempt no 3 docker ([994956a](https://github.com/i701/sarlink-portal/commit/994956a))
* attempt to build docker again ([d72f9ae](https://github.com/i701/sarlink-portal/commit/d72f9ae))
* auto build ([fe3161e](https://github.com/i701/sarlink-portal/commit/fe3161e))
* bruh ([c79f06c](https://github.com/i701/sarlink-portal/commit/c79f06c))
* bun can build ([d5c9253](https://github.com/i701/sarlink-portal/commit/d5c9253))
* clean up dangling images after restart ([2597ac7](https://github.com/i701/sarlink-portal/commit/2597ac7))
* clean up dangling images after restart ([f9ed95a](https://github.com/i701/sarlink-portal/commit/f9ed95a))
* cleaner output ([4776952](https://github.com/i701/sarlink-portal/commit/4776952))
* cleaner output ([480f649](https://github.com/i701/sarlink-portal/commit/480f649))
* deploy in 1 ssh session ([135edf8](https://github.com/i701/sarlink-portal/commit/135edf8))
* deploy in 1 ssh session ([5bc8366](https://github.com/i701/sarlink-portal/commit/5bc8366))
* docker build works now ([5fb6f52](https://github.com/i701/sarlink-portal/commit/5fb6f52))
* down kohffa up kuraanee.. also there is 1 ui ([e841932](https://github.com/i701/sarlink-portal/commit/e841932))
* Enhance dashboard functionality with new payment and device management features ([c6f4571](https://github.com/i701/sarlink-portal/commit/c6f4571))
* Enhance device management and user experience features ([745f8d8](https://github.com/i701/sarlink-portal/commit/745f8d8))
* Enhance payment processing and device management features ([1a195d2](https://github.com/i701/sarlink-portal/commit/1a195d2))
* Enhance payment processing and device management features ([75ad431](https://github.com/i701/sarlink-portal/commit/75ad431))
* Enhance payment processing and user interaction features ([36f22c0](https://github.com/i701/sarlink-portal/commit/36f22c0))
* Enhance user management and payment processing features ([0a63e43](https://github.com/i701/sarlink-portal/commit/0a63e43))
* Enhance user verification and data validation features ([ff0eae6](https://github.com/i701/sarlink-portal/commit/ff0eae6))
* Enhance user verification and UI components ([2c67848](https://github.com/i701/sarlink-portal/commit/2c67848))
* example readme for prod deployment ([40b40ad](https://github.com/i701/sarlink-portal/commit/40b40ad))
* f all that just deploy ([1b724ca](https://github.com/i701/sarlink-portal/commit/1b724ca))
* final fix ([d14b0b3](https://github.com/i701/sarlink-portal/commit/d14b0b3))
* first commit ([7389de4](https://github.com/i701/sarlink-portal/commit/7389de4))
* fix docker build yml ([c0a71dd](https://github.com/i701/sarlink-portal/commit/c0a71dd))
* fix docker build yml ([b22a26a](https://github.com/i701/sarlink-portal/commit/b22a26a))
* fix getProfile import in PaymentId 🐛 ([361a01d](https://github.com/i701/sarlink-portal/commit/361a01d))
* fix:wut ([e2e5751](https://github.com/i701/sarlink-portal/commit/e2e5751))
* hamker magic ([ef32b74](https://github.com/i701/sarlink-portal/commit/ef32b74))
* Implement new features and enhance existing components for improved user experience ([bdf3729](https://github.com/i701/sarlink-portal/commit/bdf3729))
* Implement Omada device management and enhance payment processing ([e9d81c0](https://github.com/i701/sarlink-portal/commit/e9d81c0))
* Implement parental control features and enhance device management ([c06c4fe](https://github.com/i701/sarlink-portal/commit/c06c4fe))
* Initial commit from Create Next App ([92d9e90](https://github.com/i701/sarlink-portal/commit/92d9e90))
* installing openssl ([cd86a7a](https://github.com/i701/sarlink-portal/commit/cd86a7a))
* npx now ([bfba3cf](https://github.com/i701/sarlink-portal/commit/bfba3cf))
* optimize container build ([0e6f405](https://github.com/i701/sarlink-portal/commit/0e6f405))
* prepare for docker env ([fd4e147](https://github.com/i701/sarlink-portal/commit/fd4e147))
* prepare for docker env ([f9cac92](https://github.com/i701/sarlink-portal/commit/f9cac92))
* Refactor authentication actions and add user verification functionality ([3f68d83](https://github.com/i701/sarlink-portal/commit/3f68d83))
* Refactor authentication and dashboard components ([0322bee](https://github.com/i701/sarlink-portal/commit/0322bee))
* Refactor authentication components and enhance user session handling ([3f8bb4e](https://github.com/i701/sarlink-portal/commit/3f8bb4e))
* Refactor authentication middleware to use native fetch, update dependencies, and enhance error handl ([8ffabb1](https://github.com/i701/sarlink-portal/commit/8ffabb1))
* Refactor dashboard components and update global styles ([b91f34b](https://github.com/i701/sarlink-portal/commit/b91f34b))
* Refactor Docker setup and add Prisma Studio service ([1d6f4fa](https://github.com/i701/sarlink-portal/commit/1d6f4fa))
* Refactor Omada actions and enhance payment processing ([586c0e7](https://github.com/i701/sarlink-portal/commit/586c0e7))
* Refactor payment verification and add MAC address guide ([07349cd](https://github.com/i701/sarlink-portal/commit/07349cd))
* Refactor SMS notification handling to use environment variables ([5c167e4](https://github.com/i701/sarlink-portal/commit/5c167e4))
* Refactor UI components for improved consistency and functionality ([0f17800](https://github.com/i701/sarlink-portal/commit/0f17800))
* Refactor user actions and authentication components ([490150f](https://github.com/i701/sarlink-portal/commit/490150f))
* Refactor user verification and data handling ([a3f0759](https://github.com/i701/sarlink-portal/commit/a3f0759))
* registration verification WIP ([470e845](https://github.com/i701/sarlink-portal/commit/470e845))
* remove prisma stuff ([c799362](https://github.com/i701/sarlink-portal/commit/c799362))
* remove prisma stuff ([91563cb](https://github.com/i701/sarlink-portal/commit/91563cb))
* remove test api route ([0943898](https://github.com/i701/sarlink-portal/commit/0943898))
* remove unused imports ([fcf4f37](https://github.com/i701/sarlink-portal/commit/fcf4f37))
* remove welcome banner after 4 seconds with animation ✨ ([6c5f848](https://github.com/i701/sarlink-portal/commit/6c5f848))
* rename walet page to transaction history 🔨 ([b32b61a](https://github.com/i701/sarlink-portal/commit/b32b61a))
* Show no agreements if there is no agreement in agreement page 🔨 ([950ac67](https://github.com/i701/sarlink-portal/commit/950ac67))
* swap trusted origins for .env variables ([7acd118](https://github.com/i701/sarlink-portal/commit/7acd118))
* switch to node ([7bfdc10](https://github.com/i701/sarlink-portal/commit/7bfdc10))
* temporary fix for build and remove previous auth related code ([e8296ae](https://github.com/i701/sarlink-portal/commit/e8296ae))
* TEMPORARY FIX TO TEST BUILD ([b932fcf](https://github.com/i701/sarlink-portal/commit/b932fcf))
* Update dependencies and refactor calendar component ([df820ff](https://github.com/i701/sarlink-portal/commit/df820ff))
* update logo to omega ([cd2cff7](https://github.com/i701/sarlink-portal/commit/cd2cff7))
* Update package.json for turbopack support, enhance AppSidebar with new icons, and add Textarea and a ([2b0bd51](https://github.com/i701/sarlink-portal/commit/2b0bd51))
* update packages ([8673b87](https://github.com/i701/sarlink-portal/commit/8673b87))
* update readme ([a242500](https://github.com/i701/sarlink-portal/commit/a242500))
* update README todos ✏️ ([40bf841](https://github.com/i701/sarlink-portal/commit/40bf841))
* Update user schema and forms to include address and consent fields ([4e78ff2](https://github.com/i701/sarlink-portal/commit/4e78ff2))
* wip ([7fadcd5](https://github.com/i701/sarlink-portal/commit/7fadcd5))
* WIP feat(admin-devices): enhance device management from admin with dynamic filters and improved bloc ([01b064a](https://github.com/i701/sarlink-portal/commit/01b064a))
* wth what was yarn doing here 😭 ([967a1d2](https://github.com/i701/sarlink-portal/commit/967a1d2))
* refactor: add topup expiry filter to the Topups component and simplify expired badge display in Topu ([5de4bb9](https://github.com/i701/sarlink-portal/commit/5de4bb9))
* refactor: add tryCatch utility for error handling, update device-related components and types, and c ([aa18484](https://github.com/i701/sarlink-portal/commit/aa18484))
* refactor: enhance authentication and signup flow with new providers, update middleware matcher, and ([99c5fef](https://github.com/i701/sarlink-portal/commit/99c5fef))
* refactor: enhance error handling and add pagination to device queries ([aff9d26](https://github.com/i701/sarlink-portal/commit/aff9d26))
* refactor: enhance parental control features and improve device blocking logic 🔨 ([c90b003](https://github.com/i701/sarlink-portal/commit/c90b003))
* refactor: enhance topup payment verification with improved state management and error handling 🔨 ([013befa](https://github.com/i701/sarlink-portal/commit/013befa))
* refactor: enhance topup status display and improve conditional rendering in TopupsTable component 🔨 ([8c94c92](https://github.com/i701/sarlink-portal/commit/8c94c92))
* refactor: implement device payment verification with useActionState hook 🔨 ([1a11f1a](https://github.com/i701/sarlink-portal/commit/1a11f1a))
* refactor: implement session checking utility, enhance device queries with session validation, and im ([daab793](https://github.com/i701/sarlink-portal/commit/daab793))
* refactor: improve state management and layout in Topup components 🔨 ([a61231c](https://github.com/i701/sarlink-portal/commit/a61231c))
* refactor: migrate authentication and signup flow to use external API and improve type safety ([0fd269d](https://github.com/i701/sarlink-portal/commit/0fd269d))
* refactor: remove layout error component from dashboard ([bb5eff8](https://github.com/i701/sarlink-portal/commit/bb5eff8))
* refactor: remove unused import and improve conditional rendering in block device dialog 🔨 ([8051b00](https://github.com/i701/sarlink-portal/commit/8051b00))
* refactor: remove unused import from device-card and devices-table components ([4e16c0a](https://github.com/i701/sarlink-portal/commit/4e16c0a))
* refactor: remove unused import of checkIdOrPhone in OTP verification page ([acb0ead](https://github.com/i701/sarlink-portal/commit/acb0ead))
* refactor: remove unused imports in TopupToPay component 🔨 ([8f9b4ba](https://github.com/i701/sarlink-portal/commit/8f9b4ba))
* refactor: remove unused imports in user payments page component 🔨 ([905b4fa](https://github.com/i701/sarlink-portal/commit/905b4fa))
* refactor: remove unused user verification function and clean up user type definitions 🔨 ([78673e0](https://github.com/i701/sarlink-portal/commit/78673e0))
* refactor: reorder imports and improve variable naming in CancelPaymentButton component 🔨 ([837cc35](https://github.com/i701/sarlink-portal/commit/837cc35))
* refactor: reorganize imports and enhance admin category filtering in AppSidebar 🔨 ([1549b20](https://github.com/i701/sarlink-portal/commit/1549b20))
* refactor: reorganize imports and improve session handling in various components 🔨 ([a6d844e](https://github.com/i701/sarlink-portal/commit/a6d844e))
* refactor: replace custom authentication middleware with NextAuth, remove unused authentication pages ([32bb01b](https://github.com/i701/sarlink-portal/commit/32bb01b))
* refactor: simplify FloatingInput and Textarea components by removing unnecessary forwardRef usage 🔨 ([5809e26](https://github.com/i701/sarlink-portal/commit/5809e26))
* refactor: streamline authentication flow by removing unused code, replacing custom auth utilities wi ([ef9f032](https://github.com/i701/sarlink-portal/commit/ef9f032))
* refactor: streamline package.json and tailwind.config.ts ([9e0d2d2](https://github.com/i701/sarlink-portal/commit/9e0d2d2))
* refactor: update authentication flow to use NextAuth, replace better-auth with axios for API calls, ([020d74c](https://github.com/i701/sarlink-portal/commit/020d74c))
* refactor: update authentication flow to use PIN instead of email/password, enhance OTP verification ([dbdc1df](https://github.com/i701/sarlink-portal/commit/dbdc1df))
* refactor: update axios client import, enhance device and payment handling, and add cancel payment bu ([7e49bf1](https://github.com/i701/sarlink-portal/commit/7e49bf1))
* refactor: update cancelPayment function to use PATCH method and new endpoint 🔨 ([e984705](https://github.com/i701/sarlink-portal/commit/e984705))
* refactor: update payment types and user interface, enhance error handling, and adjust API base URL ([9e2a2f4](https://github.com/i701/sarlink-portal/commit/9e2a2f4))
* refactor: update progress component styles and add radix-ui/react-progress dependency 🔨 ([cd1dba0](https://github.com/i701/sarlink-portal/commit/cd1dba0))
* refactor: update topup status handling in DynamicFilter and TopupsTable components ✨ ([378fb40](https://github.com/i701/sarlink-portal/commit/378fb40))
* refactor: use single seperate AccountInfomation component and integrate it into DevicesToPay and Top ([fa12cd7](https://github.com/i701/sarlink-portal/commit/fa12cd7))
* refactor(sidebar): re arrange sidebar links 🔨 ([1e2329e](https://github.com/i701/sarlink-portal/commit/1e2329e))
* feat: add "Top Ups" option to sidebar with appropriate permissions and icon ✨ ([89a35a9](https://github.com/i701/sarlink-portal/commit/89a35a9))
* feat: add admin check for device and payment pages; update session type to include is_admin property ([6aea548](https://github.com/i701/sarlink-portal/commit/6aea548))
* feat: add age validation in signup and update payment verification logic ([0c093f1](https://github.com/i701/sarlink-portal/commit/0c093f1))
* feat: add custom headers configuration to next.js config for stream/suspense support ✨ ([3e3b5f1](https://github.com/i701/sarlink-portal/commit/3e3b5f1))
* feat: add custom headers configuration to next.js config for stream/suspense support ✨ ([683d857](https://github.com/i701/sarlink-portal/commit/683d857))
* feat: add Dashboard layout main Error component for error handling in dashboard ([7750c5a](https://github.com/i701/sarlink-portal/commit/7750c5a))
* feat: add disabled prop to DevicesToPay component and update button state management ✨ ([9f9f2e4](https://github.com/i701/sarlink-portal/commit/9f9f2e4))
* feat: add dual range slider component and integrate it into dynamic filter for device management ([0157ecc](https://github.com/i701/sarlink-portal/commit/0157ecc))
* feat: add example environment configuration file and update .gitignore to include .env files ([378c120](https://github.com/i701/sarlink-portal/commit/378c120))
* feat: add expiry label to ExpiryCountDown component ✨ ([4797ee8](https://github.com/i701/sarlink-portal/commit/4797ee8))
* feat: add expiry label to ExpiryCountDown component in TopupPage ✨ ([9e25da7](https://github.com/i701/sarlink-portal/commit/9e25da7))
* feat: add floating label input component for enhanced form usability ✨ ([9c1d1c5](https://github.com/i701/sarlink-portal/commit/9c1d1c5))
* feat: add getProfile function and integrate user profile retrieval in payment and layout components ([ba91d2c](https://github.com/i701/sarlink-portal/commit/ba91d2c))
* feat: add loading skeleton for devices table and improve payment processing logic ([0d578c9](https://github.com/i701/sarlink-portal/commit/0d578c9))
* feat: add loading state and full-page loader component; update payment page and application layout t ([bed426a](https://github.com/i701/sarlink-portal/commit/bed426a))
* feat: add payment method filter to payments page ✨ ([13f0e72](https://github.com/i701/sarlink-portal/commit/13f0e72))
* feat: add payment method filter to payments page ✨ ([da7d101](https://github.com/i701/sarlink-portal/commit/da7d101))
* feat: add radio filter for topup expiry ✨ ([d499353](https://github.com/i701/sarlink-portal/commit/d499353))
* feat: add radio filter for topup expiry ✨ ([154226b](https://github.com/i701/sarlink-portal/commit/154226b))
* feat: add topup management features including topup creation, cancellation, and countdown timer ✨ ([f3328f7](https://github.com/i701/sarlink-portal/commit/f3328f7))
* feat: add vendor information to device components and update related UI elements ([8438ceb](https://github.com/i701/sarlink-portal/commit/8438ceb))
* feat: enforce strict TypeScript rules and add biome configuration ([79ecb62](https://github.com/i701/sarlink-portal/commit/79ecb62))
* feat: enhance device cart with cancel feature and improve payments table filters ([2834f23](https://github.com/i701/sarlink-portal/commit/2834f23))
* feat: enhance dual-range-slider handling in DynamicFilter component ✨ ([f4c34d3](https://github.com/i701/sarlink-portal/commit/f4c34d3))
* feat: enhance error handling and improve API response management across components ([6365a70](https://github.com/i701/sarlink-portal/commit/6365a70))
* feat: enhance MAC address guide with contact button for assistance ([c705add](https://github.com/i701/sarlink-portal/commit/c705add))
* feat: enhance OTP verification flow with error handling and response checks ([0b2dcfc](https://github.com/i701/sarlink-portal/commit/0b2dcfc))
* feat: enhance payment and topup interfaces with status and expiration details ✨ ([df2b3da](https://github.com/i701/sarlink-portal/commit/df2b3da))
* feat: enhance payment retrieval with flexible query parameters and add dynamic filters to payments p ([9fe3c3b](https://github.com/i701/sarlink-portal/commit/9fe3c3b))
* feat: enhance payment retrieval with flexible query parameters and add dynamic filters to payments p ([fa0d088](https://github.com/i701/sarlink-portal/commit/fa0d088))
* feat: implement add device functionality with validation and error handling; refactor related compon ([d2b2812](https://github.com/i701/sarlink-portal/commit/d2b2812))
* feat: implement AuthLayout component and streamline OTP verification forms with enhanced messaging ([a4ffb1e](https://github.com/i701/sarlink-portal/commit/a4ffb1e))
* feat: implement checkTempIdOrPhone function and update OTP verification logic ([f3f5800](https://github.com/i701/sarlink-portal/commit/f3f5800))
* feat: implement topup functionality with create, get, and cancel operations ✨ ([ee461bb](https://github.com/i701/sarlink-portal/commit/ee461bb))
* feat: implement user profile page and integrate profile fetching logic in AccountPopover and Applica ([a20c939](https://github.com/i701/sarlink-portal/commit/a20c939))
* feat: implement user registration and OTP verification flow with backend integration ([0f9d110](https://github.com/i701/sarlink-portal/commit/0f9d110))
* feat: implement user verification and rejection functionality with improved error handling ✨ ([255c03a](https://github.com/i701/sarlink-portal/commit/255c03a))
* feat: integrate backend mobile login and enhance user verification flow ([2cb2059](https://github.com/i701/sarlink-portal/commit/2cb2059))
* feat: integrate checkTempIdOrPhone in signup logic to validate temporary phone numbers ([923887b](https://github.com/i701/sarlink-portal/commit/923887b))
* feat: update authentication layout and forms for improved user experience; add new dependencies and ([406733b](https://github.com/i701/sarlink-portal/commit/406733b))
* feat: update cancelTopup API call to use PATCH method and enhance success message with topup details ([b9d8e56](https://github.com/i701/sarlink-portal/commit/b9d8e56))
* feat: update signup and OTP verification forms to enhance error handling and state management ([1e023eb](https://github.com/i701/sarlink-portal/commit/1e023eb))
* feat: update Topup interface with status and paid_at fields; modify table display for topup details ([ef7121f](https://github.com/i701/sarlink-portal/commit/ef7121f))
* feat(admin-topup): add description field to topup form and validation for amount ([3da668a](https://github.com/i701/sarlink-portal/commit/3da668a))
* feat(admin-topup): add description field to topup request payload ([a717c3d](https://github.com/i701/sarlink-portal/commit/a717c3d))
* feat(admin): add admin payment tables + filters✨ ([c34285c](https://github.com/i701/sarlink-portal/commit/c34285c))
* feat(admin): add usertable and update next config ✨ ([780239d](https://github.com/i701/sarlink-portal/commit/780239d))
* feat(agreements): implement agreement fetching and display with error handling ([171b1d4](https://github.com/i701/sarlink-portal/commit/171b1d4))
* feat(devices): add proper filter handling and update shadcn 🔨 ([59adaaf](https://github.com/i701/sarlink-portal/commit/59adaaf))
* feat(filters): replace existing filters with dynamic filters for improved user and device management ([9926de9](https://github.com/i701/sarlink-portal/commit/9926de9))
* feat(portal-ui): enhance user and device information display in admin and user devices tables ✨ ([3cd3bba](https://github.com/i701/sarlink-portal/commit/3cd3bba))
* feat(topups): add support for fetching all topups in getTopups function ✨ ([aedf7cd](https://github.com/i701/sarlink-portal/commit/aedf7cd))
* feat(user-agreement): implement user agreement upload functionality and update related components ✨ ([c2578f1](https://github.com/i701/sarlink-portal/commit/c2578f1))
* feat(user-topups): add user topups page with dynamic filtering and admin table integration ✨ ([1f6fe7d](https://github.com/i701/sarlink-portal/commit/1f6fe7d))
* feat(user-update-form): display field errors in user update form ✨ ([17aa65a](https://github.com/i701/sarlink-portal/commit/17aa65a))
* feat(user-verification): implement user verification functionality and update dialog UI ✨ ([dc3b5f9](https://github.com/i701/sarlink-portal/commit/dc3b5f9))
* feat(user): add admin topup functionality in user details page ✨ ([644e4f7](https://github.com/i701/sarlink-portal/commit/644e4f7))
* feat(user): implement user update functionality and enhance verification page UI ✨ ([8fac07b](https://github.com/i701/sarlink-portal/commit/8fac07b))
* feat(verify-user): add link to view user agreement and improve layout for user information ([eadd790](https://github.com/i701/sarlink-portal/commit/eadd790))
* feat(wallet): implement wallet transactions table and filtering options ✨ ([e0b76bb](https://github.com/i701/sarlink-portal/commit/e0b76bb))
* feat(wallet): update transaction type from CREDIT to TOPUP and display total debit/credit amounts ✨ ([e9af120](https://github.com/i701/sarlink-portal/commit/e9af120))
* chore: update package.json ♻️ ([71fc914](https://github.com/i701/sarlink-portal/commit/71fc914))
* chore: update package.json to add millify dependency and remove unnecessary newline ([f7122cb](https://github.com/i701/sarlink-portal/commit/f7122cb))
* chore: update README with new todos 🔧 ([fb3ad13](https://github.com/i701/sarlink-portal/commit/fb3ad13))
* chore: upgrade to tailwind v4 and add a generic filter for dynamic filter handling ([11ac852](https://github.com/i701/sarlink-portal/commit/11ac852))
* chore(package-lock): update brace-expansion to version 1.1.12 and form-data to version 4.0.4 ([5fda723](https://github.com/i701/sarlink-portal/commit/5fda723))
* chore(README): update task list to reflect completed features in parental control and admin controls ([8bcf481](https://github.com/i701/sarlink-portal/commit/8bcf481))
* chore(user-payments-table): remove unused imports 🔧 ([d7b8e4e](https://github.com/i701/sarlink-portal/commit/d7b8e4e))
* chore(user-update-form): remove unused import for cleaner code 🔧 ([7a02cb2](https://github.com/i701/sarlink-portal/commit/7a02cb2))
* bug: fix payment status display and force timezones in creation date in PaymentsTable and DevicesTo ([27a0b5d](https://github.com/i701/sarlink-portal/commit/27a0b5d))

View File

@@ -10,36 +10,38 @@ This is a web portal for SAR Link customers.
- [x] Add cancel feature to selected devices floating button - [x] Add cancel feature to selected devices floating button
### Payments1 ### Payments
- [x] Show payments table - [x] Show payments table
- [x] Add all the filters for payment table (mobile responsive) - [x] Add all the filters for payment table (mobile responsive)
- [x] add slider range filter - [x] add slider range filter
- [ ] Fix bill formula linking for generated payments - [ ] Fix bill formula linking for generated payments
### Parental Control ### Parental Control
- [ ] Fix block device feature - [x] Fix block device feature
- [ ] Add all the filters for parental control table (mobile responsive) - [x] Add all the filters for parental control table (mobile responsive)
- [ ] Disable blocking if payment is pending or omit from the table if device payment is pending - [x] Disable blocking if payment is pending or omit from the table if device payment is pending
### Topups
- [ ] add textarea in topup dialog for admin. if no detail given generate on backend otherwise take the text from text area
### Agreements ### Agreements
- [ ] Implement file upload for admin side - [x] Implement file upload for admin side
- [ ] Add customer relavant documents in a grid view - [ ] Add customer relavant documents in a grid view
- [ ] Add filtering by date for agreements
## Admin Controls ## Admin Controls
### Users ### Users
- [ ] Show users table - [x] Show users table
- [ ] Add all relavant filters for users table - [x] handle verify api no response case
- [ ] Verify or reject users with a custom message - [x] Add all relavant filters for users table
- [x] Verify or reject users with a custom message
- [ ] Add functionality to send custom sms to users in user:id page - [ ] Add functionality to send custom sms to users in user:id page
### User Devices ### User Devices
- [ ] Block or unblock from admin with custom message - [x] Block or unblock from admin with custom message
- [ ] Blocking devices for client and admin using useActionState instead of client side button onClick handlers - [x] Blocking devices for client and admin using useActionState instead of client side button onClick handlers
- [ ] Show the devices table - [x] Show the devices table
- [ ] Add all relevant filters for user devices table - [x] Add all relevant filters for user devices table
### User Payments ### User Payments
- [ ] Show user payments table - [x] Show user payments table
- [ ] Add relevant filters for user payments table - [x] Add relevant filters for user payments table
- [ ] Add computation of total value for filtered results

View File

@@ -1,227 +1,229 @@
"use server"; "use server";
import { signUpFormSchema } from "@/lib/schemas";
import {
backendRegister,
checkIdOrPhone,
checkTempIdOrPhone,
} from "@/queries/authentication";
import { handleApiResponse, tryCatch } from "@/utils/tryCatch";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { z } from "zod"; import { z } from "zod";
import { signUpFormSchema } from "@/lib/schemas";
import {
backendRegister,
checkIdOrPhone,
checkTempIdOrPhone,
} from "@/queries/authentication";
import { handleApiResponse, tryCatch } from "@/utils/tryCatch";
const formSchema = z.object({ const formSchema = z.object({
phoneNumber: z phoneNumber: z
.string() .string()
.regex(/^[7|9][0-9]{2}-[0-9]{4}$/, "Please enter a valid phone number"), .regex(/^[7|9][0-9]{2}-[0-9]{4}$/, "Please enter a valid phone number"),
}); });
export type FilterUserResponse = { export type FilterUserResponse = {
ok: boolean; ok: boolean;
verified: boolean; verified: boolean;
}; };
export type FilterTempUserResponse = { export type FilterTempUserResponse = {
ok: boolean; ok: boolean;
otp_verified: boolean; otp_verified: boolean;
t_verified: boolean;
}; };
export async function signin(previousState: ActionState, formData: FormData) { export async function signin(_previousState: ActionState, formData: FormData) {
const phoneNumber = formData.get("phoneNumber") as string; const phoneNumber = formData.get("phoneNumber") as string;
const result = formSchema.safeParse({ phoneNumber }); const result = formSchema.safeParse({ phoneNumber });
console.log(phoneNumber); console.log(phoneNumber);
if (!result.success) { if (!result.success) {
return { return {
message: result.error.errors[0].message, // Get the error message from Zod message: result.error.errors[0].message, // Get the error message from Zod
status: "error", status: "error",
}; };
} }
if (!phoneNumber) { if (!phoneNumber) {
return { return {
message: "Please enter a phone number", message: "Please enter a phone number",
status: "error", status: "error",
}; };
} }
const FORMATTED_MOBILE_NUMBER: string = `${phoneNumber.split("-").join("")}`; const FORMATTED_MOBILE_NUMBER: string = `${phoneNumber.split("-").join("")}`;
console.log({ FORMATTED_MOBILE_NUMBER }); console.log({ FORMATTED_MOBILE_NUMBER });
const user = await fetch( const user = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/auth/users/filter/?mobile=${FORMATTED_MOBILE_NUMBER}`, `${process.env.SARLINK_API_BASE_URL}/api/auth/users/filter/?mobile=${FORMATTED_MOBILE_NUMBER}`,
{ {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
}, },
); );
const userData = (await user.json()) as FilterUserResponse; const userData = (await user.json()) as FilterUserResponse;
console.log({ userData }); console.log({ userData });
if (!userData.ok) { if (!userData.ok) {
redirect(`/auth/signup?phone_number=${phoneNumber}`); redirect(`/auth/signup?phone_number=${phoneNumber}`);
} }
if (!userData.verified) { if (!userData.verified) {
return { return {
message: message:
"Your account is on pending verification. Please wait for a response from admin or contact shihaam.", "Your account is on pending verification. Please wait for a response from admin or contact shihaam.",
status: "error", status: "error",
}; };
} }
const sendOTPResponse = await fetch( const sendOTPResponse = await fetch(
`${process.env.SARLINK_API_BASE_URL}/auth/mobile/`, `${process.env.SARLINK_API_BASE_URL}/auth/mobile/`,
{ {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
mobile: FORMATTED_MOBILE_NUMBER, mobile: FORMATTED_MOBILE_NUMBER,
}), }),
}, },
); );
const otpResponse = await sendOTPResponse.json(); const otpResponse = await sendOTPResponse.json();
console.log("otpResponse", otpResponse); console.log("otpResponse", otpResponse);
redirect(`/auth/verify-otp?phone_number=${FORMATTED_MOBILE_NUMBER}`); redirect(`/auth/verify-otp?phone_number=${FORMATTED_MOBILE_NUMBER}`);
} }
export type ActionState = { export type ActionState = {
status?: string; status?: string;
payload?: FormData; payload?: FormData;
errors?: z.typeToFlattenedError< errors?: z.typeToFlattenedError<
{ {
id_card: string; id_card: string;
phone_number: string; phone_number: string;
name: string; name: string;
atoll_id: string; atoll_id: string;
island_id: string; island_id: string;
address: string; address: string;
dob: Date; dob: Date;
terms: string; terms: string;
policy: string; policy: string;
accNo: string; accNo: string;
}, },
string string
>; >;
db_error?: string; db_error?: string;
error?: string; error?: string;
}; };
export async function signup(_actionState: ActionState, formData: FormData) { export async function signup(_actionState: ActionState, formData: FormData) {
const data = Object.fromEntries(formData.entries()); const data = Object.fromEntries(formData.entries());
const parsedData = signUpFormSchema.safeParse(data); const parsedData = signUpFormSchema.safeParse(data);
// get phone number from /signup?phone_number=999-1231 // get phone number from /signup?phone_number=999-1231
console.log("DATA ON SERVER SIDE", data); console.log("DATA ON SERVER SIDE", data);
if (!parsedData.success) { if (!parsedData.success) {
return { return {
message: "Invalid form data", message: "Invalid form data",
payload: formData, payload: formData,
errors: parsedData.error.flatten(), errors: parsedData.error.flatten(),
}; };
} }
const age = const age =
new Date().getFullYear() - new Date(parsedData.data.dob).getFullYear(); new Date().getFullYear() - new Date(parsedData.data.dob).getFullYear();
if (age < 18) { if (age < 18) {
return { return {
message: "You must be at least 18 years old to register.", message: "You must be at least 18 years old to register.",
payload: formData, payload: formData,
db_error: "dob", db_error: "dob",
}; };
} }
const idCardExists = await checkIdOrPhone({ const idCardExists = await checkIdOrPhone({
id_card: parsedData.data.id_card, id_card: parsedData.data.id_card,
}); });
if (idCardExists.ok) { if (idCardExists.ok) {
return { return {
message: "ID card already exists.", message: "ID card already exists.",
payload: formData, payload: formData,
db_error: "id_card", db_error: "id_card",
}; };
} }
const phoneNumberExists = await checkIdOrPhone({ const phoneNumberExists = await checkIdOrPhone({
phone_number: parsedData.data.phone_number, phone_number: parsedData.data.phone_number,
}); });
const tempPhoneNumberExists = await checkTempIdOrPhone({ const tempPhoneNumberExists = await checkTempIdOrPhone({
phone_number: parsedData.data.phone_number, phone_number: parsedData.data.phone_number,
}); });
if (phoneNumberExists.ok || tempPhoneNumberExists.ok) { if (phoneNumberExists.ok || tempPhoneNumberExists.ok) {
return { return {
message: "Phone number already exists.", message: "Phone number already exists.",
payload: formData, payload: formData,
db_error: "phone_number", db_error: "phone_number",
}; };
} }
const [signupError, signupResponse] = await tryCatch( const [signupError, signupResponse] = await tryCatch(
backendRegister({ backendRegister({
payload: { payload: {
firstname: parsedData.data.name.split(" ")[0], firstname: parsedData.data.name.split(" ")[0],
lastname: parsedData.data.name.split(" ").slice(1).join(" "), lastname: parsedData.data.name.split(" ").slice(1).join(" "),
username: parsedData.data.phone_number, username: parsedData.data.phone_number,
address: parsedData.data.address, address: parsedData.data.address,
id_card: parsedData.data.id_card, id_card: parsedData.data.id_card,
dob: new Date(parsedData.data.dob).toISOString().split("T")[0], dob: new Date(parsedData.data.dob).toISOString().split("T")[0],
mobile: parsedData.data.phone_number, mobile: parsedData.data.phone_number,
island: Number.parseInt(parsedData.data.island_id), island: Number.parseInt(parsedData.data.island_id),
atoll: Number.parseInt(parsedData.data.atoll_id), atoll: Number.parseInt(parsedData.data.atoll_id),
acc_no: parsedData.data.accNo, acc_no: parsedData.data.accNo,
terms_accepted: parsedData.data.terms, terms_accepted: parsedData.data.terms,
policy_accepted: parsedData.data.policy, policy_accepted: parsedData.data.policy,
}, },
}), }),
); );
if (signupError) { if (signupError) {
return { return {
message: signupError.message, message: signupError.message,
payload: formData, payload: formData,
db_error: "phone_number", db_error: "phone_number",
}; };
} }
console.log("SIGNUP RESPONSE", signupResponse); console.log("SIGNUP RESPONSE", signupResponse);
redirect( redirect(
`/auth/verify-otp-registration?phone_number=${encodeURIComponent(signupResponse.t_username)}`, `/auth/verify-otp-registration?phone_number=${encodeURIComponent(signupResponse.t_username)}`,
); );
return { message: "User created successfully", error: "success" }; return { message: "User created successfully", error: "success" };
} }
export const sendOtp = async (phoneNumber: string, code: string) => { export const sendOtp = async (phoneNumber: string, code: string) => {
// Implement sending OTP code via SMS // Implement sending OTP code via SMS
console.log("Send OTP server fn", phoneNumber, code); console.log("Send OTP server fn", phoneNumber, code);
const respose = await fetch(`${process.env.SMS_API_BASE_URL}/api/sms`, { const respose = await fetch(`${process.env.SMS_API_BASE_URL}/api/sms`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${process.env.SMS_API_KEY}`, Authorization: `Bearer ${process.env.SMS_API_KEY}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
check_delivery: false, check_delivery: false,
number: phoneNumber, number: phoneNumber,
message: `Your OTP code is ${code}`, message: `Your OTP code is ${code}`,
}), }),
}); });
const data = await respose.json(); const data = await respose.json();
console.log(data); console.log(data);
return data; return data;
}; };
export async function backendMobileLogin({ mobile }: { mobile: string }) { export async function backendMobileLogin({ mobile }: { mobile: string }) {
const response = await fetch( const response = await fetch(
`${process.env.SARLINK_API_BASE_URL}/auth/mobile/`, `${process.env.SARLINK_API_BASE_URL}/auth/mobile/`,
{ {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
mobile, mobile,
}), }),
}, },
); );
return handleApiResponse<{ detail: string }>(response, "backendMobileLogin"); return handleApiResponse<{ detail: string }>(response, "backendMobileLogin");
} }

View File

@@ -8,10 +8,9 @@ import type {
ApiResponse, ApiResponse,
NewPayment, NewPayment,
Payment, Payment,
Topup Topup,
} from "@/lib/backend-types"; } from "@/lib/backend-types";
import type { TopupResponse } from "@/lib/types"; import type { TopupResponse } from "@/lib/types";
import type { User } from "@/lib/types/user";
import { handleApiResponse } from "@/utils/tryCatch"; import { handleApiResponse } from "@/utils/tryCatch";
type GenericGetResponseProps = { type GenericGetResponseProps = {
@@ -25,7 +24,8 @@ export async function createPayment(data: NewPayment) {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
console.log("data", data); console.log("data", data);
const response = await fetch( const response = await fetch(
`${process.env.SARLINK_API_BASE_URL // }); `${
process.env.SARLINK_API_BASE_URL // });
}/api/billing/payment/`, }/api/billing/payment/`,
{ {
method: "POST", method: "POST",
@@ -94,15 +94,21 @@ type GetPaymentProps = {
[key: string]: string | number | undefined; // Allow additional properties for flexibility [key: string]: string | number | undefined; // Allow additional properties for flexibility
}; };
export async function getPayments(params: GetPaymentProps) { export async function getPayments(
params: GetPaymentProps,
allPayments = false,
) {
// Build query string from all defined params // Build query string from all defined params
const query = Object.entries(params) const query = Object.entries(params)
.filter(([_, value]) => value !== undefined && value !== "") .filter(([_, value]) => value !== undefined && value !== "")
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`) .map(
([key, value]) =>
`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`,
)
.join("&"); .join("&");
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
const response = await fetch( const response = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/billing/payment/?${query}`, `${process.env.SARLINK_API_BASE_URL}/api/billing/payment/?${query}&all_payments=${allPayments}`,
{ {
method: "GET", method: "GET",
headers: { headers: {
@@ -123,17 +129,22 @@ export async function getPayments(params: GetPaymentProps) {
return data; return data;
} }
export async function getTopups(params: GenericGetResponseProps) { export async function getTopups(
params: GenericGetResponseProps,
all_topups = false,
) {
// Build query string from all defined params // Build query string from all defined params
const query = Object.entries(params) const query = Object.entries(params)
.filter(([_, value]) => value !== undefined && value !== "") .filter(([_, value]) => value !== undefined && value !== "")
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`) .map(
([key, value]) =>
`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`,
)
.join("&"); .join("&");
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
const response = await fetch( const response = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/billing/topup/?${query}`, `${process.env.SARLINK_API_BASE_URL}/api/billing/topup/?${query}&all_topups=${all_topups}`,
{ {
method: "GET", method: "GET",
headers: { headers: {
@@ -224,17 +235,17 @@ export type VerifyDevicePaymentState = {
success: boolean; success: boolean;
fieldErrors: Record<string, string>; fieldErrors: Record<string, string>;
payload?: FormData; payload?: FormData;
} };
export async function verifyDevicePayment( export async function verifyDevicePayment(
_prevState: VerifyDevicePaymentState, _prevState: VerifyDevicePaymentState,
formData: FormData formData: FormData,
): Promise<VerifyDevicePaymentState> { ): Promise<VerifyDevicePaymentState> {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
// Get the payment ID and method from the form data // Get the payment ID and method from the form data
const paymentId = formData.get('paymentId') as string; const paymentId = formData.get("paymentId") as string;
const method = formData.get('method') as "TRANSFER" | "WALLET"; const method = formData.get("method") as "TRANSFER" | "WALLET";
if (!paymentId) { if (!paymentId) {
return { return {
@@ -267,14 +278,18 @@ export async function verifyDevicePayment(
}, },
); );
const result = await handleApiResponse<Payment>(response, "verifyDevicePayment"); const result = await handleApiResponse<Payment>(
response,
"verifyDevicePayment",
);
revalidatePath("/payments/[paymentId]", "page"); revalidatePath("/payments/[paymentId]", "page");
return { return {
message: method === "WALLET" message:
? "Payment completed successfully using wallet!" method === "WALLET"
: "Payment verification successful!", ? "Payment completed successfully using wallet!"
: "Payment verification successful!",
success: true, success: true,
fieldErrors: {}, fieldErrors: {},
payment: result, payment: result,
@@ -282,7 +297,8 @@ export async function verifyDevicePayment(
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
return { return {
message: error.message || "Payment verification failed. Please try again.", message:
error.message || "Payment verification failed. Please try again.",
success: false, success: false,
fieldErrors: {}, fieldErrors: {},
}; };
@@ -296,7 +312,6 @@ export async function verifyDevicePayment(
} }
} }
export type VerifyTopupPaymentState = { export type VerifyTopupPaymentState = {
transaction?: { transaction?: {
sourceBank: string; sourceBank: string;
@@ -306,15 +321,15 @@ export type VerifyTopupPaymentState = {
success: boolean; success: boolean;
fieldErrors: Record<string, string>; fieldErrors: Record<string, string>;
payload?: FormData; payload?: FormData;
} };
export async function verifyTopupPayment( export async function verifyTopupPayment(
_prevState: VerifyTopupPaymentState, _prevState: VerifyTopupPaymentState,
formData: FormData formData: FormData,
): Promise<VerifyTopupPaymentState> { ): Promise<VerifyTopupPaymentState> {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
// Get the topup ID from the form data or use a hidden input // Get the topup ID from the form data or use a hidden input
const topupId = formData.get('topupId') as string; const topupId = formData.get("topupId") as string;
if (!topupId) { if (!topupId) {
return { return {
@@ -336,7 +351,10 @@ export async function verifyTopupPayment(
}, },
); );
const result = await handleApiResponse<TopupResponse>(response, "verifyTopupPayment"); const result = await handleApiResponse<TopupResponse>(
response,
"verifyTopupPayment",
);
revalidatePath("/top-ups/[topupId]", "page"); revalidatePath("/top-ups/[topupId]", "page");
@@ -349,7 +367,8 @@ export async function verifyTopupPayment(
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
return { return {
message: error.message || "Please check your payment details and try again.", message:
error.message || "Please check your payment details and try again.",
success: false, success: false,
fieldErrors: {}, fieldErrors: {},
}; };
@@ -362,19 +381,3 @@ export async function verifyTopupPayment(
} }
} }
} }
export async function getProfile() {
const session = await getServerSession(authOptions);
const response = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/auth/profile/`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Token ${session?.apiToken}`,
},
},
);
return handleApiResponse<User>(response, "getProfile");
}

View File

@@ -1,123 +1,305 @@
"use server"; "use server";
import { VerifyUserDetails } from "@/lib/person";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { CreateClient } from "./ninja/client"; import { getServerSession } from "next-auth";
import { authOptions } from "@/app/auth";
import type { RejectUserFormState } from "@/components/user/user-reject-dialog";
import type { ApiError } from "@/lib/backend-types";
import type { User } from "@/lib/types/user";
import { handleApiResponse } from "@/utils/tryCatch";
export async function VerifyUser(userId: string) { type VerifyUserResponse =
// const user = await prisma.user.findUnique({ | {
// where: { ok: boolean;
// id: userId, mismatch_fields: string[] | null;
// }, error: string | null;
// include: { detail: string | null;
// atoll: true, }
// island: true, | {
// }, message: boolean;
// }); };
// if (!user) { export async function verifyUser(userId: string) {
// throw new Error("User not found"); const session = await getServerSession(authOptions);
// } if (!session?.apiToken) {
// const isValidPerson = await VerifyUserDetails({ user }); return { ok: false, error: "Not authenticated" } as const;
// if (!isValidPerson) }
// throw new Error("The user details does not match national data.");
// if (isValidPerson) {
// await prisma.user.update({
// where: {
// id: userId,
// },
// data: {
// verified: true,
// },
// });
// const ninjaClient = await CreateClient({
// group_settings_id: "",
// address1: "",
// city: user.atoll?.name || "",
// state: user.island?.name || "",
// postal_code: "",
// country_id: "462",
// address2: user.address || "",
// contacts: {
// first_name: user.name?.split(" ")[0] || "",
// last_name: user.name?.split(" ")[1] || "",
// email: user.email || "",
// phone: user.phoneNumber || "",
// send_email: false,
// custom_value1: user.dob?.toISOString().split("T")[0] || "",
// custom_value2: user.id_card || "",
// custom_value3: "",
// },
// });
// }
// revalidatePath("/users");
}
export async function Rejectuser({
userId,
reason,
}: { userId: string; reason: string }) {
// const user = await prisma.user.findUnique({
// where: {
// id: userId,
// },
// });
// if (!user) {
// throw new Error("User not found");
// }
// await SendUserRejectionDetailSMS({
// details: reason,
// phoneNumber: user.phoneNumber,
// });
// await prisma.user.delete({
// where: {
// id: userId,
// },
// });
revalidatePath("/users");
redirect("/users");
}
export const SendUserRejectionDetailSMS = async ({
details,
phoneNumber,
}: {
details: string;
phoneNumber: string;
}) => {
try { try {
const respose = await fetch(`${process.env.SMS_API_BASE_URL}/api/sms`, { const r = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/auth/users/${userId}/verify/`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Token ${session.apiToken}`,
},
},
);
const body = (await r.json().catch(() => ({}))) as VerifyUserResponse & {
message?: string;
detail?: string;
};
if (!r.ok) {
const msg = body?.message || body?.detail || "User verification failed";
return {
ok: false,
error: msg,
mismatch_fields: body?.mismatch_fields || null,
} as const;
}
return { ok: true, data: body } as const;
} catch (err) {
return { ok: false, error: (err as Error).message } as const;
}
}
export async function getProfile() {
const session = await getServerSession(authOptions);
const response = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/auth/profile/`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Token ${session?.apiToken}`,
},
},
);
return handleApiResponse<User>(response, "getProfile");
}
export async function rejectUser(
_prevState: RejectUserFormState,
formData: FormData,
): Promise<RejectUserFormState> {
const userId = formData.get("userId") as string;
const rejection_details = formData.get("rejection_details") as string;
const session = await getServerSession(authOptions);
const response = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/auth/users/${userId}/reject/`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Token ${session?.apiToken}`,
},
body: JSON.stringify({ rejection_details: rejection_details }),
},
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(
errorData.message || errorData.detail || "Failed to reject user",
);
}
// Handle 204 No Content response (successful deletion)
if (response.status === 204) {
revalidatePath("/users");
redirect("/users");
}
revalidatePath("/users");
const error = await response.json();
return {
message:
(error as ApiError).message ||
(error as ApiError).detail ||
"An unexpected error occurred.",
fieldErrors: {},
payload: formData,
};
}
export type UpdateUserFormState = {
message: string;
fieldErrors?: {
id_card?: string[];
first_name?: string[];
last_name?: string[];
dob?: string[];
mobile?: string[];
address?: string[];
};
payload?: FormData;
};
export async function updateUser(
_prevState: UpdateUserFormState,
formData: FormData,
): Promise<UpdateUserFormState> {
const userId = formData.get("userId") as string;
const data: Record<string, string | number | boolean> = {};
for (const [key, value] of formData.entries()) {
if (value !== undefined && value !== "") {
data[key] = typeof value === "number" ? value : String(value);
}
}
const session = await getServerSession(authOptions);
const response = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/auth/users/${userId}/update/`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Token ${session?.apiToken}`,
},
body: JSON.stringify(data),
},
);
const json = await response.json();
if (!response.ok) {
const isFieldErrorObject =
json &&
typeof json === "object" &&
!Array.isArray(json) &&
Object.values(json).every(
(val) => Array.isArray(val) && val.every((v) => typeof v === "string"),
);
return {
message:
json.message ||
json.detail ||
(isFieldErrorObject
? "Please correct the highlighted fields."
: "An error occurred while updating the user."),
fieldErrors: isFieldErrorObject ? json : json.field_errors || {},
payload: formData,
};
}
// Successful update
const updatedUser = json as User;
revalidatePath("/users/[userId]/update", "page");
revalidatePath("/users/[userId]/verify", "page");
return {
...updatedUser,
message: "User updated successfully",
};
}
export async function updateUserAgreement(
_prevState: UpdateUserFormState,
formData: FormData,
): Promise<UpdateUserFormState> {
const userId = formData.get("userId") as string;
// Remove userId from formData before sending to API
const apiFormData = new FormData();
for (const [key, value] of formData.entries()) {
if (key !== "userId") {
apiFormData.append(key, value);
}
}
console.log({ apiFormData });
const session = await getServerSession(authOptions);
const response = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/auth/users/${userId}/agreement/`,
{
method: "PUT",
headers: {
Authorization: `Token ${session?.apiToken}`,
},
body: apiFormData,
},
);
console.log("response in update user agreement action", response);
if (!response.ok) {
const errorData = await response.json();
return {
message:
errorData.message ||
errorData.detail ||
"An error occurred while updating the user agreement.",
fieldErrors: errorData.field_errors || {},
payload: formData,
};
}
const updatedUserAgreement = (await response.json()) as { agreement: string };
revalidatePath("/users/[userId]/update", "page");
revalidatePath("/users/[userId]/verify", "page");
revalidatePath("/users/[userId]/agreement", "page");
return {
...updatedUserAgreement,
message: "User agreement updated successfully",
};
}
export type AddTopupFormState = {
status: boolean;
message: string;
fieldErrors?: {
amount?: string[];
description?: string[];
};
payload?: FormData;
};
export async function adminUserTopup(
_prevState: AddTopupFormState,
formData: FormData,
): Promise<AddTopupFormState> {
const user_id = formData.get("user_id") as string;
const amount = formData.get("amount") as string;
const description = formData.get("description") as string;
const session = await getServerSession(authOptions);
if (!amount) {
return {
status: false,
message: "Amount is required",
fieldErrors: { amount: ["Amount is required"], description: [] },
payload: formData,
}
}
const response = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/billing/admin-topup/`,
{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${process.env.SMS_API_KEY}`, Authorization: `Token ${session?.apiToken}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
check_delivery: false, amount: Number.parseInt(amount),
number: phoneNumber, user_id: Number.parseInt(user_id),
message: details, description,
}), }),
}); },
const data = await respose.json(); );
console.log(data); if (!response.ok) {
return data; const errorData = await response.json();
} catch (error) { return {
console.error(error); status: false,
message:
errorData.message ||
errorData.detail ||
"An error occurred while topping up the user.",
fieldErrors: {},
payload: formData,
};
} }
};
export async function AddDevice({ revalidatePath("/users/[userId]/topup", "page");
name, return {
mac_address, status: true,
user_id, message: "User topped up successfully",
}: { name: string; mac_address: string; user_id: string }) { fieldErrors: {},
// const newDevice = await prisma.device.create({ payload: formData,
// data: { };
// name: name,
// mac: mac_address,
// userId: user_id,
// },
// });
revalidatePath("/devices");
// return newDevice;
} }

View File

@@ -1,7 +1,8 @@
export default function AuthLayout({ export default function AuthLayout({
children, children,
}: { children: React.ReactNode }) { }: {
children: React.ReactNode;
}) {
return ( return (
<div className="bg-gray-100 dark:bg-black w-full h-screen flex items-center justify-center font-sans"> <div className="bg-gray-100 dark:bg-black w-full h-screen flex items-center justify-center font-sans">
{children} {children}

View File

@@ -11,7 +11,7 @@ export default async function VerifyRegistrationOTP({
}: { }: {
searchParams: Promise<{ phone_number: string }>; searchParams: Promise<{ phone_number: string }>;
}) { }) {
const session = await getServerSession(authOptions) const session = await getServerSession(authOptions);
if (session) { if (session) {
// If the user is already logged in, redirect them to the home page // If the user is already logged in, redirect them to the home page
return redirect("/"); return redirect("/");

View File

@@ -1,13 +1,29 @@
import React from 'react' import { getProfile } from "@/actions/user-actions";
import { AgreementCard } from "@/components/agreement-card";
import { tryCatch } from "@/utils/tryCatch";
export default function Agreements() { export default async function Agreements() {
const [error, profile] = await tryCatch(getProfile());
return ( return (
<div> <div>
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4"> <div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl"> <h3 className="text-sarLinkOrange text-2xl">Agreements</h3>
Agreements </div>
</h3> <div className="grid grid-cols-1">
{error ? (
<div className="text-red-500">
An error occurred while fetching agreements: {error.message}
</div>
) : (
<div>
{profile.agreement ? (
<AgreementCard agreement={profile.agreement} />
) : (
<div className="text-gray-500">No agreement found.</div>
)}
</div>
)}
</div> </div>
</div> </div>
) );
} }

View File

@@ -1,8 +1,6 @@
import FullPageLoader from '@/components/full-page-loader' import FullPageLoader from "@/components/full-page-loader";
import React from 'react' import React from "react";
export default function Loading() { export default function Loading() {
return ( return <FullPageLoader />;
<FullPageLoader />
)
} }

View File

@@ -1,78 +1,76 @@
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { import {
Table, Table,
TableBody, TableBody,
TableCaption, TableCell,
TableCell, TableFooter,
TableFooter, TableHead,
TableHead, TableHeader,
TableHeader, TableRow,
TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export default function DevicesTableSkeleton() { export default function DevicesTableSkeleton() {
return ( return (
<> <>
<div className="hidden sm:block"> <div className="hidden sm:block">
<Table className="overflow-scroll"> <Table className="overflow-scroll">
<TableCaption>Table of all devices.</TableCaption> <TableHeader>
<TableHeader> <TableRow>
<TableRow> <TableHead>Device Name</TableHead>
<TableHead>Device Name</TableHead> <TableHead>MAC Address</TableHead>
<TableHead>MAC Address</TableHead> <TableHead>#</TableHead>
<TableHead>#</TableHead> </TableRow>
</TableRow> </TableHeader>
</TableHeader> <TableBody className="overflow-scroll">
<TableBody className="overflow-scroll"> {Array.from({ length: 10 }).map((_, i) => (
{Array.from({ length: 10 }).map((_, i) => ( <TableRow key={`${i + 1}`}>
<TableRow key={`${i + 1}`}> <TableCell>
<TableCell> <Skeleton className="w-full h-10 rounded" />
<Skeleton className="w-full h-10 rounded" /> </TableCell>
</TableCell> <TableCell>
<TableCell> <Skeleton className="w-full h-10 rounded" />
<Skeleton className="w-full h-10 rounded" /> </TableCell>
</TableCell> <TableCell>
<TableCell> <Skeleton className="w-full h-10 rounded" />
<Skeleton className="w-full h-10 rounded" /> </TableCell>
</TableCell> </TableRow>
</TableRow> ))}
))} </TableBody>
</TableBody> <TableFooter>
<TableFooter> <TableRow>
<TableRow> <TableCell colSpan={2}>
<TableCell colSpan={2}> <Skeleton className="w-full h-4 rounded" />
<Skeleton className="w-full h-4 rounded" /> </TableCell>
</TableCell> <TableCell className="text-muted-foreground">
<TableCell className="text-muted-foreground"> <Skeleton className="w-20 h-4 rounded" />
<Skeleton className="w-20 h-4 rounded" /> </TableCell>
</TableCell> </TableRow>
</TableRow> </TableFooter>
</TableFooter> </Table>
</Table> </div>
</div> <div className="sm:hidden my-4">
<div className="sm:hidden my-4"> {Array.from({ length: 10 }).map((_, i) => (
{Array.from({ length: 10 }).map((_, i) => ( <DeviceCardSkeleton key={`${i + 1}`} />
<DeviceCardSkeleton key={`${i + 1}`} /> ))}
))} </div>
</div> </>
</> );
);
} }
function DeviceCardSkeleton() { function DeviceCardSkeleton() {
return ( return (
<div <div
className={cn( className={cn(
"flex text-sm justify-between items-center my-2 p-4 border rounded-md bg-gray-100", "flex text-sm justify-between items-center my-2 p-4 border rounded-md bg-gray-100",
)} )}
> >
<div className="font-semibold flex w-full flex-col items-start gap-2 mb-2 relative"> <div className="font-semibold flex w-full flex-col items-start gap-2 mb-2 relative">
<Skeleton className="w-32 h-6" /> <Skeleton className="w-32 h-6" />
<Skeleton className="w-36 h-6" /> <Skeleton className="w-36 h-6" />
<Skeleton className="w-32 h-4" /> <Skeleton className="w-32 h-4" />
<Skeleton className="w-40 h-8" /> <Skeleton className="w-40 h-8" />
</div> </div>
</div> </div>
); );
} }

View File

@@ -49,7 +49,7 @@ export default async function Devices({
label: "Vendor", label: "Vendor",
type: "string", type: "string",
placeholder: "Enter vendor name", placeholder: "Enter vendor name",
} },
]} ]}
/> />
</div> </div>

View File

@@ -6,9 +6,9 @@ export default function DashboardLayout({
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return <ApplicationLayout> return (
<QueryProvider> <ApplicationLayout>
{children} <QueryProvider>{children}</QueryProvider>
</QueryProvider> </ApplicationLayout>
</ApplicationLayout>; );
} }

View File

@@ -1,41 +1,63 @@
import { Suspense } from "react"; import { Suspense } from "react";
import { DevicesTable } from "@/components/devices-table"; import { DevicesTable } from "@/components/devices-table";
import Search from "@/components/search"; import DynamicFilter from "@/components/generic-filter";
export default async function ParentalControl({ export default async function ParentalControl({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ searchParams: Promise<{
page: number; page: number;
sortBy: string; sortBy: string;
status: string; status: string;
}>; }>;
}) { }) {
const parentalControlFilters = {
is_active: "true",
has_a_pending_payment: "false",
};
const parentalControlFilters = { return (
is_active: "true", <div>
has_a_pending_payment: "false", <div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
}; <h3 className="text-sarLinkOrange text-2xl">Parental Control</h3>
</div>
return ( <div
<div> id="user-filters"
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4"> className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
<h3 className="text-sarLinkOrange text-2xl">Parental Control</h3> >
</div> <DynamicFilter
description="Filter devices by name, MAC address, or vendor."
<div title="Device Filter"
id="user-filters" inputs={[
className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start" {
> name: "name",
<Search /> label: "Device Name",
</div> type: "string",
<Suspense key={(await searchParams).page} fallback={"loading...."}> placeholder: "Enter device name",
<DevicesTable },
parentalControl={true} {
searchParams={searchParams} name: "mac",
additionalFilters={parentalControlFilters} label: "MAC Address",
/> type: "string",
</Suspense> placeholder: "Enter MAC address",
</div> },
); {
name: "vendor",
label: "Vendor",
type: "string",
placeholder: "Enter vendor name",
},
]}
/>{" "}
</div>
<Suspense key={(await searchParams).page} fallback={"loading...."}>
<DevicesTable
parentalControl={true}
searchParams={searchParams}
additionalFilters={parentalControlFilters}
/>
</Suspense>
</div>
);
} }

View File

@@ -1,10 +1,12 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getPayment, getProfile } from "@/actions/payment"; import { getPayment } from "@/actions/payment";
import { getProfile } from "@/actions/user-actions";
import CancelPaymentButton from "@/components/billing/cancel-payment-button"; import CancelPaymentButton from "@/components/billing/cancel-payment-button";
import ExpiryCountDown from "@/components/billing/expiry-time-countdown"; import ExpiryCountDown from "@/components/billing/expiry-time-countdown";
import ClientErrorMessage from "@/components/client-error-message"; import ClientErrorMessage from "@/components/client-error-message";
import DevicesToPay from "@/components/devices-to-pay"; import DevicesToPay from "@/components/devices-to-pay";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { TextShimmer } from "@/components/ui/text-shimmer";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { tryCatch } from "@/utils/tryCatch"; import { tryCatch } from "@/utils/tryCatch";
export default async function PaymentPage({ export default async function PaymentPage({
@@ -29,29 +31,41 @@ export default async function PaymentPage({
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-4 mb-4 mx-2"> <div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-4 mb-4 mx-2">
<h3 className="text-sarLinkOrange text-2xl">Payment</h3> <h3 className="text-sarLinkOrange text-2xl">Payment</h3>
<div className="flex flex-col gap-4 items-end w-full"> <div className="flex flex-col gap-4 items-end w-full">
<Button {!payment.is_expired &&
disabled payment.paid &&
className={cn( payment.status !== "PENDING" && (
"rounded-md opacity-100! uppercase font-semibold", <Button
payment?.paid disabled
? "text-green-500 bg-green-500/20" className={cn(
: "text-yellow-500 bg-yellow-900", "rounded-md opacity-100! uppercase font-semibold",
payment?.paid
? "text-green-900 bg-green-500/20"
: "text-inherit bg-yellow-400",
)}
>
{payment.status}
</Button>
)} )}
> {payment.status === "PENDING" && !payment.is_expired && (
{payment?.paid ? "Paid" : "Pending"} <Button>
</Button> <TextShimmer>Payment Pending</TextShimmer>{" "}
{!payment.paid && ( </Button>
payment.is_expired ? ( )}
{!payment.paid &&
(payment.is_expired ? (
<Button <Button
disabled disabled
className="rounded-md opacity-100! uppercase font-semibold text-red-500 bg-red-500/20" className="rounded-md opacity-100! uppercase font-semibold text-red-500 bg-red-500/20"
> >
Payment Expired Payment Expired
</Button> </Button>
) : ( ) : payment.status === "PENDING" ? (
<CancelPaymentButton paymentId={paymentId} /> <CancelPaymentButton paymentId={paymentId} />
) ) : payment.status === "CANCELLED" ? (
)} <Button disabled>Payment Cancelled</Button>
) : (
""
))}
</div> </div>
</div> </div>
{!payment.paid && ( {!payment.paid && (
@@ -62,6 +76,7 @@ export default async function PaymentPage({
className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start" className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
> >
<DevicesToPay <DevicesToPay
disabled={payment.paid || payment.is_expired}
user={userProfile || undefined} user={userProfile || undefined}
payment={payment || undefined} payment={payment || undefined}
/> />

View File

@@ -1,8 +1,6 @@
import PriceCalculator from '@/components/price-calculator' import PriceCalculator from "@/components/price-calculator";
import React from 'react' import React from "react";
export default function Pricing() { export default function Pricing() {
return ( return <PriceCalculator />;
<PriceCalculator />
)
} }

View File

@@ -0,0 +1,95 @@
import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/auth";
import ClientErrorMessage from "@/components/client-error-message";
import { Badge } from "@/components/ui/badge";
import { FloatingLabelInput } from "@/components/ui/floating-label";
import { getProfileById } from "@/queries/users";
import { tryCatch } from "@/utils/tryCatch";
export default async function Profile() {
const session = await getServerSession(authOptions);
if (!session?.user) return redirect("/auth/signin?callbackUrl=/profile");
const [error, profile] = await tryCatch(getProfileById(session?.user.id));
if (error) {
if (error.message === "Invalid token.") redirect("/auth/signin");
return <ClientErrorMessage message={error.message} />;
}
return (
<div>
<div className="flex justify-between items-center font-bold border rounded-md border-dashed title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">Profile</h3>
<div className="text-sarLinkOrange uppercase font-mono text-sm flex flex-col items-center rounded gap-2 py-2 px-4">
<span>Profile Status</span>
{verifiedStatus(profile?.verified ?? false)}
</div>
</div>
<fieldset>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 max-w-4xl">
<FloatingLabelInput
id="floating-name"
label="Full Name"
value={`${profile?.first_name} ${profile?.last_name}`}
readOnly
/>
<FloatingLabelInput
id="floating-id-card"
label="ID Card"
value={`${profile?.id_card}`}
readOnly
/>
<FloatingLabelInput
id="floating-island"
label="Island"
value={`${profile?.atoll.name}. ${profile?.island.name}`}
readOnly
/>
<FloatingLabelInput
id="floating-dob"
label="Date of Birth"
value={`${new Date(
profile?.dob.toString() ?? "",
).toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
})}`}
readOnly
/>
<FloatingLabelInput
id="floating-address"
label="Address"
value={`${profile?.address}`}
readOnly
/>
<FloatingLabelInput
id="floating-mobile"
label="Phone Number"
value={`${profile?.mobile}`}
readOnly
/>
<FloatingLabelInput
id="floating-account"
label="Account Number"
value={`${profile?.acc_no}`}
readOnly
/>
</div>
</fieldset>
{/* <Suspense key={query} fallback={"loading...."}>
<TopupsTable searchParams={searchParams} />
</Suspense> */}
</div>
);
}
function verifiedStatus(status: boolean) {
switch (status) {
case true:
return <Badge className="bg-green-500 text-white">Verified</Badge>;
case false:
return <Badge className="bg-red-500 text-white">Not Verified</Badge>;
default:
return <Badge className="bg-yellow-500 text-white">Unknown</Badge>;
}
}

View File

@@ -20,42 +20,48 @@ export default async function TopupPage({
return <ClientErrorMessage message={error.message} />; return <ClientErrorMessage message={error.message} />;
} }
return ( return (
<div> <div>
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-4 mb-4 mx-2"> <div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-4 mb-4 mx-2">
<h3 className="text-sarLinkOrange text-2xl">Topup</h3> <h3 className="text-sarLinkOrange text-2xl">Topup</h3>
<div className="flex flex-col gap-4 items-end w-full"> <div className="flex flex-col gap-4 items-end w-full">
{!topup.is_expired && ( {!topup.is_expired && topup.paid && topup.status !== "PENDING" && (
<Button <Button
disabled disabled
className={cn( className={cn(
"rounded-md opacity-100! uppercase font-semibold", "rounded-md opacity-100! uppercase font-semibold",
// topup?.paid topup?.paid
// ? "text-green-900 bg-green-500/20" ? "text-green-900 bg-green-500/20"
// : "text-inherit bg-yellow-400", : "text-inherit bg-yellow-400",
)} )}
> >
{topup?.paid ? <span>Paid</span> : <TextShimmer>Payment Pending</TextShimmer>} {topup.status}
</Button>
)}
{topup.status === "PENDING" && !topup.is_expired && (
<Button>
<TextShimmer>Payment Pending</TextShimmer>{" "}
</Button> </Button>
)} )}
{!topup.paid && ( {!topup.paid &&
topup.is_expired ? ( (topup.is_expired ? (
<Button <Button
disabled disabled
className="rounded-md opacity-100! uppercase font-semibold text-red-500 bg-red-500/20" className="rounded-md opacity-100! uppercase font-semibold text-red-500 bg-red-500/20"
> >
Topup Expired Topup Expired
</Button> </Button>
) : ( ) : topup.status === "PENDING" ? (
<CancelTopupButton topupId={topupId} /> <CancelTopupButton topupId={topupId} />
) ) : topup.status === "CANCELLED" ? (
)} <Button disabled>Topup Cancelled</Button>
) : (
""
))}
</div> </div>
</div> </div>
{!topup.paid && ( {!topup.paid && topup.status === "PENDING" && !topup.is_expired && (
<ExpiryCountDown expiryLabel="Top up" expiresAt={topup.expires_at} /> <ExpiryCountDown expiryLabel="Top up" expiresAt={topup.expires_at} />
)} )}
<div <div
@@ -63,7 +69,9 @@ export default async function TopupPage({
className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start" className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
> >
<TopupToPay <TopupToPay
disabled={topup.paid || topup.is_expired} disabled={
topup.paid || topup.is_expired || topup.status === "CANCELLED"
}
topup={topup || undefined} topup={topup || undefined}
/> />
</div> </div>

View File

@@ -3,84 +3,84 @@ import DynamicFilter from "@/components/generic-filter";
import { TopupsTable } from "@/components/topups-table"; import { TopupsTable } from "@/components/topups-table";
export default async function Topups({ export default async function Topups({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ searchParams: Promise<{
query: string; query: string;
page: number; page: number;
sortBy: string; sortBy: string;
status: string; status: string;
}>; }>;
}) { }) {
const query = (await searchParams)?.query || ""; const query = (await searchParams)?.query || "";
return ( return (
<div> <div>
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4"> <div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">My Topups</h3> <h3 className="text-sarLinkOrange text-2xl">My Topups</h3>
</div> </div>
<div <div
id="topup-filters" id="topup-filters"
className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start" className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
> >
<DynamicFilter <DynamicFilter
inputs={[ inputs={[
{ {
label: "Status", label: "Status",
name: "status", name: "status",
type: "radio-group", type: "radio-group",
options: [ options: [
{ {
label: "All", label: "All",
value: "", value: "",
}, },
{ {
label: "Pending", label: "Pending",
value: "PENDING", value: "PENDING",
}, },
{ {
label: "Cancelled", label: "Cancelled",
value: "CANCELLED", value: "CANCELLED",
}, },
{ {
label: "Paid", label: "Paid",
value: "PAID", value: "PAID",
}, },
], ],
}, },
{ {
label: "Topup Expiry", label: "Topup Expiry",
name: "is_expired", name: "is_expired",
type: "radio-group", type: "radio-group",
options: [ options: [
{ {
label: "All", label: "All",
value: "", value: "",
}, },
{ {
label: "Expired", label: "Expired",
value: "true", value: "true",
}, },
{ {
label: "Not Expired", label: "Not Expired",
value: "false", value: "false",
}, },
], ],
}, },
{ {
label: "Topup Amount", label: "Topup Amount",
name: "amount", name: "amount",
type: "dual-range-slider", type: "dual-range-slider",
min: 0, min: 0,
max: 1000, max: 1000,
step: 10, step: 10,
}, },
]} ]}
/> />
</div> </div>
<Suspense key={query} fallback={"loading...."}> <Suspense key={query} fallback={"loading...."}>
<TopupsTable searchParams={searchParams} /> <TopupsTable searchParams={searchParams} />
</Suspense> </Suspense>
</div> </div>
); );
} }

View File

@@ -17,7 +17,7 @@ export default async function UserDevices({
}) { }) {
const query = (await searchParams)?.query || ""; const query = (await searchParams)?.query || "";
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (session?.user?.is_admin !== true) redirect("/devices?page=1"); if (!session?.user?.is_admin) redirect("/devices?page=1");
return ( return (
<div> <div>
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4"> <div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
@@ -55,12 +55,12 @@ export default async function UserDevices({
label: "Device User", label: "Device User",
type: "string", type: "string",
placeholder: "User name or id card", placeholder: "User name or id card",
} },
]} ]}
/> />
</div> </div>
<Suspense key={query} fallback={"loading...."}> <Suspense key={query} fallback={"loading...."}>
<AdminDevicesTable parentalControl={true} searchParams={searchParams} /> <AdminDevicesTable searchParams={searchParams} />
</Suspense> </Suspense>
</div> </div>
); );

View File

@@ -1,5 +1,9 @@
import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
import { Suspense } from "react"; import { Suspense } from "react";
import { authOptions } from "@/app/auth";
import { UsersPaymentsTable } from "@/components/admin/user-payments-table"; import { UsersPaymentsTable } from "@/components/admin/user-payments-table";
import DynamicFilter from "@/components/generic-filter";
export default async function UserPayments({ export default async function UserPayments({
searchParams, searchParams,
@@ -12,12 +16,69 @@ export default async function UserPayments({
}>; }>;
}) { }) {
const query = (await searchParams)?.query || ""; const query = (await searchParams)?.query || "";
// const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session?.user?.is_admin) redirect("/payments?page=1");
return ( return (
<div> <div>
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4"> <div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">User Payments</h3> <h3 className="text-sarLinkOrange text-2xl">User Payments</h3>
</div> </div>
<DynamicFilter
description="Filter user payments by name, MAC address, or vendor."
title="User Payments Filter"
inputs={[
{
name: "user",
label: "User",
type: "string",
placeholder: "Enter user name",
},
{
name: "mib_reference",
label: "MIB Reference",
type: "string",
placeholder: "Enter MIB Reference",
},
{
type: "dual-range-slider",
label: "Amount Range",
name: "amount",
max: 1200,
min: 0,
step: 10,
},
{
type: "dual-range-slider",
label: "Duration Range",
name: "number_of_months",
max: 12,
min: 1,
step: 1,
},
{
type: "radio-group",
label: "Payment Status",
name: "status",
options: [
{ label: "All", value: "" },
{ label: "Pending", value: "PENDING" },
{ label: "Paid", value: "PAID" },
{ label: "Cancelled", value: "CANCELLED" },
],
},
{
type: "radio-group",
label: "Payment Method",
name: "method",
options: [
{ label: "All", value: "" },
{ label: "Wallet", value: "WALLET" },
{ label: "Transfer", value: "TRANSFER" },
],
},
]}
/>
<Suspense key={query} fallback={"loading...."}> <Suspense key={query} fallback={"loading...."}>
<UsersPaymentsTable searchParams={searchParams} /> <UsersPaymentsTable searchParams={searchParams} />
</Suspense> </Suspense>

View File

@@ -0,0 +1,90 @@
import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
import { Suspense } from "react";
import { authOptions } from "@/app/auth";
import { AdminTopupsTable } from "@/components/admin/admin-topup-table";
import DynamicFilter from "@/components/generic-filter";
export default async function UserTopups({
searchParams,
}: {
searchParams: Promise<{
[key: string]: string;
}>;
}) {
const query = (await searchParams)?.query || "";
const session = await getServerSession(authOptions);
if (!session?.user?.is_admin) redirect("/top-ups?page=1");
return (
<div>
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">User Topups</h3>
</div>
<DynamicFilter
title="User Topups Filter"
description="Filter user topups by status, topup expiry, or amount."
inputs={[
{
name: "user",
label: "User",
type: "string",
placeholder: "Enter user name",
},
{
label: "Status",
name: "status",
type: "radio-group",
options: [
{
label: "All",
value: "",
},
{
label: "Pending",
value: "PENDING",
},
{
label: "Cancelled",
value: "CANCELLED",
},
{
label: "Paid",
value: "PAID",
},
],
},
{
label: "Topup Expiry",
name: "is_expired",
type: "radio-group",
options: [
{
label: "All",
value: "",
},
{
label: "Expired",
value: "true",
},
{
label: "Not Expired",
value: "false",
},
],
},
{
label: "Topup Amount",
name: "amount",
type: "dual-range-slider",
min: 0,
max: 1000,
step: 10,
},
]}
/>
<Suspense key={query} fallback={"loading...."}>
<AdminTopupsTable searchParams={searchParams} />
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,48 @@
import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/auth";
import ClientErrorMessage from "@/components/client-error-message";
import UserAgreementForm from "@/components/user/user-agreement-form";
import { getProfileById } from "@/queries/users";
import { tryCatch } from "@/utils/tryCatch";
// import {
// Select,
// SelectContent,
// SelectGroup,
// SelectItem,
// SelectLabel,
// SelectTrigger,
// SelectValue,
// } from "@/components/ui/select";
export default async function UserUpdate({
params,
}: {
params: Promise<{
userId: string;
}>;
}) {
const { userId } = await params;
const session = await getServerSession(authOptions);
if (!session?.user?.is_admin) return redirect("/devices?page=1");
const [error, user] = await tryCatch(getProfileById(userId));
if (error) {
if (error.message === "UNAUTHORIZED") {
redirect("/auth/signin");
} else {
return <ClientErrorMessage message={error.message} />;
}
}
return (
<div>
<div className="flex items-center justify-between text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">
Upload user user agreement
</h3>
</div>
<UserAgreementForm user={user} />
</div>
);
}

View File

@@ -0,0 +1,211 @@
import { EyeIcon, FileTextIcon, PencilIcon } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { redirect } from "next/navigation";
import AddTopupDialogForm from "@/components/admin/admin-topup-form";
import ClientErrorMessage from "@/components/client-error-message";
import InputReadOnly from "@/components/input-read-only";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import UserRejectDialog from "@/components/user/user-reject-dialog";
import { UserVerifyDialog } from "@/components/user/user-verify-dialog";
import { getNationalPerson } from "@/lib/person";
import { getProfileById } from "@/queries/users";
import { tryCatch } from "@/utils/tryCatch";
export default async function VerifyUserPage({
params,
}: {
params: Promise<{
userId: string;
}>;
}) {
const userId = (await params).userId;
const [error, dbUser] = await tryCatch(getProfileById(userId));
const [nationalDataEror, nationalData] = await tryCatch(
getNationalPerson({ idCard: dbUser?.id_card ?? "" }),
);
if (nationalDataEror) {
console.warn("Error fetching national data:", nationalDataEror);
}
if (error) {
if (error.message === "UNAUTHORIZED") {
redirect("/auth/signin");
} else {
return <ClientErrorMessage message={error.message} />;
}
}
// return <pre>{JSON.stringify(nationalData, null, 2)}</pre>
const fullName = `${dbUser?.first_name} ${dbUser?.last_name}`;
const nationalDob = nationalData?.dob?.split("T")[0];
const dbUserDob = new Date(dbUser?.dob).toISOString().split("T")[0];
return (
<div>
<div className="flex flex-wrap gap-2 items-center justify-between text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">User Information</h3>
<div className="flex flex-wrap gap-2">
{dbUser && !dbUser?.verified && <UserVerifyDialog user={dbUser} />}
{dbUser && !dbUser?.verified && <UserRejectDialog user={dbUser} />}
<AddTopupDialogForm user_id={userId} />
<Link href={"update"}>
<Button className="hover:cursor-pointer">
<PencilIcon />
Update User
</Button>
</Link>
<Link href={"agreement"}>
<Button className="hover:cursor-pointer">
<FileTextIcon />
Update Agreement
</Button>
</Link>
<Link
href={dbUser?.agreement || "#"}
target="_blank"
rel="noopener noreferrer"
>
<Button className="hover:cursor-pointer">
<EyeIcon />
View Agreement
</Button>
</Link>
{dbUser?.verified && (
<Badge variant={"secondary"} className="bg-lime-500">
Verified
</Badge>
)}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-start justify-start">
<div id="database-information">
<h4 className="p-2 rounded font-semibold">Database Information</h4>
<div className="shadow-md p-2 bg-sarLinkOrange/10 border border-dashed border-sarLinkOrange rounded-lg space-y-1 my-2 grid grid-cols-1 md:grid-cols-2 gap-2">
<InputReadOnly
showCheck
checkTrue={dbUser?.id_card === nationalData?.nic}
labelClassName="text-sarLinkOrange"
label="ID Card"
value={dbUser?.id_card ?? ""}
/>
<InputReadOnly
showCheck
checkTrue={fullName === nationalData?.name_en}
labelClassName="text-sarLinkOrange"
label="Name"
value={fullName}
/>
<InputReadOnly
showCheck
checkTrue={dbUser?.address === nationalData?.house_name_en}
labelClassName="text-sarLinkOrange"
label="House Name"
value={dbUser?.address ?? ""}
/>
<InputReadOnly
showCheck
checkTrue={dbUser?.island?.name === nationalData?.island_name_en}
labelClassName="text-sarLinkOrange"
label="Island"
value={dbUser?.island?.name ?? ""}
/>
<InputReadOnly
showCheck
checkTrue={dbUser?.atoll.name === nationalData?.atoll_en}
labelClassName="text-sarLinkOrange"
label="Atoll"
value={dbUser?.atoll?.name ?? ""}
/>
<InputReadOnly
showCheck
checkTrue={dbUserDob === nationalDob}
labelClassName="text-sarLinkOrange"
label="DOB"
value={new Date(dbUser?.dob ?? "").toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
})}
/>
<InputReadOnly
showCheck
checkTrue={dbUser?.mobile === nationalData?.primary_contact}
labelClassName="text-sarLinkOrange"
label="Phone Number"
value={dbUser?.mobile ?? ""}
/>
</div>
</div>
{
<div id="national-information">
<h4 className="p-2 rounded font-semibold">National Information</h4>
<div className="bg-green-800/10 shadow p-2 rounded-lg border border-dashed border-green-800 space-y-1 my-2 grid grid-cols-1 md:grid-cols-2 gap-2">
<InputReadOnly
showCheck={false}
labelClassName="text-green-500"
label="ID Card"
value={nationalData?.nic ?? ""}
/>
<InputReadOnly
showCheck={false}
labelClassName="text-green-500"
label="Name"
value={nationalData?.name_en ?? ""}
/>
<InputReadOnly
showCheck={false}
labelClassName="text-green-500"
label="House Name"
value={nationalData?.house_name_en ?? ""}
/>
<InputReadOnly
showCheck={false}
labelClassName="text-green-500"
label="Island"
value={nationalData?.island_name_en ?? ""}
/>
<InputReadOnly
showCheck={false}
labelClassName="text-green-500"
label="Atoll"
value={nationalData?.atoll_en ?? ""}
/>
<InputReadOnly
showCheck={false}
labelClassName="text-green-500"
label="DOB"
value={new Date(nationalData?.dob ?? "").toLocaleDateString(
"en-US",
{
month: "short",
day: "2-digit",
year: "numeric",
},
)}
/>
<InputReadOnly
showCheck={false}
labelClassName="text-green-500"
label="Phone Number"
value={nationalData?.primary_contact ?? ""}
/>
<div className="flex flex-col col-span-2 items-center justify-center">
<Image
src={nationalData?.image_url || "https://i.pravatar.cc/300"}
height={100}
width={100}
className="object-fit aspect-square rounded-full"
alt="id photo"
/>
</div>
</div>
</div>
}
</div>
</div>
);
}

View File

@@ -0,0 +1,46 @@
import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/auth";
import ClientErrorMessage from "@/components/client-error-message";
import UserUpdateForm from "@/components/user/user-update-form";
import { getProfileById } from "@/queries/users";
import { tryCatch } from "@/utils/tryCatch";
// import {
// Select,
// SelectContent,
// SelectGroup,
// SelectItem,
// SelectLabel,
// SelectTrigger,
// SelectValue,
// } from "@/components/ui/select";
export default async function UserUpdate({
params,
}: {
params: Promise<{
userId: string;
}>;
}) {
const { userId } = await params;
const session = await getServerSession(authOptions);
if (!session?.user?.is_admin) return redirect("/devices?page=1");
const [error, user] = await tryCatch(getProfileById(userId));
if (error) {
if (error.message === "UNAUTHORIZED") {
redirect("/auth/signin");
} else {
return <ClientErrorMessage message={error.message} />;
}
}
return (
<div>
<div className="flex items-center justify-between text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">Verify user</h3>
</div>
<UserUpdateForm user={user} />
</div>
);
}

View File

@@ -1,169 +0,0 @@
export default async function VerifyUserPage({
params,
}: {
params: Promise<{
userId: string;
}>;
}) {
const userId = (await params).userId;
console.log("userId", userId);
// const dbUser = await prisma.user.findUnique({
// where: {
// id: userId,
// },
// include: {
// island: {
// include: {
// atoll: true
// }
// }
// }
// })
// const nationalData = await getNationalPerson({ idCard: dbUser?.id_card ?? "" })
return null;
// return (
// <div>
// <div className="flex items-center justify-between text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
// <h3 className="text-sarLinkOrange text-2xl">Verify user</h3>
// <div className="flex gap-2">
// {dbUser && !dbUser?.verified && <UserVerifyDialog user={dbUser} />}
// {dbUser && !dbUser?.verified && <UserRejectDialog user={dbUser} />}
// {dbUser?.verified && (
// <Badge variant={"secondary"} className="bg-lime-500">
// Verified
// </Badge>
// )}
// </div>
// </div>
// <div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-start justify-start">
// <div id="database-information">
// <h4 className="p-2 rounded font-semibold">Database Information</h4>
// <div className="shadow p-2 rounded-lg title-bg space-y-1 my-2 grid grid-cols-1 md:grid-cols-2 gap-2">
// <InputReadOnly
// showCheck
// checkTrue={dbUser?.id_card === nationalData.nic}
// labelClassName="text-sarLinkOrange"
// label="ID Card"
// value={dbUser?.id_card ?? ""}
// />
// <InputReadOnly
// showCheck
// checkTrue={dbUser?.name === nationalData.name_en}
// labelClassName="text-sarLinkOrange"
// label="Name"
// value={dbUser?.name ?? ""}
// />
// <InputReadOnly
// showCheck
// checkTrue={dbUser?.address === nationalData.house_name_en}
// labelClassName="text-sarLinkOrange"
// label="House Name"
// value={dbUser?.address ?? ""}
// />
// <InputReadOnly
// showCheck
// checkTrue={dbUser?.island?.name === nationalData.island_name_en}
// labelClassName="text-sarLinkOrange"
// label="Island"
// value={dbUser?.island?.name ?? ""}
// />
// <InputReadOnly
// showCheck
// checkTrue={dbUser?.island?.atoll.name === nationalData.atoll_en}
// labelClassName="text-sarLinkOrange"
// label="Atoll"
// value={dbUser?.island?.atoll.name ?? ""}
// />
// <InputReadOnly
// showCheck
// checkTrue={
// new Date(dbUser?.dob ?? "") === new Date(nationalData.dob)
// }
// labelClassName="text-sarLinkOrange"
// label="DOB"
// value={new Date(dbUser?.dob ?? "").toLocaleDateString("en-US", {
// month: "short",
// day: "2-digit",
// year: "numeric",
// })}
// />
// <InputReadOnly
// showCheck
// checkTrue={dbUser?.phoneNumber === nationalData.primary_contact}
// labelClassName="text-sarLinkOrange"
// label="Phone Number"
// value={dbUser?.phoneNumber ?? ""}
// />
// </div>
// </div>
// <div id="national-information">
// <h4 className="p-2 rounded font-semibold">National Information</h4>
// <div className="shadow p-2 rounded-md title-bg space-y-1 my-2 grid grid-cols-1 md:grid-cols-2 gap-2">
// <InputReadOnly
// showCheck={false}
// labelClassName="text-green-500"
// label="ID Card"
// value={nationalData?.nic ?? ""}
// />
// <InputReadOnly
// showCheck={false}
// labelClassName="text-green-500"
// label="Name"
// value={nationalData?.name_en ?? ""}
// />
// <InputReadOnly
// showCheck={false}
// labelClassName="text-green-500"
// label="House Name"
// value={nationalData?.house_name_en ?? ""}
// />
// <InputReadOnly
// showCheck={false}
// labelClassName="text-green-500"
// label="Island"
// value={nationalData?.island_name_en ?? ""}
// />
// <InputReadOnly
// showCheck={false}
// labelClassName="text-green-500"
// label="Atoll"
// value={nationalData?.atoll_en ?? ""}
// />
// <InputReadOnly
// showCheck={false}
// labelClassName="text-green-500"
// label="DOB"
// value={new Date(nationalData?.dob ?? "").toLocaleDateString(
// "en-US",
// {
// month: "short",
// day: "2-digit",
// year: "numeric",
// },
// )}
// />
// <InputReadOnly
// showCheck={false}
// labelClassName="text-green-500"
// label="Phone Number"
// value={nationalData?.primary_contact ?? ""}
// />
// <div className="flex flex-col col-span-2 items-center justify-center">
// <Image
// src={nationalData.image_url || "https://i.pravatar.cc/300"}
// height={100}
// width={100}
// className="object-fit aspect-square rounded-full"
// alt="id photo"
// />
// </div>
// </div>
// </div>
// </div>
// </div>
// );
}

View File

@@ -1,38 +1,19 @@
import Filter from "@/components/filter"; import { redirect } from "next/navigation";
import Search from "@/components/search"; import { getServerSession } from "next-auth";
import { UsersTable } from "@/components/user-table";
import {
AArrowDown,
AArrowUp,
CheckCheck,
Hourglass,
Minus,
} from "lucide-react";
import { Suspense } from "react"; import { Suspense } from "react";
import { authOptions } from "@/app/auth";
const sortfilterOptions = [ import DynamicFilter from "@/components/generic-filter";
{ import { UsersTable } from "@/components/user-table";
value: "asc",
label: "Ascending",
icon: <AArrowUp size={16} />,
},
{
value: "desc",
label: "Descending",
icon: <AArrowDown size={16} />,
},
];
export default async function AdminUsers({ export default async function AdminUsers({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ searchParams: Promise<{
query: string; [key: string]: string;
page: number;
sortBy: string;
status: string;
}>; }>;
}) { }) {
const session = await getServerSession(authOptions);
if (!session?.user?.is_admin) redirect("/devices?page=1");
return ( return (
<div> <div>
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4"> <div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
@@ -43,33 +24,60 @@ export default async function AdminUsers({
id="user-table-filters" id="user-table-filters"
className=" pb-4 gap-4 flex items-center justify-start" className=" pb-4 gap-4 flex items-center justify-start"
> >
<Search /> <DynamicFilter
<Filter description="Filter users by id card, name, or house name and more."
options={[ title="User Filter"
inputs={[
{ {
value: "all", name: "first_name",
label: "ALL", label: "User First Name",
icon: <Minus size={14} />, type: "string",
placeholder: "Enter user first name",
}, },
{ {
value: "unverified", name: "last_name",
label: "Unverfieid", label: "User Last Name",
icon: <CheckCheck size={14} />, type: "string",
placeholder: "Enter user last name",
}, },
{ {
value: "verified", name: "id_card",
label: "Verified", label: "ID Card",
icon: <Hourglass size={14} />, type: "string",
placeholder: "Enter ID card number",
},
{
name: "house_name",
label: "House Name",
type: "string",
placeholder: "Enter house name",
},
{
name: "mobile",
label: "Phone Number",
type: "string",
placeholder: "Enter phone number",
},
{
name: "verified",
type: "radio-group",
label: "User Status",
options: [
{
value: "",
label: "All",
},
{
label: "Verified",
value: "true",
},
{
label: "Unverified",
value: "false",
},
],
}, },
]} ]}
defaultOption="all"
queryParamKey="status"
/>
<Filter
options={sortfilterOptions}
defaultOption="asc"
queryParamKey="sortBy"
/> />
</div> </div>
<Suspense fallback={"loading...."}> <Suspense fallback={"loading...."}>

View File

@@ -1,13 +1,63 @@
import React from 'react' import { Suspense } from "react";
import DynamicFilter from "@/components/generic-filter";
import { WalletTransactionsTable } from "@/components/wallet-transactions-table";
export default async function Wallet({
searchParams,
}: {
searchParams: Promise<{
query: string;
page: number;
sortBy: string;
status: string;
}>;
}) {
const query = (await searchParams)?.query || "";
export default function UserWallet() {
return ( return (
<div> <div>
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4"> <div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl"> <h3 className="text-sarLinkOrange text-2xl">Transaction History</h3>
My Wallet
</h3>
</div> </div>
<div
id="wallet-filters"
className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
>
<DynamicFilter
inputs={[
{
label: "Type",
name: "transaction_type",
type: "radio-group",
options: [
{
label: "All",
value: "",
},
{
label: "Debit",
value: "debit",
},
{
label: "Credit",
value: "credit",
},
],
},
{
label: "Topup Amount",
name: "amount",
type: "dual-range-slider",
min: 0,
max: 1000,
step: 10,
},
]}
/>
</div>
<Suspense key={query} fallback={"loading...."}>
<WalletTransactionsTable searchParams={searchParams} />
</Suspense>
</div> </div>
) );
} }

File diff suppressed because one or more lines are too long

View File

@@ -11,49 +11,48 @@ import QueryProvider from "@/providers/query-provider";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { authOptions } from "./auth"; import { authOptions } from "./auth";
const barlow = Barlow({ const barlow = Barlow({
subsets: ["latin"], subsets: ["latin"],
weight: ["100", "300", "400", "500", "600", "700", "800", "900"], weight: ["100", "300", "400", "500", "600", "700", "800", "900"],
variable: "--font-barlow", variable: "--font-barlow",
}); });
const bokor = Bokor({ const bokor = Bokor({
subsets: ["latin"], subsets: ["latin"],
weight: ["400"], weight: ["400"],
variable: "--font-bokor", variable: "--font-bokor",
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "SAR Link Portal", title: "SAR Link Portal",
description: "Sarlink Portal", description: "Sarlink Portal",
}; };
export default async function RootLayout({ export default async function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body className={`${barlow.variable} ${bokor.variable} antialiased font-sans bg-gray-100 dark:bg-black`}> <body
<AuthProvider session={session || undefined}> className={`${barlow.variable} ${bokor.variable} antialiased font-sans bg-gray-100 dark:bg-black`}
<Provider> >
<NextTopLoader color="#f49d1b" showSpinner={false} zIndex={9999} /> <AuthProvider session={session || undefined}>
<Toaster richColors /> <Provider>
<ThemeProvider <NextTopLoader color="#f49d1b" showSpinner={false} zIndex={9999} />
attribute="class" <Toaster richColors />
defaultTheme="system" <ThemeProvider
enableSystem attribute="class"
disableTransitionOnChange defaultTheme="system"
> enableSystem
<QueryProvider> disableTransitionOnChange
{children} >
</QueryProvider> <QueryProvider>{children}</QueryProvider>
</ThemeProvider> </ThemeProvider>
</Provider> </Provider>
</AuthProvider> </AuthProvider>
</body> </body>
</html>
</html> );
);
} }

4
app/next-auth.d.ts vendored
View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
// @ts-expect-error importing unused types are required here // @ts-expect-error importing unused types are required here
import NextAuth, { DefaultSession, type User, Session } from "next-auth"; import NextAuth, { DefaultSession, Session, type User } from "next-auth";
/* eslint-enable @typescript-eslint/no-unused-vars */ /* eslint-enable @typescript-eslint/no-unused-vars */
declare module "next-auth" { declare module "next-auth" {
/** /**
@@ -26,6 +27,7 @@ declare module "next-auth" {
date_joined?: string; date_joined?: string;
is_superuser?: boolean; is_superuser?: boolean;
is_admin?: boolean; is_admin?: boolean;
agreement?: string;
}; };
expires: ISODateString; expires: ISODateString;
} }

View File

@@ -1,5 +1,5 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
export default async function Home() { export default async function Home() {
return redirect("/devices"); return redirect("/devices");
} }

3
commitlint.config.js Normal file
View File

@@ -0,0 +1,3 @@
export default {
extends: ['@commitlint/config-conventional']
};

View File

@@ -1,21 +1,21 @@
{ {
"$schema": "https://ui.shadcn.com/schema.json", "$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york", "style": "new-york",
"rsc": true, "rsc": true,
"tsx": true, "tsx": true,
"tailwind": { "tailwind": {
"config": "tailwind.config.ts", "config": "tailwind.config.ts",
"css": "app/globals.css", "css": "app/globals.css",
"baseColor": "neutral", "baseColor": "neutral",
"cssVariables": true, "cssVariables": true,
"prefix": "" "prefix": ""
}, },
"aliases": { "aliases": {
"components": "@/components", "components": "@/components",
"utils": "@/lib/utils", "utils": "@/lib/utils",
"ui": "@/components/ui", "ui": "@/components/ui",
"lib": "@/lib", "lib": "@/lib",
"hooks": "@/hooks" "hooks": "@/hooks"
}, },
"iconLibrary": "lucide" "iconLibrary": "lucide"
} }

View File

@@ -0,0 +1,56 @@
"use client";
import { Clipboard, ClipboardCheck } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "./ui/button";
export function AccountInfomation({
accountNo,
accName,
}: {
accountNo: string;
accName: string;
}) {
const [accNo, setAccNo] = useState(false);
return (
<div className="justify-center items-center border my-4 flex flex-col gap-2 p-2 rounded-md">
<h6 className="title-bg uppercase p-2 border rounded w-full font-semibold">
Account Information
</h6>
<div className="border justify-center flex flex-col items-center bg-white/10 w-full p-2 rounded">
<div className="text-sm font-semibold">Account Name</div>
<span>{accName}</span>
</div>
<div className="border flex justify-between items-center gap-2 w-full p-2 rounded">
<div className="flex flex-col items-center justify-center w-full">
<p className="text-sm font-semibold">Account No</p>
<span>{accountNo}</span>
<Button
onClick={() => {
setTimeout(() => {
setAccNo(true);
navigator.clipboard.writeText(accountNo);
}, 2000);
toast.success("Account number copied!");
setAccNo((prev) => !prev);
}}
className="mt-2 w-full"
variant={"secondary"}
>
{accNo ? (
<div className="flex items-center gap-2">
<span>Copy Account Number</span>
<Clipboard />
</div>
) : (
<div className="flex items-center gap-2">
<span>Copy Account Number</span>
<ClipboardCheck color="green" />
</div>
)}
</Button>
</div>
</div>
</div>
);
}

View File

@@ -4,185 +4,171 @@ import { redirect } from "next/navigation";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { authOptions } from "@/app/auth"; import { authOptions } from "@/app/auth";
import { import {
Table, Table,
TableBody, TableBody,
TableCaption, TableCell,
TableCell, TableFooter,
TableFooter, TableHead,
TableHead, TableHeader,
TableHeader, TableRow,
TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { getDevices } from "@/queries/devices"; import { getDevices } from "@/queries/devices";
import { tryCatch } from "@/utils/tryCatch"; import { tryCatch } from "@/utils/tryCatch";
import BlockDeviceDialog from "../block-device-dialog"; import BlockDeviceDialog from "../block-device-dialog";
import ClientErrorMessage from "../client-error-message"; import ClientErrorMessage from "../client-error-message";
import DeviceCard from "../device-card";
import Pagination from "../pagination"; import Pagination from "../pagination";
export async function AdminDevicesTable({ export async function AdminDevicesTable({
searchParams, searchParams,
parentalControl,
}: { }: {
searchParams: Promise<{ searchParams: Promise<{
[key: string]: unknown; [key: string]: unknown;
}>; }>;
parentalControl?: boolean;
}) { }) {
const resolvedParams = await searchParams; const resolvedParams = await searchParams;
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
const isAdmin = session?.user?.is_superuser; const isAdmin = session?.user?.is_admin;
const page = Number.parseInt(resolvedParams.page as string) || 1; const page = Number.parseInt(resolvedParams.page as string) || 1;
const limit = 10; const limit = 10;
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
// Build params object for getDevices // Build params object for getDevices
const apiParams: Record<string, string | number | undefined> = {}; const apiParams: Record<string, string | number | undefined> = {};
for (const [key, value] of Object.entries(resolvedParams)) { for (const [key, value] of Object.entries(resolvedParams)) {
if (value !== undefined && value !== "") { if (value !== undefined && value !== "") {
apiParams[key] = typeof value === "number" ? value : String(value); apiParams[key] = typeof value === "number" ? value : String(value);
} }
} }
apiParams.limit = limit; apiParams.limit = limit;
apiParams.offset = offset; apiParams.offset = offset;
const [error, devices] = await tryCatch( const [error, devices] = await tryCatch(getDevices(apiParams, true));
getDevices(apiParams, true), if (error) {
); if (error.message === "UNAUTHORIZED") {
if (error) { redirect("/auth/signin");
if (error.message === "UNAUTHORIZED") { } else {
redirect("/auth/signin"); return <ClientErrorMessage message={error.message} />;
} else { }
return <ClientErrorMessage message={error.message} />; }
} const { meta, data } = devices;
} return (
const { meta, data } = devices; <div>
return ( {data?.length === 0 ? (
<div> <div className="h-[calc(100svh-400px)] text-muted-foreground flex flex-col items-center justify-center my-4">
{data?.length === 0 ? ( <h3>No devices.</h3>
<div className="h-[calc(100svh-400px)] text-muted-foreground flex flex-col items-center justify-center my-4"> </div>
<h3>No devices.</h3> ) : (
</div> <>
) : ( <div>
<> <Table className="overflow-scroll">
<div className="hidden sm:block"> <TableHeader>
<Table className="overflow-scroll"> <TableRow>
<TableCaption>Table of all devices.</TableCaption> <TableHead>Device Name</TableHead>
<TableHeader> <TableHead>User</TableHead>
<TableRow> <TableHead>MAC Address</TableHead>
<TableHead>Device Name</TableHead> <TableHead>Vendor</TableHead>
<TableHead>User</TableHead> <TableHead>#</TableHead>
<TableHead>MAC Address</TableHead> </TableRow>
<TableHead>Vendor</TableHead> </TableHeader>
<TableHead>#</TableHead> <TableBody className="overflow-scroll">
</TableRow> {data?.map((device) => (
</TableHeader> <TableRow key={device.id}>
<TableBody className="overflow-scroll"> <TableCell>
{data?.map((device) => ( <div className="flex flex-col items-start">
<TableRow <Link
key={device.id} className={cn(
> "hover:underline font-semibold",
<TableCell> device.is_active ? "text-green-600" : "",
<div className="flex flex-col items-start"> )}
<Link href={`/devices/${device.id}`}
className={cn( >
"hover:underline font-semibold", {device.name}
device.is_active ? "text-green-600" : "", </Link>
)} {device.is_active ? (
href={`/devices/${device.id}`} <div className="text-muted-foreground">
> Active until{" "}
{device.name} <span className="font-semibold">
</Link> {new Date(
{device.is_active ? ( device.expiry_date || "",
<div className="text-muted-foreground"> ).toLocaleDateString("en-US", {
Active until{" "} month: "short",
<span className="font-semibold"> day: "2-digit",
{new Date(device.expiry_date || "").toLocaleDateString( year: "numeric",
"en-US", })}
{ </span>
month: "short", </div>
day: "2-digit", ) : (
year: "numeric", <p className="text-muted-foreground">
}, Device Inactive
)} </p>
</span> )}
</div> {device.has_a_pending_payment && (
) : ( <Link href={`/payments/${device.pending_payment_id}`}>
<p className="text-muted-foreground">Device Inactive</p> <span className="bg-muted rounded px-2 p-1 mt-2 flex hover:underline items-center justify-center gap-2 text-muted-foreground">
)} Payment Pending{" "}
{device.has_a_pending_payment && ( <HandCoins className="animate-pulse" size={14} />
<Link href={`/payments/${device.pending_payment_id}`}> </span>
<span className="bg-muted rounded px-2 p-1 mt-2 flex hover:underline items-center justify-center gap-2 text-muted-foreground"> </Link>
Payment Pending{" "} )}
<HandCoins className="animate-pulse" size={14} />
</span>
</Link>
)}
{device.blocked_by === "ADMIN" && device.blocked && ( {device.blocked_by === "ADMIN" && device.blocked && (
<div className="p-2 rounded border my-2 bg-white dark:bg-neutral-800 shadow"> <div className="p-2 rounded border my-2 bg-white dark:bg-neutral-800 shadow">
<span className="font-semibold">Comment</span> <span className="font-semibold">Comment</span>
<p className="text-neutral-400">{device?.reason_for_blocking}</p> <p className="text-neutral-400">
</div> {device?.reason_for_blocking}
)} </p>
</div> </div>
</TableCell> )}
<TableCell className="font-medium"> </div>
<div className="flex flex-col items-start"> </TableCell>
{device?.user?.name} <TableCell className="font-medium">
<span className="text-muted-foreground">{device?.user?.id_card}</span> <div className="flex flex-col items-start">
</div> {device?.user?.name}
<span className="text-muted-foreground">
{device?.user?.id_card}
</span>
</div>
</TableCell>
<TableCell className="font-medium">{device.mac}</TableCell>
<TableCell className="font-medium">
{device?.vendor}
</TableCell>
<TableCell>
{!device.has_a_pending_payment && (
<BlockDeviceDialog
admin={isAdmin}
type={device.blocked ? "unblock" : "block"}
device={device}
/>
)}
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={5} className="text-muted-foreground">
{meta?.total === 1 ? (
<p className="text-center">Total {meta?.total} device.</p>
) : (
<p className="text-center">
Total {meta?.total} devices.
</p>
)}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
</TableCell> <Pagination
<TableCell className="font-medium">{device.mac}</TableCell> totalPages={meta?.last_page}
<TableCell className="font-medium">{device?.vendor}</TableCell> currentPage={meta?.current_page}
<TableCell> />
{!device.has_a_pending_payment && ( </>
<BlockDeviceDialog )}
admin={isAdmin} </div>
type={device.blocked ? "unblock" : "block"} );
device={device}
/>
)}
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={5} className="text-muted-foreground">
{meta?.total === 1 ? (
<p className="text-center">
Total {meta?.total} device.
</p>
) : (
<p className="text-center">
Total {meta?.total} devices.
</p>
)}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
<div className="sm:hidden my-4">
{data?.map((device) => (
<DeviceCard
parentalControl={parentalControl}
key={device.id}
device={device}
/>
))}
</div>
<Pagination
totalPages={meta?.last_page}
currentPage={meta?.current_page}
/>
</>
)
}
</div >
);
} }

View File

@@ -0,0 +1,131 @@
"use client";
import { Loader2, PlusCircle } from "lucide-react";
import { useRouter } from "next/navigation";
import { useActionState, useEffect, useState } from "react"; // Import useActionState
import { toast } from "sonner";
import { adminUserTopup } from "@/actions/user-actions";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "../ui/textarea";
export type AddTopupFormState = {
status: boolean;
message: string;
fieldErrors?: {
amount?: string[];
description?: string[];
};
payload?: FormData;
};
export const initialState: AddTopupFormState = {
message: "",
fieldErrors: {},
status: false,
};
export default function AddTopupDialogForm({ user_id }: { user_id?: string }) {
const [open, setOpen] = useState(false);
const router = useRouter();
const [state, formAction, pending] = useActionState(
adminUserTopup,
initialState,
);
useEffect(() => {
if (state.message && state !== initialState) {
if (state.fieldErrors && Object.keys(state.fieldErrors).length > 0) {
toast.error(state.message);
} else if (state.status) {
setOpen(false);
toast.success(state.message);
router.push("/user-topups?page=1");
} else {
toast.error(state.message);
}
}
}, [state, router]);
if (!user_id) {
return null;
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
className="gap-2 items-center"
disabled={pending}
variant="default"
>
Add cash topup
<PlusCircle size={16} />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>New Manual Topup</DialogTitle>
<DialogDescription>
To add a new manual topup, enter the amount below. Click save when
you are done.
</DialogDescription>
</DialogHeader>
<form action={formAction}>
<div className="grid gap-4 py-4">
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-3">
<Label htmlFor="amount">Topup Amount</Label>
<input type="hidden" name="user_id" value={user_id} />
<Input
type="number"
defaultValue={(state?.payload?.get("amount") || "") as string}
name="amount"
id="topup_amount"
className="col-span-3"
/>
{state.fieldErrors?.amount && (
<span className="text-red-500 text-sm">
{state.fieldErrors.amount[0]}
</span>
)}
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="description">Topup Description</Label>
<input type="hidden" name="user_id" value={user_id} />
<Textarea
defaultValue={(state?.payload?.get("description") || "") as string}
rows={10}
name="description"
id="topup_description"
className="col-span-5 max-w-[375px] h-32 resize-none"
style={{ overflowY: "auto" }}
/>
{state.fieldErrors?.description && (
<span className="text-red-500 text-sm">
{state.fieldErrors.description[0]}
</span>
)}
</div>
</div>
</div>
<DialogFooter>
<Button disabled={pending} type="submit">
{pending ? <Loader2 className="animate-spin" /> : "Save"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,137 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { getTopups } from "@/actions/payment";
import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { tryCatch } from "@/utils/tryCatch";
import Pagination from "../pagination";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
export async function AdminTopupsTable({
searchParams,
}: {
searchParams: Promise<{
[key: string]: unknown;
}>;
}) {
const resolvedParams = await searchParams;
const page = Number.parseInt(resolvedParams.page as string) || 1;
const limit = 10;
const offset = (page - 1) * limit;
// Build params object
const apiParams: Record<string, string | number | undefined> = {};
for (const [key, value] of Object.entries(resolvedParams)) {
if (value !== undefined && value !== "") {
apiParams[key] = typeof value === "number" ? value : String(value);
}
}
apiParams.limit = limit;
apiParams.offset = offset;
const [error, topups] = await tryCatch(getTopups(apiParams, true));
if (error) {
if (error.message.includes("Unauthorized")) {
redirect("/auth/signin");
} else {
return <pre>{JSON.stringify(error, null, 2)}</pre>;
}
}
const { data, meta } = topups;
return (
<div>
{data?.length === 0 ? (
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
<h3>No topups yet.</h3>
</div>
) : (
<>
<div>
<Table className="overflow-scroll">
<TableHeader>
<TableRow>
<TableHead>User</TableHead>
<TableHead>Status</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Action</TableHead>
</TableRow>
</TableHeader>
<TableBody className="overflow-scroll">
{topups?.data?.map((topup) => (
<TableRow key={topup.id}>
<TableCell>
<div className="flex flex-col items-start">
{topup?.user?.name}
<span className="text-muted-foreground">
{topup?.user?.id_card}
</span>
</div>
</TableCell>
<TableCell>
<span className="font-semibold pr-2">
{topup.paid ? (
<Badge
className="bg-green-100 dark:bg-green-700"
variant="outline"
>
{topup.status}
</Badge>
) : topup.is_expired ? (
<Badge>Expired</Badge>
) : (
<Badge variant="outline">{topup.status}</Badge>
)}
</span>
</TableCell>
<TableCell>
<span className="font-semibold pr-2">
{topup.amount.toFixed(2)}
</span>
MVR
</TableCell>
<TableCell>
<div>
<div className="flex items-center gap-2 mt-2">
<Link
className="font-medium hover:underline"
href={`/top-ups/${topup.id}`}
>
<Button size={"sm"} variant="outline">
View Details
</Button>
</Link>
</div>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={4} className="text-muted-foreground">
{meta?.total === 1 ? (
<p className="text-center">Total {meta?.total} topup.</p>
) : (
<p className="text-center">Total {meta?.total} topups.</p>
)}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
<Pagination
totalPages={meta?.last_page}
currentPage={meta?.current_page}
/>
</>
)}
</div>
);
}

View File

@@ -1,236 +1,172 @@
// import Pagination from "@/components/pagination"; import Link from "next/link";
// import { Badge } from "@/components/ui/badge"; import { redirect } from "next/navigation";
// import { Button } from "@/components/ui/button"; import { getPayments } from "@/actions/payment";
// import { import Pagination from "@/components/pagination";
// Table, import { Badge } from "@/components/ui/badge";
// TableBody, import { Button } from "@/components/ui/button";
// TableCaption, import {
// TableCell, Table,
// TableFooter, TableBody,
// TableHead, TableCell,
// TableHeader, TableFooter,
// TableRow, TableHead,
// } from "@/components/ui/table"; TableHeader,
// import Link from "next/link"; TableRow,
} from "@/components/ui/table";
import { tryCatch } from "@/utils/tryCatch";
import ClientErrorMessage from "../client-error-message";
export async function UsersPaymentsTable({ export async function UsersPaymentsTable({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ searchParams: Promise<{
query: string; [key: string]: unknown;
page: number; }>;
sortBy: string;
status: string;
}>;
}) { }) {
const query = (await searchParams)?.query || ""; const resolvedParams = await searchParams;
console.log(query);
// const page = (await searchParams)?.page;
// const sortBy = (await searchParams)?.sortBy || "asc";
// const totalPayments = await prisma.payment.count({
// where: {
// OR: [
// {
// user: {
// name: {
// contains: query || "",
// mode: "insensitive",
// }
// },
// },
// {
// user: {
// phoneNumber: {
// contains: query || "",
// mode: "insensitive",
// }
// },
// },
// {
// user: {
// address: {
// contains: query || "",
// mode: "insensitive",
// }
// },
// },
// {
// user: {
// id_card: {
// contains: query || "",
// mode: "insensitive",
// }
// },
// },
// ],
// },
// });
// const totalPages = Math.ceil(totalPayments / 10); const page = Number.parseInt(resolvedParams.page as string) || 1;
// const limit = 10; const limit = 10;
// const offset = (Number(page) - 1) * limit || 0; const offset = (page - 1) * limit;
// const payments = await prisma.payment.findMany({ // Build params object for getDevices
// where: { const apiParams: Record<string, string | number | undefined> = {};
// OR: [ for (const [key, value] of Object.entries(resolvedParams)) {
// { if (value !== undefined && value !== "") {
// user: { apiParams[key] = typeof value === "number" ? value : String(value);
// name: { }
// contains: query || "", }
// mode: "insensitive", apiParams.limit = limit;
// } apiParams.offset = offset;
// },
// },
// {
// user: {
// phoneNumber: {
// contains: query || "",
// mode: "insensitive",
// }
// },
// },
// {
// user: {
// address: {
// contains: query || "",
// mode: "insensitive",
// }
// },
// },
// {
// user: {
// id_card: {
// contains: query || "",
// mode: "insensitive",
// }
// },
// },
// ],
// },
// include: {
// user: true,
// devices: true,
// },
// skip: offset,
// take: limit,
// orderBy: {
// id: `${sortBy}` as "asc" | "desc",
// },
// });
// const users = await prisma.user.findMany({ const [error, payments] = await tryCatch(getPayments(apiParams, true));
// where: { if (error) {
// role: "USER", if (error.message === "UNAUTHORIZED") {
// }, redirect("/auth/signin");
// include: { } else {
// atoll: true, return <ClientErrorMessage message={error.message} />;
// island: true, }
// }, }
// }); const { meta, data } = payments;
return null;
// return (
// <div>
// {payments.length === 0 ? (
// <div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
// <h3>No user payments yet.</h3>
// </div>
// ) : (
// <>
// <Table className="overflow-scroll">
// <TableCaption>Table of all users.</TableCaption>
// <TableHeader>
// <TableRow>
// <TableHead>Devices paid</TableHead>
// <TableHead>User</TableHead>
// <TableHead>Amount</TableHead>
// <TableHead>Duration</TableHead>
// <TableHead>Payment Status</TableHead>
// <TableHead>Payment Method</TableHead>
// <TableHead>Paid At</TableHead>
// <TableHead>Action</TableHead>
// </TableRow>
// </TableHeader>
// <TableBody className="overflow-scroll">
// {payments.map((payment) => (
// <TableRow
// className={`${payment.paid && "title-bg dark:bg-black"}`}
// key={payment.id}
// >
// <TableCell className="font-medium">
// <ol className="list-disc list-inside text-sm">
// {payment.devices.map((device) => (
// <li
// key={device.id}
// className="text-sm text-muted-foreground"
// >
// {device.name}
// </li>
// ))}
// </ol>
// </TableCell>
// <TableCell className="font-medium">
// {payment.user.id_card}
// </TableCell>
// <TableCell>{payment.user?.name}</TableCell>
// <TableCell>{payment.user?.name}</TableCell>
// <TableCell>{payment.id}</TableCell>
// <TableCell> // return <pre>{JSON.stringify(payments, null, 2)}</pre>;
// {payment.paid ? ( return (
// <Badge <div>
// variant="outline" {data.length === 0 ? (
// className="bg-lime-100 text-black" <div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
// > <h3>No user payments yet.</h3>
// Verified </div>
// </Badge> ) : (
// ) : ( <>
// <Badge <Table className="overflow-scroll">
// variant="outline" <TableHeader>
// className="bg-yellow-100 text-black" <TableRow>
// > <TableHead>Devices paid</TableHead>
// Unverified <TableHead>User</TableHead>
// </Badge> <TableHead>Amount</TableHead>
// )} <TableHead>Duration</TableHead>
// </TableCell> <TableHead>Payment Status</TableHead>
// <TableCell> <TableHead>Payment Method</TableHead>
// {new Date(payment.paidAt ?? "").toLocaleDateString( <TableHead>MIB Reference</TableHead>
// "en-US", <TableHead>Paid At</TableHead>
// { <TableHead>Action</TableHead>
// month: "short", </TableRow>
// day: "2-digit", </TableHeader>
// year: "numeric", <TableBody className="overflow-scroll">
// }, {data.map((payment) => (
// )} <TableRow
// </TableCell> className={`${payment.paid && "title-bg dark:bg-black"}`}
key={payment.id}
>
<TableCell className="font-medium">
<ol className="list-disc list-inside text-sm">
{payment.devices.map((device) => (
<li
key={device.id}
className="text-sm text-muted-foreground"
>
{device.name}
</li>
))}
</ol>
</TableCell>
<TableCell className="font-medium">
{/* {payment.user.id_card} */}
<div className="flex flex-col items-start">
{payment?.user?.name}
<span className="text-muted-foreground">
{payment?.user?.id_card}
</span>
</div>{" "}
</TableCell>
<TableCell>{payment.amount} MVR</TableCell>
<TableCell>{payment.number_of_months} Months</TableCell>
// <TableCell>{payment.id}</TableCell> <TableCell>
// <TableCell> {payment.status === "PENDING" ? (
// <Link href={`/payments/${payment.id}/verify`}> <Badge
// <Button>Details</Button> variant="outline"
// </Link> className="bg-yellow-100 text-black"
// </TableCell> >
// </TableRow> {payment.status}
// ))} </Badge>
// </TableBody> ) : payment.status === "PAID" ? (
// <TableFooter> <Badge
// <TableRow> variant="outline"
// <TableCell colSpan={8}> className="bg-lime-100 text-black"
// {query.length > 0 && ( >
// <p className="text-sm text-muted-foreground"> {payment.status}
// Showing {payments.length} locations for &quot;{query} </Badge>
// &quot; ) : (
// </p> <Badge
// )} variant="outline"
// </TableCell> className="bg-red-100 text-black"
// <TableCell className="text-muted-foreground"> >
// {totalPayments} payments {payment.status}
// </TableCell> </Badge>
// </TableRow> )}
// </TableFooter> </TableCell>
// </Table> <TableCell>{payment.method}</TableCell>
// <Pagination totalPages={totalPages} currentPage={page} /> <TableCell>{payment.mib_reference}</TableCell>
// </> <TableCell>
// )} {new Date(payment.paid_at ?? "").toLocaleDateString(
// </div> "en-US",
// ); {
month: "short",
day: "2-digit",
year: "numeric",
minute: "2-digit",
hour: "2-digit",
},
)}
</TableCell>
<TableCell>
<Link href={`/payments/${payment.id}`}>
<Button>Details</Button>
</Link>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={10} className="text-muted-foreground">
{meta?.total === 1 ? (
<p className="text-center">Total {meta?.total} payment.</p>
) : (
<p className="text-center">Total {meta?.total} payments.</p>
)}{" "}
</TableCell>
</TableRow>
</TableFooter>
</Table>
<Pagination
totalPages={meta?.last_page}
currentPage={meta?.current_page}
/>
</>
)}
</div>
);
} }

View File

@@ -0,0 +1,30 @@
import { EyeIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle
} from "@/components/ui/card"
export function AgreementCard({ agreement }: { agreement: string }) {
return (
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Sarlink User Agreement</CardTitle>
<CardDescription>
User agreement for Sarlink services.
</CardDescription>
</CardHeader>
<CardFooter className="flex-col gap-2">
<a target="_blank" rel="noopener noreferrer" className="w-full hover:cursor-pointer" href={agreement}>
<Button type="button" className="w-full hover:cursor-pointer">
<EyeIcon />
View Agreement
</Button>
</a>
</CardFooter>
</Card>
)
}

View File

@@ -1,13 +1,14 @@
"use client"; "use client";
import { Loader2, User as UserIcon } from "lucide-react";
import Link from "next/link";
import { signOut, useSession } from "next-auth/react";
import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import { Loader2, User as UserIcon } from "lucide-react";
import { signOut, useSession } from "next-auth/react";
import { useState } from "react";
export function AccountPopover() { export function AccountPopover() {
const session = useSession(); const session = useSession();
@@ -36,16 +37,23 @@ export function AccountPopover() {
<p>{session.data?.user?.mobile}</p> <p>{session.data?.user?.mobile}</p>
</div> </div>
</div> </div>
<Button <div className="flex flex-col gap-2">
disabled={loading} <Button
onClick={async () => { disabled={loading}
setLoading(true); onClick={async () => {
await signOut(); setLoading(true);
setLoading(false); await signOut();
}} setLoading(false);
> }}
{loading ? <Loader2 className="animate-spin" /> : "Logout"} >
</Button> {loading ? <Loader2 className="animate-spin" /> : "Logout"}
</Button>
<Link href="/profile" className="text-muted-foreground">
<Button variant={"secondary"} className="w-full">
View Profile
</Button>
</Link>
</div>
</div> </div>
</PopoverContent> </PopoverContent>
</Popover> </Popover>

View File

@@ -1,7 +1,7 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { NuqsAdapter } from 'nuqs/adapters/next/app' import { NuqsAdapter } from "nuqs/adapters/next/app";
import { getProfile } from "@/actions/payment"; import { getProfile } from "@/actions/user-actions";
import { authOptions } from "@/app/auth"; import { authOptions } from "@/app/auth";
import { DeviceCartDrawer } from "@/components/device-cart"; import { DeviceCartDrawer } from "@/components/device-cart";
import { ModeToggle } from "@/components/theme-toggle"; import { ModeToggle } from "@/components/theme-toggle";
@@ -19,7 +19,9 @@ import { AccountPopover } from "./account-popver";
export async function ApplicationLayout({ export async function ApplicationLayout({
children, children,
}: { children: React.ReactNode }) { }: {
children: React.ReactNode;
}) {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session) return redirect("/auth/signin"); if (!session) return redirect("/auth/signin");
@@ -50,9 +52,7 @@ export async function ApplicationLayout({
/> />
<DeviceCartDrawer /> <DeviceCartDrawer />
<div className="p-4 flex flex-col flex-1 rounded-lg bg-background"> <div className="p-4 flex flex-col flex-1 rounded-lg bg-background">
<NuqsAdapter> <NuqsAdapter>{children}</NuqsAdapter>
{children}
</NuqsAdapter>
</div> </div>
</SidebarInset> </SidebarInset>
</SidebarProvider> </SidebarProvider>

View File

@@ -1,14 +1,13 @@
"use client"; "use client";
import { Button } from "@/components/ui/button"; import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { Input } from "@/components/ui/input";
import Link from "next/link";
import { signup } from "@/actions/auth-actions";
import { cn } from "@/lib/utils";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import * as React from "react"; import * as React from "react";
import { signup } from "@/actions/auth-actions";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -19,9 +18,8 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import type { ApiResponse, Atoll } from "@/lib/backend-types"; import type { ApiResponse, Atoll } from "@/lib/backend-types";
import { cn } from "@/lib/utils";
import { getAtolls } from "@/queries/islands"; import { getAtolls } from "@/queries/islands";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import Image from "next/image";
export default function SignUpForm() { export default function SignUpForm() {
const { data: atolls } = useQuery<ApiResponse<Atoll>>({ const { data: atolls } = useQuery<ApiResponse<Atoll>>({
@@ -59,7 +57,8 @@ export default function SignUpForm() {
<Link href="login" className="underline"> <Link href="login" className="underline">
login login
</Link> </Link>
</div>- </div>
-
</> </>
); );
} }
@@ -72,12 +71,20 @@ export default function SignUpForm() {
{/* Logo */} {/* Logo */}
<div className="mb-8"> <div className="mb-8">
<div className="w-20 h-20 bg-transparent backdrop-blur-sm rounded-2xl flex items-center justify-center mx-auto mb-4"> <div className="w-20 h-20 bg-transparent backdrop-blur-sm rounded-2xl flex items-center justify-center mx-auto mb-4">
<Image src="/logo.png" alt="Company Logo" height={1080} width={1080} className="w-12 h-12 text-white" /> <Image
src="/logo.png"
alt="Company Logo"
height={1080}
width={1080}
className="w-12 h-12 text-white"
/>
</div> </div>
<h3 className="text-xl font-semibold dark:text-orange-100">SAR Link Portal</h3> <h3 className="text-xl font-semibold dark:text-orange-100">
SAR Link by OmegaTech Solutions
</h3>
</div> </div>
<h1 className="text-4xl font-bold mb-6">Welcome to Our Platform</h1> <h1 className="text-4xl font-bold mb-6">Register your account</h1>
<p className="text-xl mb-8 dark:text-orange-100"> <p className="text-xl mb-8 dark:text-orange-100">
Pay for your devices and track your bills. Pay for your devices and track your bills.
</p> </p>
@@ -92,7 +99,10 @@ export default function SignUpForm() {
<div className="max-w-sm shadow-2xl shadow-sarLinkOrange/20 h-fit my-auto mt-2 w-full bg-white dark:bg-transparent rounded-lg m-auto backdrop-blur-lg self-center border-2 border-sarLinkOrange/10 dark:border-sarLinkOrange/50"> <div className="max-w-sm shadow-2xl shadow-sarLinkOrange/20 h-fit my-auto mt-2 w-full bg-white dark:bg-transparent rounded-lg m-auto backdrop-blur-lg self-center border-2 border-sarLinkOrange/10 dark:border-sarLinkOrange/50">
<div className="py-2 px-4 my-2 space-y-2"> <div className="py-2 px-4 my-2 space-y-2">
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="name" className="text-sm font-medium text-slate-700 dark:text-slate-300"> <label
htmlFor="name"
className="text-sm font-medium text-slate-700 dark:text-slate-300"
>
Full Name Full Name
</label> </label>
@@ -105,7 +115,9 @@ export default function SignUpForm() {
name="name" name="name"
type="text" type="text"
disabled={isPending} disabled={isPending}
defaultValue={(actionState?.payload?.get("name") || "") as string} defaultValue={
(actionState?.payload?.get("name") || "") as string
}
placeholder="Full Name" placeholder="Full Name"
/> />
{actionState?.errors?.fieldErrors.name && ( {actionState?.errors?.fieldErrors.name && (
@@ -115,7 +127,10 @@ export default function SignUpForm() {
)} )}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="id_card" className="text-sm font-medium text-slate-700 dark:text-slate-300"> <label
htmlFor="id_card"
className="text-sm font-medium text-slate-700 dark:text-slate-300"
>
ID Card ID Card
</label> </label>
<Input <Input
@@ -146,7 +161,10 @@ export default function SignUpForm() {
</div> </div>
<div> <div>
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="atoll" className="text-sm font-medium text-slate-700 dark:text-slate-300"> <label
htmlFor="atoll"
className="text-sm font-medium text-slate-700 dark:text-slate-300"
>
Atoll Atoll
</label> </label>
<Select <Select
@@ -154,7 +172,9 @@ export default function SignUpForm() {
onValueChange={(v) => { onValueChange={(v) => {
console.log({ v }); console.log({ v });
setAtoll( setAtoll(
atolls?.data.find((atoll) => atoll.id === Number.parseInt(v)), atolls?.data.find(
(atoll) => atoll.id === Number.parseInt(v),
),
); );
}} }}
name="atoll_id" name="atoll_id"
@@ -181,7 +201,10 @@ export default function SignUpForm() {
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="island" className="text-sm font-medium text-slate-700 dark:text-slate-300"> <label
htmlFor="island"
className="text-sm font-medium text-slate-700 dark:text-slate-300"
>
Island Island
</label> </label>
<Select disabled={isPending} name="island_id"> <Select disabled={isPending} name="island_id">
@@ -192,7 +215,10 @@ export default function SignUpForm() {
<SelectGroup> <SelectGroup>
<SelectLabel>Islands</SelectLabel> <SelectLabel>Islands</SelectLabel>
{atoll?.islands?.map((island) => ( {atoll?.islands?.map((island) => (
<SelectItem key={island.id} value={island.id.toString()}> <SelectItem
key={island.id}
value={island.id.toString()}
>
{island.name} {island.name}
</SelectItem> </SelectItem>
))} ))}
@@ -208,7 +234,10 @@ export default function SignUpForm() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="address" className="text-sm font-medium text-slate-700 dark:text-slate-300"> <label
htmlFor="address"
className="text-sm font-medium text-slate-700 dark:text-slate-300"
>
Address Address
</label> </label>
<Input <Input
@@ -233,7 +262,10 @@ export default function SignUpForm() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="dob" className="text-sm font-medium text-slate-700 dark:text-slate-300"> <label
htmlFor="dob"
className="text-sm font-medium text-slate-700 dark:text-slate-300"
>
Date of Birth Date of Birth
</label> </label>
<Input <Input
@@ -244,7 +276,9 @@ export default function SignUpForm() {
)} )}
name="dob" name="dob"
disabled={isPending} disabled={isPending}
defaultValue={(actionState?.payload?.get("dob") || "") as string} defaultValue={
(actionState?.payload?.get("dob") || "") as string
}
type="date" type="date"
placeholder="Date of birth" placeholder="Date of birth"
/> />
@@ -260,7 +294,10 @@ export default function SignUpForm() {
)} )}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="accNo" className="text-sm font-medium text-slate-700 dark:text-slate-300"> <label
htmlFor="accNo"
className="text-sm font-medium text-slate-700 dark:text-slate-300"
>
Account Number Account Number
</label> </label>
@@ -273,7 +310,9 @@ export default function SignUpForm() {
name="accNo" name="accNo"
type="number" type="number"
disabled={isPending} disabled={isPending}
defaultValue={(actionState?.payload?.get("accNo") || "") as string} defaultValue={
(actionState?.payload?.get("accNo") || "") as string
}
placeholder="Account no" placeholder="Account no"
/> />
{actionState?.errors?.fieldErrors.accNo && ( {actionState?.errors?.fieldErrors.accNo && (
@@ -283,7 +322,10 @@ export default function SignUpForm() {
)} )}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="phone_number" className="text-sm font-medium text-slate-700 dark:text-slate-300"> <label
htmlFor="phone_number"
className="text-sm font-medium text-slate-700 dark:text-slate-300"
>
Phone Number Phone Number
</label> </label>
<Input <Input
@@ -316,7 +358,8 @@ export default function SignUpForm() {
<input <input
type="checkbox" type="checkbox"
defaultChecked={ defaultChecked={
((actionState?.payload?.get("terms") || "") as string) === "on" ((actionState?.payload?.get("terms") || "") as string) ===
"on"
} }
name="terms" name="terms"
id="terms" id="terms"
@@ -340,7 +383,8 @@ export default function SignUpForm() {
<input <input
type="checkbox" type="checkbox"
defaultChecked={ defaultChecked={
((actionState?.payload?.get("policy") || "") as string) === "on" ((actionState?.payload?.get("policy") || "") as string) ===
"on"
} }
name="policy" name="policy"
id="terms" id="terms"
@@ -372,9 +416,7 @@ export default function SignUpForm() {
</Link> </Link>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
); );
} }

View File

@@ -1,17 +1,18 @@
"use client"; "use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { signIn } from "next-auth/react";
import Link from "next/link"; import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { signIn } from "next-auth/react";
import { useTransition } from "react"; import { useTransition } from "react";
import { type SubmitHandler, useForm } from "react-hook-form"; import { type SubmitHandler, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
const OTPSchema = z.object({ const OTPSchema = z.object({
pin: z.string().min(6, { pin: z.string().min(6, {
message: "Your one-time password must be 6 characters.", message: "Your one-time password must be 6 characters.",
@@ -20,7 +21,9 @@ const OTPSchema = z.object({
export default function VerifyOTPForm({ export default function VerifyOTPForm({
phone_number, phone_number,
}: { phone_number: string }) { }: {
phone_number: string;
}) {
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const router = useRouter(); const router = useRouter();
console.log("verification in OTP form", phone_number); console.log("verification in OTP form", phone_number);
@@ -57,7 +60,7 @@ export default function VerifyOTPForm({
return ( return (
<form <form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
className="w-full max-w-xs title-bg border rounded-lg shadow my-4" className="w-full max-w-xs title-bg border-2 border-sarLinkOrange/50 rounded-lg shadow my-4"
> >
<div className="grid pb-4 pt-4 gap-4 px-4"> <div className="grid pb-4 pt-4 gap-4 px-4">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">

View File

@@ -1,18 +1,20 @@
"use client"; "use client";
import { Loader2 } from "lucide-react";
import Link from "next/link";
import { redirect, useSearchParams } from "next/navigation";
import { useActionState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { VerifyRegistrationOTP } from "@/queries/authentication"; import { VerifyRegistrationOTP } from "@/queries/authentication";
import { Loader2 } from "lucide-react";
import Link from "next/link";
import { redirect, useSearchParams } from "next/navigation";
import { useActionState } from "react";
export default function VerifyRegistrationOTPForm({ export default function VerifyRegistrationOTPForm({
phone_number, phone_number,
}: { phone_number: string }) { }: {
phone_number: string;
}) {
console.log("verification in OTP form", phone_number); console.log("verification in OTP form", phone_number);
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@@ -26,7 +28,7 @@ export default function VerifyRegistrationOTPForm({
return ( return (
<form <form
action={formAction} action={formAction}
className="w-full max-w-xs bg-white dark:bg-sarLinkOrange/10 title-bg border rounded-lg shadow my-4" className="w-full max-w-xs bg-white dark:bg-sarLinkOrange/10 title-bg border-2 border-sarLinkOrange/50 rounded-lg shadow my-4"
> >
<div className="grid pb-4 pt-4 gap-4 px-4"> <div className="grid pb-4 pt-4 gap-4 px-4">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
@@ -35,7 +37,7 @@ export default function VerifyRegistrationOTPForm({
{state.message} {state.message}
</p> </p>
) : ( ) : (
<p className="bg-sarLinkOrange/50 border border-yellow-900/50 dark:border-sarLinkOrange/50 rounded p-2 text-center text-sm text-gray-900 dark:text-gray-300"> <p className="bg-sarLinkOrange border border-yellow-900/50 dark:border-sarLinkOrange/50 rounded p-2 text-center text-sm text-gray-900 dark:text-orange-950">
Account verification OTP sent to [{phone_number}] Account verification OTP sent to [{phone_number}]
</p> </p>
)} )}
@@ -57,7 +59,7 @@ export default function VerifyRegistrationOTPForm({
type="number" type="number"
placeholder="Enter OTP" placeholder="Enter OTP"
className={cn( className={cn(
"bg-white text-black", "bg-white text-black dark:text-white dark:bg-gray-950",
state.status === "verify_success" && "hidden", state.status === "verify_success" && "hidden",
)} )}
/> />

View File

@@ -10,14 +10,18 @@ import { Button } from "../ui/button";
export default function CancelPaymentButton({ export default function CancelPaymentButton({
paymentId, paymentId,
}: { paymentId: string }) { }: {
paymentId: string;
}) {
const router = useRouter(); const router = useRouter();
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
return ( return (
<Button <Button
onClick={async () => { onClick={async () => {
setLoading(true); setLoading(true);
const [error, payment] = await tryCatch(cancelPayment({ id: paymentId })); const [error, payment] = await tryCatch(
cancelPayment({ id: paymentId }),
);
console.log(payment); console.log(payment);
if (error) { if (error) {
toast.error(error.message); toast.error(error.message);

View File

@@ -8,32 +8,30 @@ import { cancelTopup } from "@/actions/payment";
import { tryCatch } from "@/utils/tryCatch"; import { tryCatch } from "@/utils/tryCatch";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
export default function CancelTopupButton({ export default function CancelTopupButton({ topupId }: { topupId: string }) {
topupId, const router = useRouter();
}: { topupId: string }) { const [loading, setLoading] = React.useState(false);
const router = useRouter(); return (
const [loading, setLoading] = React.useState(false); <Button
return ( onClick={async () => {
<Button setLoading(true);
onClick={async () => { const [error, topup] = await tryCatch(cancelTopup({ id: topupId }));
setLoading(true); if (error) {
const [error, topup] = await tryCatch(cancelTopup({ id: topupId })); toast.error(error.message);
if (error) { setLoading(false);
toast.error(error.message); } else {
setLoading(false); toast.success("Topup cancelled successfully!", {
} else { description: `Your topup of ${topup?.amount} MVR has been cancelled.`,
toast.success("Topup cancelled successfully!", { closeButton: true,
description: `Your topup of ${topup?.amount} MVR has been cancelled.`, });
closeButton: true, router.replace("/top-ups");
}) }
router.replace("/top-ups"); }}
} disabled={loading}
}} variant={"destructive"}
disabled={loading} >
variant={"destructive"} Cancel Topup
> {loading ? <Loader2 className="animate-spin" /> : <Trash2 />}
Cancel Topup </Button>
{loading ? <Loader2 className="animate-spin" /> : <Trash2 />} );
</Button>
);
} }

View File

@@ -1,57 +1,67 @@
'use client' "use client";
import { usePathname, useRouter } from 'next/navigation' import { usePathname, useRouter } from "next/navigation";
import { useEffect, useState } from 'react' import { useEffect, useState } from "react";
import { Progress } from '@/components/ui/progress' import { Progress } from "@/components/ui/progress";
const calculateTimeLeft = (expiresAt: string) => { const calculateTimeLeft = (expiresAt: string) => {
const now = Date.now() const now = Date.now();
const expirationTime = new Date(expiresAt).getTime() const expirationTime = new Date(expiresAt).getTime();
return Math.max(0, Math.floor((expirationTime - now) / 1000)) return Math.max(0, Math.floor((expirationTime - now) / 1000));
} };
const HumanizeTimeLeft = (seconds: number) => { const HumanizeTimeLeft = (seconds: number) => {
const minutes = Math.floor(seconds / 60) const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60 const remainingSeconds = seconds % 60;
return `${minutes}m ${remainingSeconds}s` return `${minutes}m ${remainingSeconds}s`;
} };
export default function ExpiryCountDown({ expiresAt, expiryLabel }: { expiresAt: string, expiryLabel: string }) { export default function ExpiryCountDown({
const [timeLeft, setTimeLeft] = useState(calculateTimeLeft(expiresAt)) expiresAt,
const [mounted, setMounted] = useState(false) expiryLabel,
const router = useRouter() }: {
const pathname = usePathname() expiresAt: string;
useEffect(() => { expiryLabel: string;
setMounted(true) }) {
}, []) const [timeLeft, setTimeLeft] = useState(calculateTimeLeft(expiresAt));
const [mounted, setMounted] = useState(false);
useEffect(() => { const router = useRouter();
const timer = setInterval(() => { const pathname = usePathname();
setTimeLeft(calculateTimeLeft(expiresAt)) useEffect(() => {
}, 1000) setMounted(true);
}, []);
return () => clearInterval(timer)
}, [expiresAt]) useEffect(() => {
const timer = setInterval(() => {
useEffect(() => { setTimeLeft(calculateTimeLeft(expiresAt));
if (timeLeft <= 0) { }, 1000);
router.replace(pathname)
} return () => clearInterval(timer);
}, [timeLeft, router, pathname]) }, [expiresAt]);
if (!mounted) { useEffect(() => {
return null if (timeLeft <= 0) {
} router.replace(pathname);
return ( }
<div className='overflow-clip relative mx-2 p-4 rounded-md border border-dashed flex items-center justify-between text-muted-foreground'> }, [timeLeft, router, pathname]);
<div className='absolute inset-0 title-bg mask-b-from-0' />
{timeLeft ? ( if (!mounted) {
<span>Time left: {HumanizeTimeLeft(timeLeft)}</span> return null;
) : ( }
<span>{expiryLabel} has expired.</span> return (
)} <div className="overflow-clip relative mx-2 p-4 rounded-md border border-dashed flex items-center justify-between text-muted-foreground">
{timeLeft > 0 && ( <div className="absolute inset-0 title-bg mask-b-from-0" />
<Progress className='absolute bottom-0 left-0 right-0' color='#f49b5b' value={timeLeft / 600 * 100} /> {timeLeft ? (
)} <span>Time left: {HumanizeTimeLeft(timeLeft)}</span>
</div> ) : (
) <span>{expiryLabel} has expired.</span>
)}
{timeLeft > 0 && (
<Progress
className="absolute bottom-0 left-0 right-0"
color="#f49b5b"
value={(timeLeft / 600) * 100}
/>
)}
</div>
);
} }

View File

@@ -46,7 +46,10 @@ export default function BlockDeviceDialog({
parentalControl?: boolean; parentalControl?: boolean;
}) { }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [state, formAction, isPending] = useActionState(blockDeviceAction, initialState); const [state, formAction, isPending] = useActionState(
blockDeviceAction,
initialState,
);
const [isTransitioning, startTransition] = useTransition(); const [isTransitioning, startTransition] = useTransition();
const handleSimpleBlock = () => { const handleSimpleBlock = () => {
@@ -99,7 +102,11 @@ export default function BlockDeviceDialog({
// If device is not blocked and user is not admin, show simple block button // If device is not blocked and user is not admin, show simple block button
if (!device.blocked && parentalControl) { if (!device.blocked && parentalControl) {
return ( return (
<Button onClick={handleSimpleBlock} disabled={isLoading} variant="destructive"> <Button
onClick={handleSimpleBlock}
disabled={isLoading}
variant="destructive"
>
<ShieldBan /> <ShieldBan />
{isLoading ? <TextShimmer>Blocking</TextShimmer> : "Block"} {isLoading ? <TextShimmer>Blocking</TextShimmer> : "Block"}
</Button> </Button>
@@ -137,10 +144,13 @@ export default function BlockDeviceDialog({
rows={10} rows={10}
name="reason_for_blocking" name="reason_for_blocking"
id="reason_for_blocking" id="reason_for_blocking"
defaultValue={(state?.payload?.get("reason_for_blocking") || "") as string} defaultValue={
(state?.payload?.get("reason_for_blocking") || "") as string
}
className={cn( className={cn(
"col-span-5", "col-span-5 mt-2",
(state.fieldErrors?.reason_for_blocking) && "ring-2 ring-red-500", state.fieldErrors?.reason_for_blocking &&
"ring-2 ring-red-500",
)} )}
/> />
<span className="text-sm text-red-500"> <span className="text-sm text-red-500">

View File

@@ -12,7 +12,11 @@ export default function ClickableRow({
device, device,
parentalControl, parentalControl,
admin = false, admin = false,
}: { device: Device; parentalControl?: boolean; admin?: boolean }) { }: {
device: Device;
parentalControl?: boolean;
admin?: boolean;
}) {
const [devices, setDeviceCart] = useAtom(deviceCartAtom); const [devices, setDeviceCart] = useAtom(deviceCartAtom);
return ( return (

View File

@@ -12,14 +12,19 @@ import { Badge } from "./ui/badge";
export default function DeviceCard({ export default function DeviceCard({
device, device,
parentalControl, parentalControl,
}: { device: Device; parentalControl?: boolean }) { }: {
device: Device;
parentalControl?: boolean;
isAdmin?: boolean;
}) {
const [devices, setDeviceCart] = useAtom(deviceCartAtom); const [devices, setDeviceCart] = useAtom(deviceCartAtom);
const isChecked = devices.some((d) => d.id === device.id); const isChecked = devices.some((d) => d.id === device.id);
return ( return (
// biome-ignore lint/a11y/noStaticElementInteractions: <dw about it>
<div <div
onKeyUp={() => { }} onKeyUp={() => {}}
onClick={() => { onClick={() => {
if (device.blocked) return; if (device.blocked) return;
if (device.is_active === true) return; if (device.is_active === true) return;
@@ -45,7 +50,10 @@ export default function DeviceCard({
<div className=""> <div className="">
<div className="font-semibold flex flex-col items-start gap-2 mb-2"> <div className="font-semibold flex flex-col items-start gap-2 mb-2">
<Link <Link
className={cn("font-medium hover:underline ml-0.5", device.is_active ? "text-green-600" : "")} className={cn(
"font-medium hover:underline ml-0.5",
device.is_active ? "text-green-600" : "",
)}
href={`/devices/${device.id}`} href={`/devices/${device.id}`}
> >
{device.name} {device.name}
@@ -77,7 +85,7 @@ export default function DeviceCard({
)} )}
{device.has_a_pending_payment && ( {device.has_a_pending_payment && (
<Link href={`/payments/${device.pending_payment_id}`}> <Link href={`/payments/${device.pending_payment_id}`}>
<span className="bg-muted rounded px-2 p-1 mt-2 flex hover:underline items-center justify-center gap-2 text-muted-foreground text-yellow-600"> <span className="bg-muted rounded px-2 p-1 mt-2 flex hover:underline items-center justify-center gap-2 text-yellow-600">
Payment Pending{" "} Payment Pending{" "}
<HandCoins className="animate-pulse" size={14} /> <HandCoins className="animate-pulse" size={14} />
</span> </span>

View File

@@ -16,7 +16,7 @@ export function DeviceCartDrawer() {
if (devices.length === 0) return null; if (devices.length === 0) return null;
return ( return (
<div className="bg-sarLinkOrange rounded-lg shadow-2xl dark:hover:bg-orange-900 fixed bottom-20 w-80 uppercase h-auto z-20 left-1/2 transform -translate-x-1/2 hover:ring-2 hover:ring-sarLinkOrange transition-all duration-200 p-2 flex flex-col gap-2"> <div className="bg-sarLinkOrange rounded-lg shadow-2xl dark:bg-orange-900 fixed bottom-20 w-80 uppercase h-auto z-20 left-1/2 transform -translate-x-1/2 hover:ring-2 hover:ring-sarLinkOrange transition-all duration-200 p-2 flex flex-col gap-2">
<Button <Button
size={"lg"} size={"lg"}
className="w-ful" className="w-ful"
@@ -24,7 +24,8 @@ export function DeviceCartDrawer() {
variant="secondary" variant="secondary"
> >
<MonitorSmartphone /> <MonitorSmartphone />
Pay {devices.length > 0 && `(${devices.length})`} {devices.length > 1 ? "devices" : "device"} Pay {devices.length > 0 && `(${devices.length})`}{" "}
{devices.length > 1 ? "devices" : "device"}
</Button> </Button>
<Button <Button
variant={"destructive"} variant={"destructive"}

View File

@@ -1,5 +1,10 @@
"use client"; "use client";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { CircleDollarSign, Loader2 } from "lucide-react";
import { redirect, usePathname } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
import { createPayment } from "@/actions/payment"; import { createPayment } from "@/actions/payment";
import DeviceCard from "@/components/device-card"; import DeviceCard from "@/components/device-card";
import NumberInput from "@/components/number-input"; import NumberInput from "@/components/number-input";
@@ -7,41 +12,21 @@ import { Button } from "@/components/ui/button";
import { deviceCartAtom, numberOfMonths } from "@/lib/atoms"; import { deviceCartAtom, numberOfMonths } from "@/lib/atoms";
import type { NewPayment } from "@/lib/backend-types"; import type { NewPayment } from "@/lib/backend-types";
import { tryCatch } from "@/utils/tryCatch"; import { tryCatch } from "@/utils/tryCatch";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { CircleDollarSign, Loader2 } from "lucide-react";
import { redirect, usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import FullPageLoader from "./full-page-loader"; import FullPageLoader from "./full-page-loader";
export default function DevicesForPayment() { export default function DevicesForPayment() {
const baseAmount = 100;
const discountPercentage = 75;
const pathname = usePathname(); const pathname = usePathname();
const devices = useAtomValue(deviceCartAtom); const devices = useAtomValue(deviceCartAtom);
const setDeviceCart = useSetAtom(deviceCartAtom); const setDeviceCart = useSetAtom(deviceCartAtom);
const [months, setMonths] = useAtom(numberOfMonths); const [months, setMonths] = useAtom(numberOfMonths);
const [message, setMessage] = useState("");
const [disabled, setDisabled] = useState(false); const [disabled, setDisabled] = useState(false);
const [total, setTotal] = useState(0);
console.log(total);
useEffect(() => {
if (months === 7) {
setMessage("You will get 1 month free.");
} else if (months === 12) {
setMessage("You will get 2 months free.");
} else {
setMessage("");
}
setTotal(baseAmount + (devices.length + 1 - 1) * discountPercentage);
}, [months, devices.length]);
if (pathname === "/payments") { if (pathname === "/payments") {
return null; return null;
} }
const data: NewPayment = { const data: NewPayment = {
amount: 100, number_of_months: months,
number_of_months: 2,
device_ids: devices.map((device) => device.id), device_ids: devices.map((device) => device.id),
}; };
@@ -63,11 +48,7 @@ export default function DevicesForPayment() {
maxAllowed={12} maxAllowed={12}
isDisabled={devices.length === 0} isDisabled={devices.length === 0}
/> />
{message && (
<span className="title-bg text-lime-800 bg-lime-100/50 dark:text-lime-100 rounded text-center p-2 w-full">
{message}
</span>
)}
</div> </div>
<Button <Button
onClick={async () => { onClick={async () => {

View File

@@ -2,14 +2,13 @@ import { redirect } from "next/navigation";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { authOptions } from "@/app/auth"; import { authOptions } from "@/app/auth";
import { import {
Table, Table,
TableBody, TableBody,
TableCaption, TableCell,
TableCell, TableFooter,
TableFooter, TableHead,
TableHead, TableHeader,
TableHeader, TableRow,
TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { getDevices } from "@/queries/devices"; import { getDevices } from "@/queries/devices";
import { tryCatch } from "@/utils/tryCatch"; import { tryCatch } from "@/utils/tryCatch";
@@ -19,113 +18,106 @@ import DeviceCard from "./device-card";
import Pagination from "./pagination"; import Pagination from "./pagination";
export async function DevicesTable({ export async function DevicesTable({
searchParams, searchParams,
parentalControl, parentalControl,
additionalFilters = {}, additionalFilters = {},
}: { }: {
searchParams: Promise<{ searchParams: Promise<{
[key: string]: unknown; [key: string]: unknown;
}>; }>;
parentalControl?: boolean; parentalControl?: boolean;
additionalFilters?: Record<string, string | number | boolean>; additionalFilters?: Record<string, string | number | boolean>;
}) { }) {
const resolvedParams = await searchParams; const resolvedParams = await searchParams;
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
const isAdmin = session?.user?.is_admin; const isAdmin = session?.user?.is_admin;
const page = Number.parseInt(resolvedParams.page as string) || 1; const page = Number.parseInt(resolvedParams.page as string) || 1;
const limit = 10; const limit = 10;
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
// Build params object for getDevices // Build params object for getDevices
const apiParams: Record<string, string | number | undefined> = {}; const apiParams: Record<string, string | number | undefined> = {};
for (const [key, value] of Object.entries(resolvedParams)) { for (const [key, value] of Object.entries(resolvedParams)) {
if (value !== undefined && value !== "") { if (value !== undefined && value !== "") {
apiParams[key] = typeof value === "number" ? value : String(value); apiParams[key] = typeof value === "number" ? value : String(value);
} }
} }
for (const [key, value] of Object.entries(additionalFilters)) { for (const [key, value] of Object.entries(additionalFilters)) {
if (value !== undefined && value !== "") { if (value !== undefined && value !== "") {
apiParams[key] = typeof value === "number" ? value : String(value); apiParams[key] = typeof value === "number" ? value : String(value);
} }
} }
apiParams.limit = limit; apiParams.limit = limit;
apiParams.offset = offset; apiParams.offset = offset;
console.log("API Params:", apiParams); const [error, devices] = await tryCatch(getDevices(apiParams));
const [error, devices] = await tryCatch( if (error) {
getDevices(apiParams), if (error.message === "UNAUTHORIZED") {
); redirect("/auth/signin");
if (error) { } else {
if (error.message === "UNAUTHORIZED") { return <ClientErrorMessage message={error.message} />;
redirect("/auth/signin"); }
} else { }
return <ClientErrorMessage message={error.message} />; const { meta, data } = devices;
} return (
} <div>
const { meta, data } = devices; {data?.length === 0 ? (
return ( <div className="h-[calc(100svh-400px)] text-muted-foreground flex flex-col items-center justify-center my-4">
<div> <h3>{parentalControl ? "No active devices" : "No devices."}</h3>
{data?.length === 0 ? ( </div>
<div className="h-[calc(100svh-400px)] text-muted-foreground flex flex-col items-center justify-center my-4"> ) : (
<h3>No devices.</h3> <>
</div> <div className="hidden sm:block">
) : ( <Table className="overflow-scroll">
<> <TableHeader>
<div className="hidden sm:block"> <TableRow>
<Table className="overflow-scroll"> <TableHead>Device Name</TableHead>
<TableCaption>Table of all devices.</TableCaption> <TableHead>MAC Address</TableHead>
<TableHeader> <TableHead>Vendor</TableHead>
<TableRow> <TableHead>#</TableHead>
<TableHead>Device Name</TableHead> </TableRow>
<TableHead>MAC Address</TableHead> </TableHeader>
<TableHead>Vendor</TableHead> <TableBody className="overflow-scroll">
<TableHead>#</TableHead> {data?.map((device) => (
</TableRow> <ClickableRow
</TableHeader> admin={isAdmin}
<TableBody className="overflow-scroll"> key={device.id}
{data?.map((device) => ( device={device}
<ClickableRow parentalControl={parentalControl}
admin={isAdmin} />
key={device.id} ))}
device={device} </TableBody>
parentalControl={parentalControl} <TableFooter>
/> <TableRow>
))} <TableCell colSpan={4} className="text-muted-foreground">
</TableBody> {meta?.total === 1 ? (
<TableFooter> <p className="text-center">Total {meta?.total} device.</p>
<TableRow> ) : (
<TableCell colSpan={4} className="text-muted-foreground"> <p className="text-center">
{meta?.total === 1 ? ( Total {meta?.total} devices.
<p className="text-center"> </p>
Total {meta?.total} device. )}
</p> </TableCell>
) : ( </TableRow>
<p className="text-center"> </TableFooter>
Total {meta?.total} devices. </Table>
</p> </div>
)} <div className="sm:hidden my-4">
</TableCell> {data?.map((device) => (
</TableRow> <DeviceCard
</TableFooter> parentalControl={parentalControl}
</Table> key={device.id}
</div> device={device}
<div className="sm:hidden my-4"> />
{data?.map((device) => ( ))}
<DeviceCard </div>
parentalControl={parentalControl} <Pagination
key={device.id} totalPages={meta?.last_page}
device={device} currentPage={meta?.current_page}
/> />
))} </>
</div> )}
<Pagination </div>
totalPages={meta?.last_page} );
currentPage={meta?.current_page}
/>
</>
)
}
</div >
);
} }

View File

@@ -1,14 +1,11 @@
"use client"; "use client";
import { import { BadgeDollarSign, Loader2, Wallet } from "lucide-react";
BadgeDollarSign, import { useActionState, useEffect } from "react";
Clipboard,
ClipboardCheck,
Loader2,
Wallet,
} from "lucide-react";
import { useActionState, useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { type VerifyDevicePaymentState, verifyDevicePayment } from "@/actions/payment"; import {
type VerifyDevicePaymentState,
verifyDevicePayment,
} from "@/actions/payment";
import { import {
Table, Table,
TableBody, TableBody,
@@ -19,6 +16,7 @@ import {
} from "@/components/ui/table"; } from "@/components/ui/table";
import type { Payment } from "@/lib/backend-types"; import type { Payment } from "@/lib/backend-types";
import type { User } from "@/lib/types/user"; import type { User } from "@/lib/types/user";
import { AccountInfomation } from "./account-information";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
const initialState: VerifyDevicePaymentState = { const initialState: VerifyDevicePaymentState = {
@@ -30,8 +28,16 @@ const initialState: VerifyDevicePaymentState = {
export default function DevicesToPay({ export default function DevicesToPay({
payment, payment,
user, user,
}: { payment?: Payment; user?: User }) { disabled,
const [state, formAction, isPending] = useActionState(verifyDevicePayment, initialState); }: {
payment?: Payment;
user?: User;
disabled?: boolean;
}) {
const [state, formAction, isPending] = useActionState(
verifyDevicePayment,
initialState,
);
// Handle toast notifications based on state changes // Handle toast notifications based on state changes
useEffect(() => { useEffect(() => {
@@ -40,7 +46,11 @@ export default function DevicesToPay({
closeButton: true, closeButton: true,
description: state.message, description: state.message,
}); });
} else if (!state.success && state.message && state.message !== initialState.message) { } else if (
!state.success &&
state.message &&
state.message !== initialState.message
) {
toast.error("Payment Verification Failed", { toast.error("Payment Verification Failed", {
closeButton: true, closeButton: true,
description: state.message, description: state.message,
@@ -101,7 +111,11 @@ export default function DevicesToPay({
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{isWalletPayVisible && ( {isWalletPayVisible && (
<form action={formAction}> <form action={formAction}>
<input type="hidden" name="paymentId" value={payment?.id ?? ""} /> <input
type="hidden"
name="paymentId"
value={payment?.id ?? ""}
/>
<input type="hidden" name="method" value="WALLET" /> <input type="hidden" name="method" value="WALLET" />
<Button <Button
disabled={isPending} disabled={isPending}
@@ -109,16 +123,22 @@ export default function DevicesToPay({
variant={"secondary"} variant={"secondary"}
size={"lg"} size={"lg"}
> >
{isPending ? "Processing payment..." : "Pay with wallet"} {isPending
? "Processing payment..."
: "Pay with wallet"}
<Wallet /> <Wallet />
</Button> </Button>
</form> </form>
)} )}
<form action={formAction}> <form action={formAction}>
<input type="hidden" name="paymentId" value={payment?.id ?? ""} /> <input
type="hidden"
name="paymentId"
value={payment?.id ?? ""}
/>
<input type="hidden" name="method" value="TRANSFER" /> <input type="hidden" name="method" value="TRANSFER" />
<Button <Button
disabled={isPending} disabled={isPending || disabled}
type="submit" type="submit"
size={"lg"} size={"lg"}
className="mb-4" className="mb-4"
@@ -136,6 +156,22 @@ export default function DevicesToPay({
</div> </div>
</TableCaption> </TableCaption>
<TableBody className=""> <TableBody className="">
<TableRow>
<TableCell>Payment created</TableCell>
<TableCell className="text-right">
{new Date(payment?.created_at ?? "").toLocaleDateString(
"en-US",
{
month: "short",
day: "2-digit",
year: "numeric",
minute: "2-digit",
hour: "2-digit",
second: "2-digit",
},
)}
</TableCell>
</TableRow>
<TableRow> <TableRow>
<TableCell>Total Devices</TableCell> <TableCell>Total Devices</TableCell>
<TableCell className="text-right text-xl"> <TableCell className="text-right text-xl">
@@ -162,43 +198,3 @@ export default function DevicesToPay({
</div> </div>
); );
} }
function AccountInfomation({
accountNo,
accName,
}: {
accountNo: string;
accName: string;
}) {
const [accNo, setAccNo] = useState(false);
return (
<div className="justify-start items-start border my-4 flex flex-col gap-2 p-2 rounded-md">
<h6 className="title-bg uppercase p-2 border rounded w-full font-semibold">
Account Information
</h6>
<div className="border justify-start flex flex-col items-start bg-white/10 w-full p-2 rounded">
<div className="text-sm font-semibold">Account Name</div>
<span>{accName}</span>
</div>
<div className="border flex justify-between items-start gap-2 bg-white/10 w-full p-2 rounded">
<div className="flex flex-col items-start justify-start">
<p className="text-sm font-semibold">Account No</p>
<span>{accountNo}</span>
</div>
<Button
onClick={() => {
setTimeout(() => {
setAccNo(true);
navigator.clipboard.writeText(accountNo);
}, 2000);
toast.success("Account number copied!");
setAccNo((prev) => !prev);
}}
variant={"link"}
>
{accNo ? <Clipboard /> : <ClipboardCheck color="green" />}
</Button>
</div>
</div>
);
}

View File

@@ -1,183 +1,218 @@
"use client" "use client";
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"; import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ListFilter, Loader2, X } from "lucide-react"; import { ListFilter, Loader2, X } from "lucide-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useQueryState } from "nuqs"; import { useQueryState } from "nuqs";
import { useState, useTransition } from 'react'; import { useState, useTransition } from "react";
export default function DeviceFilter() { export default function DeviceFilter() {
const { replace } = useRouter(); const { replace } = useRouter();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [disabled, startTransition] = useTransition(); const [disabled, startTransition] = useTransition();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const pathname = usePathname(); const pathname = usePathname();
const params = new URLSearchParams(searchParams.toString()); const params = new URLSearchParams(searchParams.toString());
const [urlInputName] = useQueryState("name", {
clearOnDefault: true,
});
const [urlInputMac] = useQueryState("mac", {
clearOnDefault: true,
});
const [urlInputVendor] = useQueryState("vendor", {
clearOnDefault: true,
});
const [urlInputName] = useQueryState("name", { // Local state for input fields
clearOnDefault: true const [inputName, setInputName] = useState(urlInputName ?? "");
}) const [inputMac, setInputMac] = useState(urlInputMac ?? "");
const [urlInputMac] = useQueryState("mac", { const [inputVendor, setInputVendor] = useState(urlInputVendor ?? "");
clearOnDefault: true
})
const [urlInputVendor] = useQueryState("vendor", {
clearOnDefault: true
})
// Local state for input fields // Map filter keys to their state setters
const [inputName, setInputName] = useState(urlInputName ?? ""); const filterSetters: Record<
const [inputMac, setInputMac] = useState(urlInputMac ?? ""); string,
const [inputVendor, setInputVendor] = useState(urlInputVendor ?? ""); React.Dispatch<React.SetStateAction<string>>
> = {
name: setInputName,
mac: setInputMac,
vendor: setInputVendor,
};
function handleSearch({
name,
mac,
vendor,
}: {
name: string;
mac: string;
vendor: string;
}) {
if (name) params.set("name", name);
else params.delete("name");
if (mac) params.set("mac", mac);
else params.delete("mac");
if (vendor) params.set("vendor", vendor);
else params.delete("vendor");
params.set("page", "1");
// Map filter keys to their state setters startTransition(() => {
const filterSetters: Record<string, React.Dispatch<React.SetStateAction<string>>> = { replace(`${pathname}?${params.toString()}`);
name: setInputName, });
mac: setInputMac, }
vendor: setInputVendor,
};
function handleSearch({ name, mac, vendor }: { name: string; mac: string; vendor: string }) {
if (name) params.set("name", name); else params.delete("name"); const appliedFilters = searchParams
if (mac) params.set("mac", mac); else params.delete("mac"); .toString()
if (vendor) params.set("vendor", vendor); else params.delete("vendor"); .split("&")
params.set("page", "1"); .filter((filter) => !filter.startsWith("page=") && filter);
return (
<div className="flex flex-col items-start justify-start gap-2 w-full">
<Drawer open={isOpen} onOpenChange={setIsOpen}>
<DrawerTrigger asChild>
<Button
className="w-full sm:w-48 flex items-end justify-between"
onClick={() => setIsOpen(!isOpen)}
variant="outline"
>
Filter
<ListFilter />
</Button>
</DrawerTrigger>
<DrawerContent>
<div className="mx-auto w-full max-w-3xl">
<DrawerHeader>
<DrawerTitle>Device Filters</DrawerTitle>
<DrawerDescription asChild>
<div>Select your desired filters here</div>
</DrawerDescription>
</DrawerHeader>
startTransition(() => { <div className="grid sm:grid-cols-3 gap-4 p-4">
replace(`${pathname}?${params.toString()}`); <Input
}); placeholder="Device name ..."
} value={inputName || searchParams.get("name") || ""}
onChange={(e) => setInputName(e.target.value)}
/>
<Input
placeholder="Device Mac address ..."
value={inputMac || searchParams.get("mac") || ""}
onChange={(e) => setInputMac(e.target.value)}
/>
<Input
placeholder="Device vendor ..."
value={inputVendor || searchParams.get("vendor") || ""}
onChange={(e) => setInputVendor(e.target.value)}
/>
</div>
<DrawerFooter className="max-w-sm mx-auto">
<Button
onClick={() => {
handleSearch({
name: inputName ?? "",
mac: inputMac ?? "",
vendor: inputVendor ?? "",
});
}}
>
{disabled ? (
<>
<Loader2 className="ml-2 animate-spin" />
</>
) : (
<>Apply Filters</>
)}
</Button>
<Button
variant="secondary"
onClick={() => {
setInputName("");
setInputMac("");
setInputVendor("");
replace(pathname);
}}
>
Clear Filters
</Button>
const appliedFilters = searchParams <DrawerClose asChild>
.toString() <Button variant="outline">Cancel</Button>
.split("&") </DrawerClose>
.filter((filter) => !filter.startsWith("page=") && filter) </DrawerFooter>
return ( </div>
<div className="flex flex-col items-start justify-start gap-2 w-full"> </DrawerContent>
<Drawer open={isOpen} onOpenChange={setIsOpen}> </Drawer>
<DrawerTrigger asChild> <div className="flex gap-2 w-fit flex-wrap">
<Button className="w-full sm:w-48 flex items-end justify-between" onClick={() => setIsOpen(!isOpen)} variant="outline"> {appliedFilters.map((filter) => (
Filter <Badge
<ListFilter /> aria-disabled={disabled}
</Button> variant={"outline"}
</DrawerTrigger> className={cn("flex p-2 gap-2 items-center justify-between", {
<DrawerContent> "opacity-50 pointer-events-none": disabled,
<div className="mx-auto w-full max-w-3xl"> })}
<DrawerHeader> key={filter}
<DrawerTitle>Device Filters</DrawerTitle> >
<DrawerDescription asChild> <span className="text-md">{prettyPrintFilter(filter)}</span>
<div> {disabled ? (
Select your desired filters here <Loader2 className="animate-spin" size={16} />
</div> ) : (
</DrawerDescription> <X
</DrawerHeader> className="bg-sarLinkOrange/50 rounded-full p-1 hover:cursor-pointer hover:ring ring-sarLinkOrange"
size={16}
onClick={() => {
const key = filter.split("=")[0];
params.delete(key);
<div className="grid sm:grid-cols-3 gap-4 p-4"> // Use the mapping to clear the correct input state
<Input filterSetters[key]?.("");
placeholder="Device name ..."
value={inputName || searchParams.get("name") || ""}
onChange={e => setInputName(e.target.value)}
/>
<Input
placeholder="Device Mac address ..."
value={inputMac || searchParams.get("mac") || ""}
onChange={e => setInputMac(e.target.value)}
/>
<Input
placeholder="Device vendor ..."
value={inputVendor || searchParams.get("vendor") || ""}
onChange={e => setInputVendor(e.target.value)}
/>
</div>
<DrawerFooter className="max-w-sm mx-auto">
<Button
onClick={() => {
handleSearch({
name: inputName ?? "",
mac: inputMac ?? "",
vendor: inputVendor ?? "",
});
}}
>
{disabled ? (
<>
<Loader2 className="ml-2 animate-spin" />
</>
) : (
<>
Apply Filters
</>
)}
</Button>
<Button variant="secondary" onClick={() => {
setInputName("");
setInputMac("");
setInputVendor("");
replace(pathname)
}}>
Clear Filters
</Button>
<DrawerClose asChild> startTransition(() => {
<Button variant="outline">Cancel</Button> replace(`${pathname}?${params.toString()}`);
</DrawerClose> });
</DrawerFooter> }}
</div> >
</DrawerContent> Remove
</Drawer> </X>
<div className="flex gap-2 w-fit flex-wrap"> )}
{appliedFilters.map((filter) => ( </Badge>
<Badge ))}
aria-disabled={disabled} </div>
variant={"outline"} </div>
className={cn("flex p-2 gap-2 items-center justify-between", { "opacity-50 pointer-events-none": disabled })} );
key={filter}>
<span className="text-md">{prettyPrintFilter(filter)}</span>
{
disabled ? (
<Loader2 className="animate-spin" size={16} />
) : (
<X
className="bg-sarLinkOrange/50 rounded-full p-1 hover:cursor-pointer hover:ring ring-sarLinkOrange"
size={16}
onClick={() => {
const key = filter.split("=")[0];
params.delete(key);
// Use the mapping to clear the correct input state
filterSetters[key]?.("");
startTransition(() => {
replace(`${pathname}?${params.toString()}`);
});
}}
>
Remove
</X>
)
}
</Badge>
))}
</div>
</div>
);
} }
function prettyPrintFilter(filter: string) { function prettyPrintFilter(filter: string) {
const [key, value] = filter.split("="); const [key, value] = filter.split("=");
switch (key) { switch (key) {
case "name": case "name":
return <p>Device Name: <span className="text-muted-foreground">{value}</span></p>; return (
case "mac": <p>
return <p>MAC Address: <span className="text-muted-foreground">{value}</span></p>; Device Name: <span className="text-muted-foreground">{value}</span>
case "vendor": </p>
return <p>Vendor: <span className="text-muted-foreground">{value}</span></p>; );
default: case "mac":
return filter; return (
} <p>
MAC Address: <span className="text-muted-foreground">{value}</span>
</p>
);
case "vendor":
return (
<p>
Vendor: <span className="text-muted-foreground">{value}</span>
</p>
);
default:
return filter;
}
} }

View File

@@ -1,9 +1,9 @@
import React from 'react' import React from "react";
import { Loader2 } from 'lucide-react' import { Loader2 } from "lucide-react";
export default function FullPageLoader() { export default function FullPageLoader() {
return ( return (
<div className='flex items-center justify-center h-screen'> <div className="flex items-center justify-center h-screen">
<Loader2 className='animate-spin' /> <Loader2 className="animate-spin" />
</div> </div>
) );
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,74 +1,65 @@
import { PhoneIcon } from "lucide-react" import { PhoneIcon } from "lucide-react";
import Link from "next/link" import Link from "next/link";
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
AccordionItem, AccordionItem,
AccordionTrigger, AccordionTrigger,
} from "@/components/ui/accordion" } from "@/components/ui/accordion";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
export function GetMacAccordion() { export function GetMacAccordion() {
return ( return (
<Accordion <Accordion type="single" collapsible className="w-full">
type="single" <AccordionItem value="item-1">
collapsible <AccordionTrigger>How do I find my MAC Address?</AccordionTrigger>
className="w-full" <AccordionContent className="flex flex-col gap-4 text-start">
> <p>
<AccordionItem value="item-1"> A MAC (Media Access Control) address is a unique identifier assigned
<AccordionTrigger>How do I find my MAC Address?</AccordionTrigger> to a device`&apos;`s network. It is used to identify the device on a
<AccordionContent className="flex flex-col gap-4 text-start"> network, helping to differentiate devices on a network.
<p> </p>
A MAC (Media Access Control) address is a unique identifier assigned <Accordion type="single" collapsible className="w-full">
to a device`&apos;`s network. It is used to identify the device on a <AccordionItem value="iphone">
network, helping to differentiate devices on a network. <AccordionTrigger>iPhone</AccordionTrigger>
</p> <AccordionContent>
<Accordion type="single" collapsible className="w-full"> Settings General About Wi-Fi Address
<AccordionItem value="iphone"> </AccordionContent>
<AccordionTrigger>iPhone</AccordionTrigger> </AccordionItem>
<AccordionContent> <AccordionItem value="redmi">
Settings General About Wi-Fi Address <AccordionTrigger>Redmi</AccordionTrigger>
</AccordionContent> <AccordionContent>
</AccordionItem> Settings About Wi-Fi MAC Address
<AccordionItem value="redmi"> </AccordionContent>
<AccordionTrigger>Redmi</AccordionTrigger> </AccordionItem>
<AccordionContent> <AccordionItem value="samsung">
Settings About Wi-Fi MAC Address <AccordionTrigger>Samsung</AccordionTrigger>
</AccordionContent> <AccordionContent>
</AccordionItem> Settings About phone Status Information Wi-Fi MAC Address
<AccordionItem value="samsung"> </AccordionContent>
<AccordionTrigger>Samsung</AccordionTrigger> </AccordionItem>
<AccordionContent> <AccordionItem value="windows">
Settings About phone Status Information Wi-Fi MAC Address <AccordionTrigger>Windows Laptop</AccordionTrigger>
</AccordionContent> <AccordionContent>
</AccordionItem> Settings Network and Internet Wi-Fi Hardware Properties
<AccordionItem value="windows"> Physical address (MAC):
<AccordionTrigger>Windows Laptop</AccordionTrigger> </AccordionContent>
<AccordionContent> </AccordionItem>
Settings Network and Internet Wi-Fi Hardware <AccordionItem value="other">
Properties Physical address (MAC): <AccordionTrigger>Other Device</AccordionTrigger>
</AccordionContent> <AccordionContent>
</AccordionItem> <p>Please contact SAR Link for assistance.</p>
<AccordionItem value="other"> <Link href="tel:+9609198026">
<AccordionTrigger>Other Device</AccordionTrigger> <Button className="mt-4">
<AccordionContent> <PhoneIcon className="mr-2" />
<p> 9198026
Please contact SAR Link for assistance. </Button>
</p> </Link>
<Link </AccordionContent>
href="tel:+9609198026" </AccordionItem>
> </Accordion>
<Button className="mt-4"> </AccordionContent>
<PhoneIcon className="mr-2" /> </AccordionItem>
9198026 </Accordion>
</Button> );
</Link>
</AccordionContent>
</AccordionItem>
</Accordion>
</AccordionContent>
</AccordionItem>
</Accordion>
)
} }

View File

@@ -1,41 +1,54 @@
import { cn } from '@/lib/utils'; import { CheckCheck, X } from "lucide-react";
import { CheckCheck, X } from 'lucide-react'; import { cn } from "@/lib/utils";
export default function InputReadOnly({ label, value, labelClassName, className, showCheck = true, checkTrue = false }: { export default function InputReadOnly({
label: string; label,
value: string; value,
labelClassName?: string; labelClassName,
className?: string; className,
showCheck?: boolean; showCheck = true,
checkTrue?: boolean checkTrue = false,
}: {
label: string;
value: string;
labelClassName?: string;
className?: string;
showCheck?: boolean;
checkTrue?: boolean;
}) { }) {
return ( return (
<div className={cn("relative flex items-center justify-between rounded-lg border border-input bg-background shadow-sm shadow-black/5 transition-shadow focus-within:border-ring focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 has-disabled:cursor-not-allowed has-disabled:opacity-80 [&:has(input:is(:disabled))_*]:pointer-events-none", className)}> <div
<div> className={cn(
"relative flex items-center justify-between rounded-lg border border-input bg-background shadow-sm shadow-black/5 transition-shadow focus-within:border-ring focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 has-disabled:cursor-not-allowed has-disabled:opacity-80 [&:has(input:is(:disabled))_*]:pointer-events-none col-span-2 sm:col-span-1",
className,
)}
>
<div>
<label
htmlFor="input-33"
className={cn("block px-3 pt-2 text-xs font-medium", labelClassName)}
>
{label}
</label>
<input
id="input-33"
className="flex h-10 w-full bg-transparent px-3 pb-2 text-sm text-foreground placeholder:text-muted-foreground/70 focus-visible:outline-none"
placeholder={value}
disabled
value={value}
type="text"
/>
</div>
<label htmlFor="input-33" className={cn("block px-3 pt-2 text-xs font-medium", labelClassName)}> {showCheck && (
{label} <div>
</label> {checkTrue ? (
<input <CheckCheck className="mx-4 text-green-500" />
id="input-33" ) : (
className="flex h-10 w-full bg-transparent px-3 pb-2 text-sm text-foreground placeholder:text-muted-foreground/70 focus-visible:outline-none" <X className="mx-4 text-red-500" />
placeholder={value} )}
disabled </div>
value={value} )}
type="text" </div>
/> );
</div>
{showCheck && (
<div>
{checkTrue ? (
<CheckCheck className='mx-4 text-green-500' />
) : (
<X className='mx-4 text-red-500' />
)}
</div>
)}
</div>
)
} }

View File

@@ -1,4 +1,3 @@
import { cn } from "@/lib/utils";
import { Minus, Plus } from "lucide-react"; import { Minus, Plus } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { import {
@@ -8,16 +7,23 @@ import {
Label, Label,
NumberField, NumberField,
} from "react-aria-components"; } from "react-aria-components";
import { cn } from "@/lib/utils";
export default function NumberInput({ export default function NumberInput({
maxAllowed, maxAllowed,
label, label,
value, value = 100,
onChange, onChange,
className, className,
isDisabled, isDisabled,
}: { maxAllowed?: number, label: string; value: number; onChange: (value: number) => void, className?: string, isDisabled?: boolean }) { }: {
maxAllowed?: number;
label: string;
value?: number;
onChange: (value: number) => void;
className?: string;
isDisabled?: boolean;
}) {
useEffect(() => { useEffect(() => {
if (maxAllowed) { if (maxAllowed) {
if (value > maxAllowed) { if (value > maxAllowed) {
@@ -25,8 +31,15 @@ export default function NumberInput({
} }
} }
}, [maxAllowed, value, onChange]); }, [maxAllowed, value, onChange]);
return ( return (
<NumberField isDisabled={isDisabled} className={cn(className)} value={value} minValue={0} onChange={onChange}> <NumberField
isDisabled={isDisabled}
className={cn(className)}
value={value}
minValue={0}
onChange={onChange}
>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-medium text-foreground">{label}</Label> <Label className="text-sm font-medium text-foreground">{label}</Label>
<Group className="relative inline-flex h-9 w-full items-center overflow-hidden whitespace-nowrap rounded-lg border border-input text-sm shadow-sm shadow-black/5 transition-shadow data-[focus-within]:border-ring data-disabled:opacity-50 data-focus-within:outline-none data-focus-within:ring-[3px] data-[focus-within]:ring-ring/20"> <Group className="relative inline-flex h-9 w-full items-center overflow-hidden whitespace-nowrap rounded-lg border border-input text-sm shadow-sm shadow-black/5 transition-shadow data-[focus-within]:border-ring data-disabled:opacity-50 data-focus-within:outline-none data-focus-within:ring-[3px] data-[focus-within]:ring-ring/20">

View File

@@ -1,109 +1,113 @@
"use client"; "use client";
import { Button } from "@/components/ui/button";
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"; import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
type PaginationProps = { type PaginationProps = {
totalPages: number; totalPages: number;
currentPage: number; currentPage: number;
maxVisible?: number; maxVisible?: number;
}; };
export default function Pagination({ export default function Pagination({
totalPages, totalPages,
currentPage, currentPage,
maxVisible = 4, maxVisible = 4,
}: PaginationProps) { }: PaginationProps) {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const activePage = searchParams.get("page") ?? 1; const activePage = searchParams.get("page") ?? 1;
const router = useRouter(); const router = useRouter();
const [queryParams, setQueryParams] = useState<{ [key: string]: string }>({}); const [queryParams, setQueryParams] = useState<{ [key: string]: string }>({});
useEffect(() => { useEffect(() => {
const params = Object.fromEntries( const params = Object.fromEntries(
Array.from(searchParams.entries()).filter(([key]) => key !== "page"), Array.from(searchParams.entries()).filter(([key]) => key !== "page"),
); );
setQueryParams(params); setQueryParams(params);
}, [searchParams]); }, [searchParams]);
useEffect(() => { useEffect(() => {
if (!searchParams.has("page")) { if (!searchParams.has("page")) {
router.replace(`?page=1${IncludeQueries()}`); router.replace(`?page=1${IncludeQueries()}`);
} }
}); });
function IncludeQueries() { function IncludeQueries() {
return Object.entries(queryParams) return Object.entries(queryParams)
.map(([key, value]) => `&${key}=${value}`) .map(([key, value]) => `&${key}=${value}`)
.join(""); .join("");
} }
const generatePageNumbers = (): (number | string)[] => { const generatePageNumbers = (): (number | string)[] => {
const halfVisible = Math.floor(maxVisible / 2); const halfVisible = Math.floor(maxVisible / 2);
let startPage = Math.max(currentPage - halfVisible, 1); let startPage = Math.max(currentPage - halfVisible, 1);
const endPage = Math.min(startPage + maxVisible - 1, totalPages); const endPage = Math.min(startPage + maxVisible - 1, totalPages);
if (endPage - startPage + 1 < maxVisible) { if (endPage - startPage + 1 < maxVisible) {
startPage = Math.max(endPage - maxVisible + 1, 1); startPage = Math.max(endPage - maxVisible + 1, 1);
} }
const pageNumbers: (number | string)[] = []; const pageNumbers: (number | string)[] = [];
if (startPage > 1) { if (startPage > 1) {
pageNumbers.push(1); pageNumbers.push(1);
if (startPage > 2) pageNumbers.push("..."); if (startPage > 2) pageNumbers.push("...");
} }
for (let i = startPage; i <= endPage; i++) { for (let i = startPage; i <= endPage; i++) {
pageNumbers.push(i); pageNumbers.push(i);
} }
if (endPage < totalPages) { if (endPage < totalPages) {
if (endPage < totalPages - 1) pageNumbers.push("..."); if (endPage < totalPages - 1) pageNumbers.push("...");
pageNumbers.push(totalPages); pageNumbers.push(totalPages);
} }
return pageNumbers; return pageNumbers;
}; };
const pageNumbers = generatePageNumbers(); const pageNumbers = generatePageNumbers();
return ( if (totalPages <= 1) {
<div className="flex items-center justify-center space-x-2 my-4"> return null;
{currentPage > 1 && ( }
<Link href={`?page=${Number(currentPage) - 1}${IncludeQueries()}`}>
<Button variant="secondary" className="flex items-center gap-2">
<ArrowLeftIcon className="h-4 w-4" />
</Button>
</Link>
)}
{pageNumbers.map((page) => ( return (
<React.Fragment key={`${page}`}> <div className="flex items-center justify-center space-x-2 my-4">
{typeof page === "number" ? ( {currentPage > 1 && (
<Link href={`?page=${page}${IncludeQueries()}`}> <Link href={`?page=${Number(currentPage) - 1}${IncludeQueries()}`}>
<Button <Button variant="secondary" className="flex items-center gap-2">
variant={Number(activePage) === page ? "default" : "outline"} <ArrowLeftIcon className="h-4 w-4" />
> </Button>
{page} </Link>
</Button> )}
</Link>
) : (
<span className="px-2">...</span>
)}
</React.Fragment>
))}
{currentPage < totalPages && ( {pageNumbers.map((page) => (
<Link href={`?page=${Number(currentPage) + 1}${IncludeQueries()}`}> <React.Fragment key={`${page}`}>
<Button variant="secondary" className="flex items-center gap-2"> {typeof page === "number" ? (
<ArrowRightIcon className="h-4 w-4" /> <Link href={`?page=${page}${IncludeQueries()}`}>
</Button> <Button
</Link> variant={Number(activePage) === page ? "default" : "outline"}
)} >
</div> {page}
); </Button>
</Link>
) : (
<span className="px-2">...</span>
)}
</React.Fragment>
))}
{currentPage < totalPages && (
<Link href={`?page=${Number(currentPage) + 1}${IncludeQueries()}`}>
<Button variant="secondary" className="flex items-center gap-2">
<ArrowRightIcon className="h-4 w-4" />
</Button>
</Link>
)}
</div>
);
} }

View File

@@ -3,14 +3,13 @@ import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getPayments } from "@/actions/payment"; import { getPayments } from "@/actions/payment";
import { import {
Table, Table,
TableBody, TableBody,
TableCaption, TableCell,
TableCell, TableFooter,
TableFooter, TableHead,
TableHead, TableHeader,
TableHeader, TableRow,
TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import type { Payment } from "@/lib/backend-types"; import type { Payment } from "@/lib/backend-types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -21,241 +20,265 @@ import { Button } from "./ui/button";
import { Separator } from "./ui/separator"; import { Separator } from "./ui/separator";
export async function PaymentsTable({ export async function PaymentsTable({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ searchParams: Promise<{
[key: string]: unknown; [key: string]: unknown;
}>; }>;
}) { }) {
const resolvedParams = await searchParams; const resolvedParams = await searchParams;
const apiParams: Record<string, string | number | undefined> = {}; const page = Number.parseInt(resolvedParams.page as string) || 1;
for (const [key, value] of Object.entries(resolvedParams)) { const limit = 10;
if (value !== undefined && value !== "") { const offset = (page - 1) * limit;
apiParams[key] = typeof value === "number" ? value : String(value); const apiParams: Record<string, string | number | undefined> = {};
} for (const [key, value] of Object.entries(resolvedParams)) {
} if (value !== undefined && value !== "") {
const [error, payments] = await tryCatch(getPayments(apiParams)); apiParams[key] = typeof value === "number" ? value : String(value);
}
}
apiParams.limit = limit;
apiParams.offset = offset;
const [error, payments] = await tryCatch(getPayments(apiParams));
if (error) { if (error) {
if (error.message.includes("Unauthorized")) { if (error.message.includes("Unauthorized")) {
redirect("/auth/signin"); redirect("/auth/signin");
} else { } else {
return <pre>{JSON.stringify(error, null, 2)}</pre>; return <pre>{JSON.stringify(error, null, 2)}</pre>;
} }
} }
const { data, meta } = payments; const { data, meta } = payments;
return ( return (
<div> <div>
{data?.length === 0 ? ( {data?.length === 0 ? (
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4"> <div className="h-[calc(100svh-400px)] text-muted-foreground flex flex-col items-center justify-center my-4">
<h3>No Payments yet.</h3> <h3>No Payments.</h3>
</div> </div>
) : ( ) : (
<> <>
<div className="hidden sm:block"> <div className="hidden sm:block">
<Table className="overflow-scroll"> <Table className="overflow-scroll">
<TableCaption>Table of all devices.</TableCaption> <TableHeader>
<TableHeader> <TableRow>
<TableRow> <TableHead>Details</TableHead>
<TableHead>Details</TableHead> <TableHead>Duration</TableHead>
<TableHead>Duration</TableHead> <TableHead>Status</TableHead>
<TableHead>Status</TableHead> <TableHead>Amount</TableHead>
<TableHead>Amount</TableHead> </TableRow>
</TableRow> </TableHeader>
</TableHeader> <TableBody className="overflow-scroll">
<TableBody className="overflow-scroll"> {payments?.data?.map((payment) => (
{payments?.data?.map((payment) => ( <TableRow key={payment.id}>
<TableRow key={payment.id}> <TableCell>
<TableCell> <div
<div className={cn(
className={cn( "flex flex-col items-start border rounded p-2",
"flex flex-col items-start border rounded p-2", payment?.paid
payment?.paid ? "bg-green-500/10 border-dashed border-green-500"
? "bg-green-500/10 border-dashed border-green=500" : payment?.is_expired
: "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50", ? "bg-gray-500/10 border-dashed border-gray-500 dark:border-gray-500/50"
)} : "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50",
> )}
<div className="flex items-center gap-2"> >
<Calendar size={16} opacity={0.5} /> <div className="flex items-center gap-2">
<span className="text-muted-foreground"> <Calendar size={16} opacity={0.5} />
{new Date(payment.created_at).toLocaleDateString( <span className="text-muted-foreground">
"en-US", {new Date(payment.created_at).toLocaleDateString(
{ "en-US",
month: "short", {
day: "2-digit", month: "short",
year: "numeric", day: "2-digit",
minute: "2-digit", year: "numeric",
hour: "2-digit", minute: "2-digit",
}, hour: "2-digit",
)} timeZone: "Indian/Maldives", // Force consistent timezone
</span> },
</div> )}
</span>
</div>
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2 mt-2">
<Link <Link
className="font-medium hover:underline" className="font-medium hover:underline"
href={`/payments/${payment.id}`} href={`/payments/${payment.id}`}
> >
<Button size={"sm"} variant="outline"> <Button size={"sm"} variant="outline">
View Details View Details
</Button> </Button>
</Link> </Link>
</div> </div>
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border"> <div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border">
<h3 className="text-sm font-medium">Devices</h3> <h3 className="text-sm font-medium">Devices</h3>
<ol className="list-disc list-inside text-sm"> <ol className="list-disc list-inside text-sm">
{payment.devices.map((device) => ( {payment.devices.map((device) => (
<li <li
key={device.id} key={device.id}
className="text-sm text-muted-foreground" className="text-sm text-muted-foreground"
> >
{device.name} {device.name}
</li> </li>
))} ))}
</ol> </ol>
</div> </div>
</div> </div>
</TableCell> </TableCell>
<TableCell className="font-medium"> <TableCell className="font-medium">
{payment.number_of_months} Months {payment.number_of_months} Months
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className="font-semibold pr-2"> <span className="font-semibold pr-2">
{payment.paid ? ( {payment.paid ? (
<Badge <Badge
className={cn( className={cn(
payment.status === "PENDING" payment.status === "PENDING"
? "bg-yellow-100 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-100" ? "bg-yellow-100 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-100"
: "bg-green-100 dark:bg-green-700" : "bg-green-100 dark:bg-green-700",
)} )}
variant="outline" variant="outline"
> >
{payment.status} {payment.status}
</Badge> </Badge>
) : payment.is_expired ? ( ) : payment.is_expired ? (
<Badge>Expired</Badge> <Badge>Expired</Badge>
) : ( ) : (
<Badge variant="outline">{payment.status}</Badge> <Badge variant="outline">{payment.status}</Badge>
)} )}
</span> </span>
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className="font-semibold pr-2"> <span className="font-semibold pr-2">
{payment.amount.toFixed(2)} {payment.amount.toFixed(2)}
</span> </span>
MVR MVR
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TableCell colSpan={4} className="text-muted-foreground">
<TableCell colSpan={3} className="text-muted-foreground"> {meta?.total === 1 ? (
{meta?.total === 1 ? ( <p className="text-center">
<p className="text-center"> Total {meta?.total} payment.
Total {meta?.total} payment. </p>
</p> ) : (
) : ( <p className="text-center">
<p className="text-center"> Total {meta?.total} payments.
Total {meta?.total} payments. </p>
</p> )}{" "}
)} </TableCell> </TableCell>
</TableRow> </TableRow>
</TableFooter> </TableFooter>
</Table> </Table>
<Pagination <Pagination
totalPages={meta.total / meta.per_page} totalPages={meta.last_page}
currentPage={meta.current_page} currentPage={meta.current_page}
/> />
</div> </div>
<div className="sm:hidden block"> <div className="sm:hidden block">
{data.map((payment) => ( {data.map((payment) => (
<MobilePaymentDetails key={payment.id} payment={payment} /> <MobilePaymentDetails key={payment.id} payment={payment} />
))} ))}
</div> </div>
</> </>
)} )}
</div> </div>
); );
} }
function MobilePaymentDetails({ payment }: { payment: Payment }) { export function MobilePaymentDetails({
return ( payment,
<div isAdmin = false,
className={cn( }: {
"flex flex-col items-start border rounded p-2 my-2", payment: Payment;
payment?.paid isAdmin?: boolean;
? "bg-green-500/10 border-dashed border-green=500" }) {
: "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50", return (
)} <div
> className={cn(
<div className="flex items-center gap-2"> "flex flex-col items-start border rounded p-2 my-2",
<Calendar size={16} opacity={0.5} /> payment?.paid
<span className="text-muted-foreground text-sm"> ? "bg-green-500/10 border-dashed border-green-500"
{new Date(payment.created_at).toLocaleDateString("en-US", { : payment?.is_expired
month: "short", ? "bg-gray-500/10 border-dashed border-gray-500 dark:border-gray-500/50"
day: "2-digit", : "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50",
year: "numeric", )}
minute: "2-digit", >
hour: "2-digit", <div className="flex items-center gap-2">
})} <Calendar size={16} opacity={0.5} />
</span> <span className="text-muted-foreground text-sm">
</div> {new Date(payment.created_at).toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
minute: "2-digit",
hour: "2-digit",
timeZone: "Indian/Maldives", // Force consistent timezone
})}
</span>
</div>
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2 mt-2">
<Link <Link
className="font-medium hover:underline" className="font-medium hover:underline"
href={`/payments/${payment.id}`} href={`/payments/${payment.id}`}
> >
<Button size={"sm"} variant="outline"> <Button size={"sm"} variant="outline">
View Details View Details
</Button> </Button>
</Link> </Link>
</div> </div>
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border"> <div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border">
<h3 className="text-sm font-medium">Devices</h3> <h3 className="text-sm font-medium">Devices</h3>
<ol className="list-disc list-inside text-sm"> <ol className="list-disc list-inside text-sm">
{payment.devices.map((device) => ( {payment.devices.map((device) => (
<li key={device.id} className="text-sm text-muted-foreground"> <li key={device.id} className="text-sm text-muted-foreground">
{device.name} {device.name}
</li> </li>
))} ))}
</ol> </ol>
<div className="block sm:hidden"> <div className="block sm:hidden">
<Separator className="my-2" /> <Separator className="my-2" />
<h3 className="text-sm font-medium">Duration</h3> <h3 className="text-sm font-medium">Duration</h3>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{payment.number_of_months} Months {payment.number_of_months} Months
</span> </span>
<Separator className="my-2" /> <Separator className="my-2" />
<h3 className="text-sm font-medium">Amount</h3> <h3 className="text-sm font-medium">Amount</h3>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{payment.amount.toFixed(2)} MVR {payment.amount.toFixed(2)} MVR
</span> </span>
<span className="font-semibold pr-2"> <span className="font-semibold pr-2">
{payment.paid ? ( {payment.paid ? (
<Badge <Badge
className={cn( className={cn(
payment.status === "PENDING" payment.status === "PENDING"
? "bg-yellow-100 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-100" ? "bg-yellow-100 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-100"
: "bg-green-100 dark:bg-green-700" : "bg-green-100 dark:bg-green-700",
)} )}
variant="outline" variant="outline"
> >
{payment.status} {payment.status}
</Badge> </Badge>
) : payment.is_expired ? ( ) : payment.is_expired ? (
<Badge>Expired</Badge> <Badge>Expired</Badge>
) : ( ) : (
<Badge variant="secondary">{payment.status}</Badge> <Badge variant="secondary">{payment.status}</Badge>
)} )}
</span> </span>
</div> {isAdmin && (
</div> <div className="my-2 text-primary flex flex-col items-start text-sm border rounded p-2 mt-2 w-full bg-white dark:bg-black">
</div> {payment?.user?.name}
</div> <span className="text-muted-foreground">
); {payment?.user?.id_card}
</span>
</div>
)}
</div>
</div>
</div>
</div>
);
} }

View File

@@ -1,90 +1,86 @@
"use client"; "use client";
import { import {
discountPercentageAtom, discountPercentageAtom,
formulaResultAtom, formulaResultAtom,
initialPriceAtom, initialPriceAtom,
numberOfDaysAtom, numberOfDaysAtom,
numberOfDevicesAtom, numberOfDevicesAtom,
} from "@/lib/atoms"; } from "@/lib/atoms";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { useEffect } from "react"; import { useEffect } from "react";
import NumberInput from "./number-input"; import NumberInput from "./number-input";
export default function PriceCalculator() { export default function PriceCalculator() {
const [initialPrice, setInitialPrice] = useAtom(initialPriceAtom); const [initialPrice, setInitialPrice] = useAtom(initialPriceAtom);
const [discountPercentage, setDiscountPercentage] = useAtom( const [discountPercentage, setDiscountPercentage] = useAtom(
discountPercentageAtom, discountPercentageAtom,
); );
const [numberOfDevices, setNumberOfDevices] = useAtom(numberOfDevicesAtom); const [numberOfDevices, setNumberOfDevices] = useAtom(numberOfDevicesAtom);
const [numberOfDays, setNumberOfDays] = useAtom(numberOfDaysAtom); const [numberOfDays, setNumberOfDays] = useAtom(numberOfDaysAtom);
const [formulaResult, setFormulaResult] = useAtom(formulaResultAtom); const [formulaResult, setFormulaResult] = useAtom(formulaResultAtom);
useEffect(() => { useEffect(() => {
const basePrice = initialPrice + (numberOfDevices - 1) * discountPercentage; const basePrice = initialPrice + (numberOfDevices - 1) * discountPercentage;
setFormulaResult( setFormulaResult(
`Price for ${numberOfDevices} device(s) over ${numberOfDays} day(s): MVR ${basePrice.toFixed(2)}`, `Price for ${numberOfDevices} device(s) over ${numberOfDays} day(s): MVR ${basePrice.toFixed(2)}`,
); );
}, [ }, [
initialPrice, initialPrice,
discountPercentage, discountPercentage,
numberOfDevices, numberOfDevices,
numberOfDays, numberOfDays,
setFormulaResult, setFormulaResult,
]); ]);
return ( return (
<div className="border p-2 rounded-xl"> <div className="border p-2 rounded-xl">
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4"> <div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl"> <h3 className="text-sarLinkOrange text-2xl">Price Calculator</h3>
Price Calculator </div>
</h3> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
</div> {/* Initial Price Input */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2"> <NumberInput
{/* Initial Price Input */} label="Initial Price"
<NumberInput value={initialPrice}
label="Initial Price" onChange={(value) => setInitialPrice(value)}
value={initialPrice} />
onChange={(value) => setInitialPrice(value)} {/* Number of Devices Input */}
/> <NumberInput
{/* Number of Devices Input */} label="Number of Devices"
<NumberInput value={numberOfDevices}
label="Number of Devices" onChange={(value) => setNumberOfDevices(value)}
value={numberOfDevices} />
onChange={(value) => setNumberOfDevices(value)} {/* Number of Days Input */}
/> <NumberInput
{/* Number of Days Input */} label="Number of Days"
<NumberInput value={numberOfDays}
label="Number of Days" onChange={(value) => setNumberOfDays(value)}
value={numberOfDays} />
onChange={(value) => setNumberOfDays(value)}
/>
{/* Discount Percentage Input */} {/* Discount Percentage Input */}
<NumberInput <NumberInput
label="Discount Percentage" label="Discount Percentage"
value={discountPercentage} value={discountPercentage}
onChange={(value) => setDiscountPercentage(value)} onChange={(value) => setDiscountPercentage(value)}
/> />
</div> </div>
<div className="mt-4"> <div className="mt-4">
<div className="title-bg relative rounded-lg border border-input shadow-sm shadow-black/5 transition-shadow focus-within:border-ring focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 has-disabled:cursor-not-allowed has-disabled:opacity-50 [&:has(input:is(:disabled))_*]:pointer-events-none"> <div className="title-bg relative rounded-lg border border-input shadow-sm shadow-black/5 transition-shadow focus-within:border-ring focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 has-disabled:cursor-not-allowed has-disabled:opacity-50 [&:has(input:is(:disabled))_*]:pointer-events-none">
<label <label
htmlFor="" htmlFor=""
className="block px-3 pt-2 text-md font-medium text-foreground" className="block px-3 pt-2 text-md font-medium text-foreground"
> >
Total Total
</label> </label>
<input <input
className="flex font-mono font-semibold h-10 w-full bg-transparent px-3 pb-2 text-sm text-foreground placeholder:text-muted-foreground/70 focus-visible:outline-none" className="flex font-mono font-semibold h-10 w-full bg-transparent px-3 pb-2 text-sm text-foreground placeholder:text-muted-foreground/70 focus-visible:outline-none"
value={formulaResult} value={formulaResult}
readOnly readOnly
placeholder={"Result"} placeholder={"Result"}
/> />
</div> </div>
</div> </div>
</div> </div>
); );
} }

View File

@@ -1,182 +1,157 @@
"use client"; "use client";
import { import { BadgeDollarSign, Loader2 } from "lucide-react";
BadgeDollarSign, import { useActionState, useEffect } from "react";
Clipboard,
ClipboardCheck,
Loader2,
} from "lucide-react";
import { useActionState, useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { type VerifyTopupPaymentState, verifyTopupPayment } from "@/actions/payment";
import { import {
Table, type VerifyTopupPaymentState,
TableBody, verifyTopupPayment,
TableCaption, } from "@/actions/payment";
TableCell, import {
TableFooter, Table,
TableRow, TableBody,
TableCaption,
TableCell,
TableFooter,
TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import type { Topup } from "@/lib/backend-types"; import type { Topup } from "@/lib/backend-types";
import { AccountInfomation } from "./account-information";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
const initialState: VerifyTopupPaymentState = { const initialState: VerifyTopupPaymentState = {
message: "", message: "",
success: false, success: false,
fieldErrors: {}, fieldErrors: {},
}; };
export default function TopupToPay({ topup, disabled }: { topup?: Topup, disabled?: boolean }) { export default function TopupToPay({
const [state, formAction, isPending] = useActionState(verifyTopupPayment, initialState); topup,
disabled,
// Handle toast notifications based on state changes
useEffect(() => {
if (state.success && state.message) {
toast.success("Topup successful!", {
closeButton: true,
description: state.transaction
? `Your topup payment has been verified successfully using ${state.transaction.sourceBank} bank transfer on ${state.transaction.trxDate}.`
: state.message,
});
} else if (!state.success && state.message && state.message !== initialState.message) {
toast.error("Topup Payment Verification Failed", {
closeButton: true,
description: state.message,
});
}
}, [state]);
return (
<div className="w-full">
<div className="m-2 flex items-end justify-end p-2 text-sm text-foreground border rounded">
<Table>
<TableCaption>
<div className="max-w-sm mx-auto">
<p>Please send the following amount to the payment address</p>
<AccountInfomation
accName="Baraveli Dev"
accountNo="90101400028321000"
/>
{topup?.paid ? (
<Button
size={"lg"}
variant={"secondary"}
disabled
className="dark:text-green-200 text-green-900 bg-green-500/20 uppercase font-semibold"
>
Topup Payment Verified
</Button>
) : (
<div className="flex flex-col gap-2">
<form action={formAction}>
<input type="hidden" name="topupId" value={topup?.id ?? ""} />
<Button
disabled={disabled || isPending}
type="submit"
size={"lg"}
className="mb-4"
>
{isPending
? "Processing payment..."
: "I have paid"}
{isPending ? (
<Loader2 className="animate-spin" />
) : (
<BadgeDollarSign />
)}
</Button>
</form>
</div>
)}
</div>
</TableCaption>
<TableBody className="">
<TableRow>
<TableCell>Topup created</TableCell>
<TableCell className="text-right text-muted-foreground">
{new Date(topup?.created_at ?? "").toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
minute: "2-digit",
hour: "2-digit",
second: "2-digit",
})}
</TableCell>
</TableRow>
<TableRow>
<TableCell>Payment received</TableCell>
<TableCell className="text-right text-sarLinkOrange">
{topup?.paid_at
? new Date(topup.paid_at).toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
minute: "2-digit",
hour: "2-digit",
second: "2-digit",
})
: "-"}
</TableCell>
</TableRow>
<TableRow>
<TableCell>MIB Reference</TableCell>
<TableCell className="text-right">
{topup?.mib_reference ? topup.mib_reference : "-"}
</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow className="">
<TableCell colSpan={1}>Total Due</TableCell>
<TableCell className="text-right text-3xl font-bold">
{topup?.amount?.toFixed(2)}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
</div>
);
}
function AccountInfomation({
accountNo,
accName,
}: { }: {
accountNo: string; topup?: Topup;
accName: string; disabled?: boolean;
}) { }) {
const [accNo, setAccNo] = useState(false); const [state, formAction, isPending] = useActionState(
return ( verifyTopupPayment,
<div className="justify-start items-start border my-4 flex flex-col gap-2 p-2 rounded-md"> initialState,
<h6 className="title-bg uppercase p-2 border rounded w-full font-semibold"> );
Account Information
</h6> // Handle toast notifications based on state changes
<div className="border justify-start flex flex-col items-start bg-white/10 w-full p-2 rounded"> useEffect(() => {
<div className="text-sm font-semibold">Account Name</div> if (state.success && state.message) {
<span>{accName}</span> toast.success("Topup successful!", {
</div> closeButton: true,
<div className="border flex justify-between items-start gap-2 bg-white/10 w-full p-2 rounded"> description: state.transaction
<div className="flex flex-col items-start justify-start"> ? `Your topup payment has been verified successfully using ${state.transaction.sourceBank} bank transfer on ${state.transaction.trxDate}.`
<p className="text-sm font-semibold">Account No</p> : state.message,
<span>{accountNo}</span> });
</div> } else if (
<Button !state.success &&
onClick={() => { state.message &&
setTimeout(() => { state.message !== initialState.message
setAccNo(true); ) {
navigator.clipboard.writeText(accountNo); toast.error("Topup Payment Verification Failed", {
}, 2000); closeButton: true,
toast.success("Account number copied!"); description: state.message,
setAccNo((prev) => !prev); });
}} }
variant={"link"} }, [state]);
>
{accNo ? <Clipboard /> : <ClipboardCheck color="green" />} return (
</Button> <div className="w-full">
</div> <div className="m-2 flex items-end justify-end p-2 text-sm text-foreground border rounded">
</div> <Table>
); <TableCaption>
{!topup?.paid ||
topup?.is_expired ||
(topup?.status !== "CANCELLED" && (
<div className="max-w-sm mx-auto">
<p>Please send the following amount to the payment address</p>
<AccountInfomation
accName="Baraveli Dev"
accountNo="90101400028321000"
/>
{topup?.paid ? (
<Button
size={"lg"}
variant={"secondary"}
disabled
className="dark:text-green-200 text-green-900 bg-green-500/20 uppercase font-semibold"
>
Topup Payment Verified
</Button>
) : (
<div className="flex flex-col gap-2">
<form action={formAction}>
<input
type="hidden"
name="topupId"
value={topup?.id ?? ""}
/>
<Button
disabled={disabled || isPending}
type="submit"
size={"lg"}
className="mb-4 w-full"
>
{isPending ? "Processing payment..." : "I have paid"}
{isPending ? (
<Loader2 className="animate-spin" />
) : (
<BadgeDollarSign />
)}
</Button>
</form>
</div>
)}
</div>
))}
</TableCaption>
<TableBody className="">
<TableRow>
<TableCell>Topup created</TableCell>
<TableCell className="text-right text-muted-foreground">
{new Date(topup?.created_at ?? "").toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
minute: "2-digit",
hour: "2-digit",
second: "2-digit",
})}
</TableCell>
</TableRow>
<TableRow>
<TableCell>Payment received</TableCell>
<TableCell className="text-right text-sarLinkOrange">
{topup?.paid_at
? new Date(topup.paid_at).toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
minute: "2-digit",
hour: "2-digit",
second: "2-digit",
})
: "-"}
</TableCell>
</TableRow>
<TableRow>
<TableCell>MIB Reference</TableCell>
<TableCell className="text-right">
{topup?.mib_reference ? topup.mib_reference : "-"}
</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow className="">
<TableCell colSpan={1}>Total Due</TableCell>
<TableCell className="text-right text-3xl font-bold">
{topup?.amount?.toFixed(2)}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
</div>
);
} }

View File

@@ -3,14 +3,13 @@ import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTopups } from "@/actions/payment"; import { getTopups } from "@/actions/payment";
import { import {
Table, Table,
TableBody, TableBody,
TableCaption, TableCell,
TableCell, TableFooter,
TableFooter, TableHead,
TableHead, TableHeader,
TableHeader, TableRow,
TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import type { Topup } from "@/lib/backend-types"; import type { Topup } from "@/lib/backend-types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -20,197 +19,199 @@ import { Badge } from "./ui/badge";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
export async function TopupsTable({ export async function TopupsTable({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ searchParams: Promise<{
[key: string]: unknown; [key: string]: unknown;
}>; }>;
}) { }) {
const resolvedParams = await searchParams; const resolvedParams = await searchParams;
const page = Number.parseInt(resolvedParams.page as string) || 1;
const limit = 10;
const offset = (page - 1) * limit;
// Build params object
const apiParams: Record<string, string | number | undefined> = {};
for (const [key, value] of Object.entries(resolvedParams)) {
if (value !== undefined && value !== "") {
apiParams[key] = typeof value === "number" ? value : String(value);
}
}
apiParams.limit = limit;
apiParams.offset = offset;
const [error, topups] = await tryCatch(getTopups(apiParams));
// Build params object if (error) {
const apiParams: Record<string, string | number | undefined> = {}; if (error.message.includes("Unauthorized")) {
for (const [key, value] of Object.entries(resolvedParams)) { redirect("/auth/signin");
if (value !== undefined && value !== "") { } else {
apiParams[key] = typeof value === "number" ? value : String(value); return <pre>{JSON.stringify(error, null, 2)}</pre>;
} }
} }
const { data, meta } = topups;
return (
<div>
{data?.length === 0 ? (
<div className="h-[calc(100svh-400px)] flex text-muted-foreground flex-col items-center justify-center my-4">
<h3>No topups.</h3>
</div>
) : (
<>
<div className="hidden sm:block">
<Table className="overflow-scroll">
<TableHeader>
<TableRow>
<TableHead>Details</TableHead>
<TableHead>Status</TableHead>
<TableHead>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody className="overflow-scroll">
{topups?.data?.map((topup) => (
<TableRow key={topup.id}>
<TableCell>
<div
className={cn(
"flex flex-col items-start border rounded p-2",
topup?.paid
? "bg-green-500/10 border-dashed border-green-500"
: topup?.is_expired
? "bg-gray-500/10 border-dashed border-gray-500 dark:border-gray-500/50"
: "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50",
)}
>
<div className="flex items-center gap-2">
<Calendar size={16} opacity={0.5} />
<span className="text-muted-foreground">
{new Date(topup.created_at).toLocaleDateString(
"en-US",
{
month: "short",
day: "2-digit",
year: "numeric",
minute: "2-digit",
hour: "2-digit",
},
)}
</span>
</div>
const [error, topups] = await tryCatch(getTopups(apiParams)); <div className="flex items-center gap-2 mt-2">
<Link
className="font-medium hover:underline"
href={`/top-ups/${topup.id}`}
>
<Button size={"sm"} variant="outline">
View Details
</Button>
</Link>
</div>
</div>
</TableCell>
if (error) { <TableCell>
if (error.message.includes("Unauthorized")) { <span className="font-semibold pr-2">
redirect("/auth/signin"); {topup.paid ? (
} else { <Badge
return <pre>{JSON.stringify(error, null, 2)}</pre>; className="bg-green-100 dark:bg-green-700"
} variant="outline"
} >
const { data, meta } = topups; {topup.status}
return ( </Badge>
<div> ) : topup.is_expired ? (
{data?.length === 0 ? ( <Badge>Expired</Badge>
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4"> ) : (
<h3>No topups yet.</h3> <Badge variant="outline">{topup.status}</Badge>
</div> )}
) : ( </span>
<> </TableCell>
<div className="hidden sm:block"> <TableCell>
<Table className="overflow-scroll"> <span className="font-semibold pr-2">
<TableCaption>Table of all topups.</TableCaption> {topup.amount.toFixed(2)}
<TableHeader> </span>
<TableRow> MVR
<TableHead>Details</TableHead> </TableCell>
<TableHead>Status</TableHead> </TableRow>
<TableHead>Amount</TableHead> ))}
</TableRow> </TableBody>
</TableHeader> <TableFooter>
<TableBody className="overflow-scroll"> <TableRow>
{topups?.data?.map((topup) => ( <TableCell colSpan={4} className="text-muted-foreground">
<TableRow key={topup.id}> {meta?.total === 1 ? (
<TableCell> <p className="text-center">Total {meta?.total} topup.</p>
<div ) : (
className={cn( <p className="text-center">Total {meta?.total} topups.</p>
"flex flex-col items-start border rounded p-2", )}
topup?.paid </TableCell>
? "bg-green-500/10 border-dashed border-green-500" </TableRow>
: topup?.is_expired </TableFooter>
? "bg-gray-500/10 border-dashed border-gray-500 dark:border-gray-500/50" </Table>
: "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50", </div>
)} <div className="sm:hidden block">
> {data.map((topup) => (
<div className="flex items-center gap-2"> <MobileTopupDetails key={topup.id} topup={topup} />
<Calendar size={16} opacity={0.5} /> ))}
<span className="text-muted-foreground"> </div>
{new Date(topup.created_at).toLocaleDateString( <Pagination
"en-US", totalPages={meta?.last_page}
{ currentPage={meta?.current_page}
month: "short", />
day: "2-digit", </>
year: "numeric", )}
minute: "2-digit", </div>
hour: "2-digit", );
},
)}
</span>
</div>
<div className="flex items-center gap-2 mt-2">
<Link
className="font-medium hover:underline"
href={`/top-ups/${topup.id}`}
>
<Button size={"sm"} variant="outline">
View Details
</Button>
</Link>
</div>
</div>
</TableCell>
<TableCell>
<span className="font-semibold pr-2">
{topup.paid ? (
<Badge
className="bg-green-100 dark:bg-green-700"
variant="outline"
>
{topup.status}
</Badge>
) : topup.is_expired ? (
<Badge>Expired</Badge>
) : (
<Badge variant="outline">{topup.status}</Badge>
)}
</span>
</TableCell>
<TableCell>
<span className="font-semibold pr-2">
{topup.amount.toFixed(2)}
</span>
MVR
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={4} className="text-muted-foreground">
{meta?.total === 1 ? (
<p className="text-center">Total {meta?.total} topup.</p>
) : (
<p className="text-center">Total {meta?.total} topups.</p>
)}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
<div className="sm:hidden block">
{data.map((topup) => (
<MobileTopupDetails key={topup.id} topup={topup} />
))}
</div>
<Pagination
totalPages={meta.total / meta.per_page}
currentPage={meta.current_page}
/>
</>
)}
</div>
);
} }
function MobileTopupDetails({ topup }: { topup: Topup }) { function MobileTopupDetails({ topup }: { topup: Topup }) {
return ( return (
<div <div
className={cn( className={cn(
"flex flex-col items-start border rounded p-2 my-2", "flex flex-col items-start border rounded p-2 my-2",
topup?.paid topup?.paid
? "bg-green-500/10 border-dashed border-green=500" ? "bg-green-500/10 border-dashed border-green=500"
: "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50", : "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50",
)} )}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Calendar size={16} opacity={0.5} /> <Calendar size={16} opacity={0.5} />
<span className="text-muted-foreground text-sm"> <span className="text-muted-foreground text-sm">
{new Date(topup.created_at).toLocaleDateString("en-US", { {new Date(topup.created_at).toLocaleDateString("en-US", {
month: "short", month: "short",
day: "2-digit", day: "2-digit",
year: "numeric", year: "numeric",
})} })}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2 mt-2">
<Link <Link
className="font-medium hover:underline" className="font-medium hover:underline"
href={`/top-ups/${topup.id}`} href={`/top-ups/${topup.id}`}
> >
<Button size={"sm"} variant="outline"> <Button size={"sm"} variant="outline">
View Details View Details
</Button> </Button>
</Link> </Link>
</div> </div>
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border flex justify-between items-center"> <div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border flex justify-between items-center">
<div className="block sm:hidden"> <div className="block sm:hidden">
<h3 className="text-sm font-medium">Amount</h3> <h3 className="text-sm font-medium">Amount</h3>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{topup.amount.toFixed(2)} MVR {topup.amount.toFixed(2)} MVR
</span> </span>
</div> </div>
<span className="font-semibold pr-2"> <span className="font-semibold pr-2">
{topup.paid ? ( {topup.paid ? (
<Badge className="bg-green-100 dark:bg-green-700" variant="outline"> <Badge className="bg-green-100 dark:bg-green-700" variant="outline">
{topup.status} {topup.status}
</Badge> </Badge>
) : topup.is_expired ? ( ) : topup.is_expired ? (
<Badge>Expired</Badge> <Badge>Expired</Badge>
) : ( ) : (
<Badge variant="secondary">{topup.status}</Badge> <Badge variant="secondary">{topup.status}</Badge>
)} )}
</span> </span>
</div> </div>
</div> </div>
); );
} }

View File

@@ -1,66 +1,66 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { Accordion as AccordionPrimitive } from "radix-ui" import { Accordion as AccordionPrimitive } from "radix-ui";
import { ChevronDownIcon } from "lucide-react" import { ChevronDownIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Accordion({ function Accordion({
...props ...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) { }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} /> return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
} }
function AccordionItem({ function AccordionItem({
className, className,
...props ...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) { }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return ( return (
<AccordionPrimitive.Item <AccordionPrimitive.Item
data-slot="accordion-item" data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)} className={cn("border-b last:border-b-0", className)}
{...props} {...props}
/> />
) );
} }
function AccordionTrigger({ function AccordionTrigger({
className, className,
children, children,
...props ...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) { }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return ( return (
<AccordionPrimitive.Header className="flex"> <AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger <AccordionPrimitive.Trigger
data-slot="accordion-trigger" data-slot="accordion-trigger"
className={cn( className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180", "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" /> <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger> </AccordionPrimitive.Trigger>
</AccordionPrimitive.Header> </AccordionPrimitive.Header>
) );
} }
function AccordionContent({ function AccordionContent({
className, className,
children, children,
...props ...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) { }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return ( return (
<AccordionPrimitive.Content <AccordionPrimitive.Content
data-slot="accordion-content" data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm" className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props} {...props}
> >
<div className={cn("pt-0 pb-4", className)}>{children}</div> <div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content> </AccordionPrimitive.Content>
) );
} }
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@@ -1,141 +1,156 @@
"use client" "use client";
import { AlertDialog as AlertDialogPrimitive } from "radix-ui" import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import * as React from "react" import * as React from "react";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button" function AlertDialog({
import { cn } from "@/lib/utils" ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
}
const AlertDialog = AlertDialogPrimitive.Root function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
);
}
const AlertDialogTrigger = AlertDialogPrimitive.Trigger function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
);
}
const AlertDialogPortal = AlertDialogPrimitive.Portal function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
const AlertDialogOverlay = React.forwardRef< function AlertDialogContent({
React.ElementRef<typeof AlertDialogPrimitive.Overlay>, className,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
<AlertDialogPrimitive.Overlay return (
className={cn( <AlertDialogPortal>
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", <AlertDialogOverlay />
className <AlertDialogPrimitive.Content
)} data-slot="alert-dialog-content"
{...props} className={cn(
ref={ref} "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
/> className,
)) )}
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName {...props}
/>
</AlertDialogPortal>
);
}
const AlertDialogContent = React.forwardRef< function AlertDialogHeader({
React.ElementRef<typeof AlertDialogPrimitive.Content>, className,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<"div">) {
<AlertDialogPortal> return (
<AlertDialogOverlay /> <div
<AlertDialogPrimitive.Content data-slot="alert-dialog-header"
ref={ref} className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
className={cn( {...props}
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 [data-[state=closed]:slide-out-to-top-[48%]] data-[state=open]:slide-in-from-left-1/2 [data-[state=open]:slide-in-from-top-[48%]] sm:rounded-lg", />
className );
)} }
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({ function AlertDialogFooter({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.ComponentProps<"div">) {
<div return (
className={cn( <div
"flex flex-col space-y-2 text-center sm:text-left", data-slot="alert-dialog-footer"
className className={cn(
)} "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
{...props} className,
/> )}
) {...props}
AlertDialogHeader.displayName = "AlertDialogHeader" />
);
}
const AlertDialogFooter = ({ function AlertDialogTitle({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
<div return (
className={cn( <AlertDialogPrimitive.Title
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", data-slot="alert-dialog-title"
className className={cn("text-lg font-semibold", className)}
)} {...props}
{...props} />
/> );
) }
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef< function AlertDialogDescription({
React.ElementRef<typeof AlertDialogPrimitive.Title>, className,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
<AlertDialogPrimitive.Title return (
ref={ref} <AlertDialogPrimitive.Description
className={cn("text-lg font-semibold", className)} data-slot="alert-dialog-description"
{...props} className={cn("text-muted-foreground text-sm", className)}
/> {...props}
)) />
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName );
}
const AlertDialogDescription = React.forwardRef< function AlertDialogAction({
React.ElementRef<typeof AlertDialogPrimitive.Description>, className,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
<AlertDialogPrimitive.Description return (
ref={ref} <AlertDialogPrimitive.Action
className={cn("text-sm text-muted-foreground", className)} className={cn(buttonVariants(), className)}
{...props} {...props}
/> />
)) );
AlertDialogDescription.displayName = }
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef< function AlertDialogCancel({
React.ElementRef<typeof AlertDialogPrimitive.Action>, className,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
<AlertDialogPrimitive.Action return (
ref={ref} <AlertDialogPrimitive.Cancel
className={cn(buttonVariants(), className)} className={cn(buttonVariants({ variant: "outline" }), className)}
{...props} {...props}
/> />
)) );
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName }
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export { export {
AlertDialog, AlertDialog,
AlertDialogPortal, AlertDialogPortal,
AlertDialogOverlay, AlertDialogOverlay,
AlertDialogTrigger, AlertDialogTrigger,
AlertDialogContent, AlertDialogContent,
AlertDialogHeader, AlertDialogHeader,
AlertDialogFooter, AlertDialogFooter,
AlertDialogTitle, AlertDialogTitle,
AlertDialogDescription, AlertDialogDescription,
AlertDialogAction, AlertDialogAction,
AlertDialogCancel, AlertDialogCancel,
} };

View File

@@ -1,59 +1,59 @@
import * as React from "react" import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const alertVariants = cva( const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-background text-foreground", default: "bg-background text-foreground",
destructive: destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
const Alert = React.forwardRef< const Alert = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants> React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => ( >(({ className, variant, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
role="alert" role="alert"
className={cn(alertVariants({ variant }), className)} className={cn(alertVariants({ variant }), className)}
{...props} {...props}
/> />
)) ));
Alert.displayName = "Alert" Alert.displayName = "Alert";
const AlertTitle = React.forwardRef< const AlertTitle = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement> React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<h5 <h5
ref={ref} ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)} className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props} {...props}
/> />
)) ));
AlertTitle.displayName = "AlertTitle" AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef< const AlertDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)} className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props} {...props}
/> />
)) ));
AlertDescription.displayName = "AlertDescription" AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription } export { Alert, AlertTitle, AlertDescription };

View File

@@ -1,216 +1,226 @@
import { import {
BadgePlus, BadgePlus,
Calculator, Calculator,
ChevronRight, ChevronRight,
Coins, Coins,
CreditCard, CreditCard,
Handshake, Handshake,
MonitorSpeaker, MonitorSpeaker,
Smartphone, Smartphone,
UsersRound, UsersRound,
Wallet2Icon, Wallet2Icon,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { authOptions } from "@/app/auth"; import { authOptions } from "@/app/auth";
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from "@/components/ui/collapsible"; } from "@/components/ui/collapsible";
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
SidebarGroup, SidebarGroup,
SidebarGroupContent, SidebarGroupContent,
SidebarGroupLabel, SidebarGroupLabel,
SidebarHeader, SidebarHeader,
SidebarMenu, SidebarMenu,
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,
SidebarRail, SidebarRail,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
type Permission = { type Permission = {
id: number; id: number;
name: string; name: string;
}; };
type Categories = { type Categories = {
id: string; id: string;
children: ( children: (
| { | {
title: string; title: string;
link: string; link: string;
perm_identifier: string; perm_identifier: string;
icon: React.JSX.Element; icon: React.JSX.Element;
} }
| { | {
title: string; title: string;
link: string; link: string;
icon: React.JSX.Element; icon: React.JSX.Element;
perm_identifier?: undefined; perm_identifier?: undefined;
} }
)[]; )[];
}[]; }[];
export async function AppSidebar({ export async function AppSidebar({
...props ...props
}: React.ComponentProps<typeof Sidebar>) { }: React.ComponentProps<typeof Sidebar>) {
const categories = [ const categories = [
{ {
id: "MENU", id: "MENU",
url: "#", url: "#",
children: [ children: [
{ {
title: "Devices", title: "Devices",
link: "/devices?page=1", link: "/devices?page=1",
perm_identifier: "device", perm_identifier: "device",
icon: <Smartphone size={16} />, icon: <Smartphone size={16} />,
}, },
{ {
title: "Payments", title: "Parental Control",
link: "/payments?page=1", link: "/parental-control?page=1",
icon: <CreditCard size={16} />, icon: <CreditCard size={16} />,
perm_identifier: "payment", perm_identifier: "device",
}, },
{ {
title: "Top Ups", title: "Subscriptions",
link: "/top-ups?page=1", link: "/payments?page=1",
icon: <BadgePlus size={16} />, icon: <CreditCard size={16} />,
perm_identifier: "topup", perm_identifier: "payment",
}, },
{ {
title: "Parental Control", title: "Top Ups",
link: "/parental-control?page=1", link: "/top-ups?page=1",
icon: <CreditCard size={16} />, icon: <BadgePlus size={16} />,
perm_identifier: "device", perm_identifier: "topup",
}, },
{ {
title: "Agreements", title: "Transaction History",
link: "/agreements", link: "/wallet",
icon: <Handshake size={16} />, icon: <Wallet2Icon size={16} />,
perm_identifier: "device", perm_identifier: "wallet transaction",
}, },
{ {
title: "Wallet", title: "Agreements",
link: "/wallet", link: "/agreements",
icon: <Wallet2Icon size={16} />, icon: <Handshake size={16} />,
perm_identifier: "wallet", perm_identifier: "device",
}, },
], ],
}, },
{ {
id: "ADMIN CONTROL", id: "ADMIN CONTROL",
url: "#", url: "#",
children: [ children: [
{ {
title: "Users", title: "Users",
link: "/users", link: "/users",
icon: <UsersRound size={16} />, icon: <UsersRound size={16} />,
perm_identifier: "device", perm_identifier: "device",
}, },
{ {
title: "User Devices", title: "User Devices",
link: "/user-devices", link: "/user-devices",
icon: <MonitorSpeaker size={16} />, icon: <MonitorSpeaker size={16} />,
perm_identifier: "device", perm_identifier: "device",
}, },
{ {
title: "User Payments", title: "User Payments",
link: "/user-payments", link: "/user-payments",
icon: <Coins size={16} />, icon: <Coins size={16} />,
perm_identifier: "payment", perm_identifier: "payment",
}, },
{ {
title: "Price Calculator", title: "User Topups",
link: "/price-calculator", link: "/user-topups",
icon: <Calculator size={16} />, icon: <Coins size={16} />,
perm_identifier: "device", perm_identifier: "topup",
}, },
], {
}, title: "Price Calculator",
]; link: "/price-calculator",
icon: <Calculator size={16} />,
perm_identifier: "device",
},
],
},
];
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
let CATEGORIES: Categories; let CATEGORIES: Categories;
if (session?.user?.is_admin) { if (session?.user?.is_admin) {
CATEGORIES = categories; CATEGORIES = categories;
} else { } else {
// Filter out ADMIN CONTROL category for non-admin users // Filter out ADMIN CONTROL category for non-admin users
const nonAdminCategories = categories.filter( const nonAdminCategories = categories.filter(
(category) => category.id !== "ADMIN CONTROL" (category) => category.id !== "ADMIN CONTROL",
); );
const filteredCategories = nonAdminCategories.map((category) => { const filteredCategories = nonAdminCategories.map((category) => {
const filteredChildren = category.children.filter((child) => { const filteredChildren = category.children.filter((child) => {
const permIdentifier = child.perm_identifier; const permIdentifier = child.perm_identifier;
return session?.user?.user_permissions?.some((permission: Permission) => { return session?.user?.user_permissions?.some(
const permissionParts = permission.name.split(" "); (permission: Permission) => {
const modelNameFromPermission = permissionParts.slice(2).join(" "); const permissionParts = permission.name.split(" ");
return modelNameFromPermission === permIdentifier; const modelNameFromPermission = permissionParts.slice(2).join(" ");
}); return modelNameFromPermission === permIdentifier;
}); },
);
});
return { ...category, children: filteredChildren }; return { ...category, children: filteredChildren };
}); });
CATEGORIES = filteredCategories.filter( CATEGORIES = filteredCategories.filter(
(category) => category.children.length > 0, (category) => category.children.length > 0,
); );
} }
return ( return (
<Sidebar {...props} className="z-50"> <Sidebar {...props} className="z-50">
<SidebarHeader> <SidebarHeader>
<h4 className="p-2 rounded title-bg border text-center uppercase "> <h4 className="p-2 rounded title-bg border text-center uppercase ">
Sar Link Portal Sar Link Portal
</h4> </h4>
</SidebarHeader> </SidebarHeader>
<SidebarContent className="gap-0"> <SidebarContent className="gap-0">
{CATEGORIES.map((item) => { {CATEGORIES.map((item) => {
return ( return (
<Collapsible <Collapsible
key={item.id} key={item.id}
title={item.id} title={item.id}
defaultOpen defaultOpen
className="group/collapsible" className="group/collapsible"
> >
<SidebarGroup> <SidebarGroup>
<SidebarGroupLabel <SidebarGroupLabel
asChild asChild
className="group/label text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground" className="group/label text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
> >
<CollapsibleTrigger> <CollapsibleTrigger>
{item.id}{" "} {item.id}{" "}
<ChevronRight className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" /> <ChevronRight className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" />
</CollapsibleTrigger> </CollapsibleTrigger>
</SidebarGroupLabel> </SidebarGroupLabel>
<CollapsibleContent> <CollapsibleContent>
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu> <SidebarMenu>
{item.children.map((item) => ( {item.children.map((item) => (
<SidebarMenuItem key={item.title}> <SidebarMenuItem key={item.title}>
<SidebarMenuButton className="py-6" asChild> <SidebarMenuButton className="py-6" asChild>
<Link className="text-md" href={item.link}> <Link className="text-md" href={item.link}>
{item.icon} {item.icon}
<span className="opacity-70 ml-2"> <span
{item.title} className={`opacity-70 motion-preset-slide-left-md ml-2`}
</span> >
</Link> {item.title}
</SidebarMenuButton> </span>
</SidebarMenuItem> </Link>
))} </SidebarMenuButton>
</SidebarMenu> </SidebarMenuItem>
</SidebarGroupContent> ))}
</CollapsibleContent> </SidebarMenu>
</SidebarGroup> </SidebarGroupContent>
</Collapsible> </CollapsibleContent>
); </SidebarGroup>
})} </Collapsible>
</SidebarContent> );
<SidebarRail /> })}
</Sidebar> </SidebarContent>
); <SidebarRail />
</Sidebar>
);
} }

View File

@@ -1,7 +1,7 @@
"use client" "use client";
import { AspectRatio as AspectRatioPrimitive } from "radix-ui" import { AspectRatio as AspectRatioPrimitive } from "radix-ui";
const AspectRatio = AspectRatioPrimitive.Root const AspectRatio = AspectRatioPrimitive.Root;
export { AspectRatio } export { AspectRatio };

View File

@@ -1,50 +1,50 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { Avatar as AvatarPrimitive } from "radix-ui" import { Avatar as AvatarPrimitive } from "radix-ui";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Avatar = React.forwardRef< const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>, React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Root <AvatarPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className className,
)} )}
{...props} {...props}
/> />
)) ));
Avatar.displayName = AvatarPrimitive.Root.displayName Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef< const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>, React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Image <AvatarPrimitive.Image
ref={ref} ref={ref}
className={cn("aspect-square h-full w-full", className)} className={cn("aspect-square h-full w-full", className)}
{...props} {...props}
/> />
)) ));
AvatarImage.displayName = AvatarPrimitive.Image.displayName AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef< const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>, React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted", "flex h-full w-full items-center justify-center rounded-full bg-muted",
className className,
)} )}
{...props} {...props}
/> />
)) ));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback } export { Avatar, AvatarImage, AvatarFallback };

View File

@@ -1,36 +1,36 @@
import { type VariantProps, cva } from "class-variance-authority" import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const badgeVariants = cva( const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{ {
variants: { variants: {
variant: { variant: {
default: default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary: secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground", outline: "text-foreground",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
export interface BadgeProps export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>, extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> { } VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, ...props }: BadgeProps) {
return ( return (
<div className={cn(badgeVariants({ variant }), className)} {...props} /> <div className={cn(badgeVariants({ variant }), className)} {...props} />
) );
} }
export { Badge, badgeVariants } export { Badge, badgeVariants };

View File

@@ -1,115 +1,115 @@
import * as React from "react" import * as React from "react";
import { Slot as SlotPrimitive } from "radix-ui" import { Slot as SlotPrimitive } from "radix-ui";
import { ChevronRight, MoreHorizontal } from "lucide-react" import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Breadcrumb = React.forwardRef< const Breadcrumb = React.forwardRef<
HTMLElement, HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & { React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode separator?: React.ReactNode;
} }
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />) >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb" Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef< const BreadcrumbList = React.forwardRef<
HTMLOListElement, HTMLOListElement,
React.ComponentPropsWithoutRef<"ol"> React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<ol <ol
ref={ref} ref={ref}
className={cn( className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className className,
)} )}
{...props} {...props}
/> />
)) ));
BreadcrumbList.displayName = "BreadcrumbList" BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef< const BreadcrumbItem = React.forwardRef<
HTMLLIElement, HTMLLIElement,
React.ComponentPropsWithoutRef<"li"> React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<li <li
ref={ref} ref={ref}
className={cn("inline-flex items-center gap-1.5", className)} className={cn("inline-flex items-center gap-1.5", className)}
{...props} {...props}
/> />
)) ));
BreadcrumbItem.displayName = "BreadcrumbItem" BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef< const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement, HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & { React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean asChild?: boolean;
} }
>(({ asChild, className, ...props }, ref) => { >(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? SlotPrimitive.Slot : "a" const Comp = asChild ? SlotPrimitive.Slot : "a";
return ( return (
<Comp <Comp
ref={ref} ref={ref}
className={cn("transition-colors hover:text-foreground", className)} className={cn("transition-colors hover:text-foreground", className)}
{...props} {...props}
/> />
) );
}) });
BreadcrumbLink.displayName = "BreadcrumbLink" BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef< const BreadcrumbPage = React.forwardRef<
HTMLSpanElement, HTMLSpanElement,
React.ComponentPropsWithoutRef<"span"> React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<span <span
ref={ref} ref={ref}
role="link" role="link"
aria-disabled="true" aria-disabled="true"
aria-current="page" aria-current="page"
className={cn("font-normal text-foreground", className)} className={cn("font-normal text-foreground", className)}
{...props} {...props}
/> />
)) ));
BreadcrumbPage.displayName = "BreadcrumbPage" BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({ const BreadcrumbSeparator = ({
children, children,
className, className,
...props ...props
}: React.ComponentProps<"li">) => ( }: React.ComponentProps<"li">) => (
<li <li
role="presentation" role="presentation"
aria-hidden="true" aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)} className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props} {...props}
> >
{children ?? <ChevronRight />} {children ?? <ChevronRight />}
</li> </li>
) );
BreadcrumbSeparator.displayName = "BreadcrumbSeparator" BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({ const BreadcrumbEllipsis = ({
className, className,
...props ...props
}: React.ComponentProps<"span">) => ( }: React.ComponentProps<"span">) => (
<span <span
role="presentation" role="presentation"
aria-hidden="true" aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)} className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props} {...props}
> >
<MoreHorizontal className="h-4 w-4" /> <MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span> <span className="sr-only">More</span>
</span> </span>
) );
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis" BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export { export {
Breadcrumb, Breadcrumb,
BreadcrumbList, BreadcrumbList,
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator, BreadcrumbSeparator,
BreadcrumbEllipsis, BreadcrumbEllipsis,
} };

View File

@@ -1,59 +1,59 @@
import { Slot as SlotPrimitive } from "radix-ui" import { Slot as SlotPrimitive } from "radix-ui";
import { type VariantProps, cva } from "class-variance-authority" import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{ {
variants: { variants: {
variant: { variant: {
default: default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive: destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline: outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary: secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4", lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9", icon: "size-9",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
size: "default", size: "default",
}, },
} },
) );
function Button({ function Button({
className, className,
variant, variant,
size, size,
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"button"> & }: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & { VariantProps<typeof buttonVariants> & {
asChild?: boolean asChild?: boolean;
}) { }) {
const Comp = asChild ? SlotPrimitive.Slot : "button" const Comp = asChild ? SlotPrimitive.Slot : "button";
return ( return (
<Comp <Comp
data-slot="button" data-slot="button"
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
{...props} {...props}
/> />
) );
} }
export { Button, buttonVariants } export { Button, buttonVariants };

View File

@@ -1,210 +1,210 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { import {
ChevronDownIcon, ChevronDownIcon,
ChevronLeftIcon, ChevronLeftIcon,
ChevronRightIcon, ChevronRightIcon,
} from "lucide-react" } from "lucide-react";
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker" import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { Button, buttonVariants } from "@/components/ui/button" import { Button, buttonVariants } from "@/components/ui/button";
function Calendar({ function Calendar({
className, className,
classNames, classNames,
showOutsideDays = true, showOutsideDays = true,
captionLayout = "label", captionLayout = "label",
buttonVariant = "ghost", buttonVariant = "ghost",
formatters, formatters,
components, components,
...props ...props
}: React.ComponentProps<typeof DayPicker> & { }: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"] buttonVariant?: React.ComponentProps<typeof Button>["variant"];
}) { }) {
const defaultClassNames = getDefaultClassNames() const defaultClassNames = getDefaultClassNames();
return ( return (
<DayPicker <DayPicker
showOutsideDays={showOutsideDays} showOutsideDays={showOutsideDays}
className={cn( className={cn(
"bg-background group/calendar p-3 [--cell-size:2rem] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent", "bg-background group/calendar p-3 [--cell-size:2rem] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className className,
)} )}
captionLayout={captionLayout} captionLayout={captionLayout}
formatters={{ formatters={{
formatMonthDropdown: (date) => formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }), date.toLocaleString("default", { month: "short" }),
...formatters, ...formatters,
}} }}
classNames={{ classNames={{
root: cn("w-fit", defaultClassNames.root), root: cn("w-fit", defaultClassNames.root),
months: cn( months: cn(
"relative flex flex-col gap-4 md:flex-row", "relative flex flex-col gap-4 md:flex-row",
defaultClassNames.months defaultClassNames.months,
), ),
month: cn("flex w-full flex-col gap-4", defaultClassNames.month), month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
nav: cn( nav: cn(
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
defaultClassNames.nav defaultClassNames.nav,
), ),
button_previous: cn( button_previous: cn(
buttonVariants({ variant: buttonVariant }), buttonVariants({ variant: buttonVariant }),
"h-(--cell-size) w-(--cell-size) select-none p-0 aria-disabled:opacity-50", "h-(--cell-size) w-(--cell-size) select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_previous defaultClassNames.button_previous,
), ),
button_next: cn( button_next: cn(
buttonVariants({ variant: buttonVariant }), buttonVariants({ variant: buttonVariant }),
"h-(--cell-size) w-(--cell-size) select-none p-0 aria-disabled:opacity-50", "h-(--cell-size) w-(--cell-size) select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_next defaultClassNames.button_next,
), ),
month_caption: cn( month_caption: cn(
"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)", "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
defaultClassNames.month_caption defaultClassNames.month_caption,
), ),
dropdowns: cn( dropdowns: cn(
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium", "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
defaultClassNames.dropdowns defaultClassNames.dropdowns,
), ),
dropdown_root: cn( dropdown_root: cn(
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border", "has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
defaultClassNames.dropdown_root defaultClassNames.dropdown_root,
), ),
dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown), dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown),
caption_label: cn( caption_label: cn(
"select-none font-medium", "select-none font-medium",
captionLayout === "label" captionLayout === "label"
? "text-sm" ? "text-sm"
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5", : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
defaultClassNames.caption_label defaultClassNames.caption_label,
), ),
table: "w-full border-collapse", table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays), weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn( weekday: cn(
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal", "text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
defaultClassNames.weekday defaultClassNames.weekday,
), ),
week: cn("mt-2 flex w-full", defaultClassNames.week), week: cn("mt-2 flex w-full", defaultClassNames.week),
week_number_header: cn( week_number_header: cn(
"w-(--cell-size) select-none", "w-(--cell-size) select-none",
defaultClassNames.week_number_header defaultClassNames.week_number_header,
), ),
week_number: cn( week_number: cn(
"text-muted-foreground select-none text-[0.8rem]", "text-muted-foreground select-none text-[0.8rem]",
defaultClassNames.week_number defaultClassNames.week_number,
), ),
day: cn( day: cn(
"group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md", "group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
defaultClassNames.day defaultClassNames.day,
), ),
range_start: cn( range_start: cn(
"bg-accent rounded-l-md", "bg-accent rounded-l-md",
defaultClassNames.range_start defaultClassNames.range_start,
), ),
range_middle: cn("rounded-none", defaultClassNames.range_middle), range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end), range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
today: cn( today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today defaultClassNames.today,
), ),
outside: cn( outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground", "text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside defaultClassNames.outside,
), ),
disabled: cn( disabled: cn(
"text-muted-foreground opacity-50", "text-muted-foreground opacity-50",
defaultClassNames.disabled defaultClassNames.disabled,
), ),
hidden: cn("invisible", defaultClassNames.hidden), hidden: cn("invisible", defaultClassNames.hidden),
...classNames, ...classNames,
}} }}
components={{ components={{
Root: ({ className, rootRef, ...props }) => { Root: ({ className, rootRef, ...props }) => {
return ( return (
<div <div
data-slot="calendar" data-slot="calendar"
ref={rootRef} ref={rootRef}
className={cn(className)} className={cn(className)}
{...props} {...props}
/> />
) );
}, },
Chevron: ({ className, orientation, ...props }) => { Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") { if (orientation === "left") {
return ( return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} /> <ChevronLeftIcon className={cn("size-4", className)} {...props} />
) );
} }
if (orientation === "right") { if (orientation === "right") {
return ( return (
<ChevronRightIcon <ChevronRightIcon
className={cn("size-4", className)} className={cn("size-4", className)}
{...props} {...props}
/> />
) );
} }
return ( return (
<ChevronDownIcon className={cn("size-4", className)} {...props} /> <ChevronDownIcon className={cn("size-4", className)} {...props} />
) );
}, },
DayButton: CalendarDayButton, DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => { WeekNumber: ({ children, ...props }) => {
return ( return (
<td {...props}> <td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center"> <div className="flex size-(--cell-size) items-center justify-center text-center">
{children} {children}
</div> </div>
</td> </td>
) );
}, },
...components, ...components,
}} }}
{...props} {...props}
/> />
) );
} }
function CalendarDayButton({ function CalendarDayButton({
className, className,
day, day,
modifiers, modifiers,
...props ...props
}: React.ComponentProps<typeof DayButton>) { }: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames() const defaultClassNames = getDefaultClassNames();
const ref = React.useRef<HTMLButtonElement>(null) const ref = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => { React.useEffect(() => {
if (modifiers.focused) ref.current?.focus() if (modifiers.focused) ref.current?.focus();
}, [modifiers.focused]) }, [modifiers.focused]);
return ( return (
<Button <Button
ref={ref} ref={ref}
variant="ghost" variant="ghost"
size="icon" size="icon"
data-day={day.date.toLocaleDateString()} data-day={day.date.toLocaleDateString()}
data-selected-single={ data-selected-single={
modifiers.selected && modifiers.selected &&
!modifiers.range_start && !modifiers.range_start &&
!modifiers.range_end && !modifiers.range_end &&
!modifiers.range_middle !modifiers.range_middle
} }
data-range-start={modifiers.range_start} data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end} data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle} data-range-middle={modifiers.range_middle}
className={cn( className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-(--cell-size) flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70", "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-(--cell-size) flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day, defaultClassNames.day,
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { Calendar, CalendarDayButton } export { Calendar, CalendarDayButton };

View File

@@ -1,76 +1,83 @@
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Card = React.forwardRef< const Card = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"rounded-xl border bg-card text-card-foreground shadow", "rounded-xl border bg-card text-card-foreground shadow",
className className,
)} )}
{...props} {...props}
/> />
)) ));
Card.displayName = "Card" Card.displayName = "Card";
const CardHeader = React.forwardRef< const CardHeader = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)} className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props} {...props}
/> />
)) ));
CardHeader.displayName = "CardHeader" CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef< const CardTitle = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)} className={cn("font-semibold leading-none tracking-tight", className)}
{...props} {...props}
/> />
)) ));
CardTitle.displayName = "CardTitle" CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef< const CardDescription = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)) ));
CardDescription.displayName = "CardDescription" CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef< const CardContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
)) ));
CardContent.displayName = "CardContent" CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef< const CardFooter = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn("flex items-center p-6 pt-0", className)} className={cn("flex items-center p-6 pt-0", className)}
{...props} {...props}
/> />
)) ));
CardFooter.displayName = "CardFooter" CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};

View File

@@ -1,262 +1,262 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import useEmblaCarousel, { import useEmblaCarousel, {
type UseEmblaCarouselType, type UseEmblaCarouselType,
} from "embla-carousel-react" } from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react" import { ArrowLeft, ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
type CarouselApi = UseEmblaCarouselType[1] type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel> type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0] type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1] type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = { type CarouselProps = {
opts?: CarouselOptions opts?: CarouselOptions;
plugins?: CarouselPlugin plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical" orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void setApi?: (api: CarouselApi) => void;
} };
type CarouselContextProps = { type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0] carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1] api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void scrollPrev: () => void;
scrollNext: () => void scrollNext: () => void;
canScrollPrev: boolean canScrollPrev: boolean;
canScrollNext: boolean canScrollNext: boolean;
} & CarouselProps } & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null) const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() { function useCarousel() {
const context = React.useContext(CarouselContext) const context = React.useContext(CarouselContext);
if (!context) { if (!context) {
throw new Error("useCarousel must be used within a <Carousel />") throw new Error("useCarousel must be used within a <Carousel />");
} }
return context return context;
} }
const Carousel = React.forwardRef< const Carousel = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps React.HTMLAttributes<HTMLDivElement> & CarouselProps
>( >(
( (
{ {
orientation = "horizontal", orientation = "horizontal",
opts, opts,
setApi, setApi,
plugins, plugins,
className, className,
children, children,
...props ...props
}, },
ref ref,
) => { ) => {
const [carouselRef, api] = useEmblaCarousel( const [carouselRef, api] = useEmblaCarousel(
{ {
...opts, ...opts,
axis: orientation === "horizontal" ? "x" : "y", axis: orientation === "horizontal" ? "x" : "y",
}, },
plugins plugins,
) );
const [canScrollPrev, setCanScrollPrev] = React.useState(false) const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false) const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => { const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) { if (!api) {
return return;
} }
setCanScrollPrev(api.canScrollPrev()) setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext()) setCanScrollNext(api.canScrollNext());
}, []) }, []);
const scrollPrev = React.useCallback(() => { const scrollPrev = React.useCallback(() => {
api?.scrollPrev() api?.scrollPrev();
}, [api]) }, [api]);
const scrollNext = React.useCallback(() => { const scrollNext = React.useCallback(() => {
api?.scrollNext() api?.scrollNext();
}, [api]) }, [api]);
const handleKeyDown = React.useCallback( const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => { (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") { if (event.key === "ArrowLeft") {
event.preventDefault() event.preventDefault();
scrollPrev() scrollPrev();
} else if (event.key === "ArrowRight") { } else if (event.key === "ArrowRight") {
event.preventDefault() event.preventDefault();
scrollNext() scrollNext();
} }
}, },
[scrollPrev, scrollNext] [scrollPrev, scrollNext],
) );
React.useEffect(() => { React.useEffect(() => {
if (!api || !setApi) { if (!api || !setApi) {
return return;
} }
setApi(api) setApi(api);
}, [api, setApi]) }, [api, setApi]);
React.useEffect(() => { React.useEffect(() => {
if (!api) { if (!api) {
return return;
} }
onSelect(api) onSelect(api);
api.on("reInit", onSelect) api.on("reInit", onSelect);
api.on("select", onSelect) api.on("select", onSelect);
return () => { return () => {
api?.off("select", onSelect) api?.off("select", onSelect);
} };
}, [api, onSelect]) }, [api, onSelect]);
return ( return (
<CarouselContext.Provider <CarouselContext.Provider
value={{ value={{
carouselRef, carouselRef,
api: api, api: api,
opts, opts,
orientation: orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"), orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev, scrollPrev,
scrollNext, scrollNext,
canScrollPrev, canScrollPrev,
canScrollNext, canScrollNext,
}} }}
> >
<div <div
ref={ref} ref={ref}
onKeyDownCapture={handleKeyDown} onKeyDownCapture={handleKeyDown}
className={cn("relative", className)} className={cn("relative", className)}
role="region" role="region"
aria-roledescription="carousel" aria-roledescription="carousel"
{...props} {...props}
> >
{children} {children}
</div> </div>
</CarouselContext.Provider> </CarouselContext.Provider>
) );
} },
) );
Carousel.displayName = "Carousel" Carousel.displayName = "Carousel";
const CarouselContent = React.forwardRef< const CarouselContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel() const { carouselRef, orientation } = useCarousel();
return ( return (
<div ref={carouselRef} className="overflow-hidden"> <div ref={carouselRef} className="overflow-hidden">
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"flex", "flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className className,
)} )}
{...props} {...props}
/> />
</div> </div>
) );
}) });
CarouselContent.displayName = "CarouselContent" CarouselContent.displayName = "CarouselContent";
const CarouselItem = React.forwardRef< const CarouselItem = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { orientation } = useCarousel() const { orientation } = useCarousel();
return ( return (
<div <div
ref={ref} ref={ref}
role="group" role="group"
aria-roledescription="slide" aria-roledescription="slide"
className={cn( className={cn(
"min-w-0 shrink-0 grow-0 basis-full", "min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4", orientation === "horizontal" ? "pl-4" : "pt-4",
className className,
)} )}
{...props} {...props}
/> />
) );
}) });
CarouselItem.displayName = "CarouselItem" CarouselItem.displayName = "CarouselItem";
const CarouselPrevious = React.forwardRef< const CarouselPrevious = React.forwardRef<
HTMLButtonElement, HTMLButtonElement,
React.ComponentProps<typeof Button> React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => { >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel() const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return ( return (
<Button <Button
ref={ref} ref={ref}
variant={variant} variant={variant}
size={size} size={size}
className={cn( className={cn(
"absolute h-8 w-8 rounded-full", "absolute h-8 w-8 rounded-full",
orientation === "horizontal" orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2" ? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90", : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className className,
)} )}
disabled={!canScrollPrev} disabled={!canScrollPrev}
onClick={scrollPrev} onClick={scrollPrev}
{...props} {...props}
> >
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span> <span className="sr-only">Previous slide</span>
</Button> </Button>
) );
}) });
CarouselPrevious.displayName = "CarouselPrevious" CarouselPrevious.displayName = "CarouselPrevious";
const CarouselNext = React.forwardRef< const CarouselNext = React.forwardRef<
HTMLButtonElement, HTMLButtonElement,
React.ComponentProps<typeof Button> React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => { >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel() const { orientation, scrollNext, canScrollNext } = useCarousel();
return ( return (
<Button <Button
ref={ref} ref={ref}
variant={variant} variant={variant}
size={size} size={size}
className={cn( className={cn(
"absolute h-8 w-8 rounded-full", "absolute h-8 w-8 rounded-full",
orientation === "horizontal" orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2" ? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className className,
)} )}
disabled={!canScrollNext} disabled={!canScrollNext}
onClick={scrollNext} onClick={scrollNext}
{...props} {...props}
> >
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span> <span className="sr-only">Next slide</span>
</Button> </Button>
) );
}) });
CarouselNext.displayName = "CarouselNext" CarouselNext.displayName = "CarouselNext";
export { export {
type CarouselApi, type CarouselApi,
Carousel, Carousel,
CarouselContent, CarouselContent,
CarouselItem, CarouselItem,
CarouselPrevious, CarouselPrevious,
CarouselNext, CarouselNext,
} };

View File

@@ -1,30 +1,30 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { Checkbox as CheckboxPrimitive } from "radix-ui" import { Checkbox as CheckboxPrimitive } from "radix-ui";
import { Check } from "lucide-react" import { Check } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef< const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>, React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", "peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className className,
)} )}
{...props} {...props}
> >
<CheckboxPrimitive.Indicator <CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")} className={cn("flex items-center justify-center text-current")}
> >
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator> </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root> </CheckboxPrimitive.Root>
)) ));
Checkbox.displayName = CheckboxPrimitive.Root.displayName Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox } export { Checkbox };

View File

@@ -1,11 +1,11 @@
"use client" "use client";
import { Collapsible as CollapsiblePrimitive } from "radix-ui" import { Collapsible as CollapsiblePrimitive } from "radix-ui";
const Collapsible = CollapsiblePrimitive.Root const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent } export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View File

@@ -1,184 +1,184 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { Command as CommandPrimitive } from "cmdk" import { Command as CommandPrimitive } from "cmdk";
import { SearchIcon } from "lucide-react" import { SearchIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog" } from "@/components/ui/dialog";
function Command({ function Command({
className, className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive>) { }: React.ComponentProps<typeof CommandPrimitive>) {
return ( return (
<CommandPrimitive <CommandPrimitive
data-slot="command" data-slot="command"
className={cn( className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md", "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CommandDialog({ function CommandDialog({
title = "Command Palette", title = "Command Palette",
description = "Search for a command to run...", description = "Search for a command to run...",
children, children,
className, className,
showCloseButton = true, showCloseButton = true,
...props ...props
}: React.ComponentProps<typeof Dialog> & { }: React.ComponentProps<typeof Dialog> & {
title?: string title?: string;
description?: string description?: string;
className?: string className?: string;
showCloseButton?: boolean showCloseButton?: boolean;
}) { }) {
return ( return (
<Dialog {...props}> <Dialog {...props}>
<DialogHeader className="sr-only"> <DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle> <DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription> <DialogDescription>{description}</DialogDescription>
</DialogHeader> </DialogHeader>
<DialogContent <DialogContent
className={cn("overflow-hidden p-0", className)} className={cn("overflow-hidden p-0", className)}
showCloseButton={showCloseButton} showCloseButton={showCloseButton}
> >
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> <Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children} {children}
</Command> </Command>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) );
} }
function CommandInput({ function CommandInput({
className, className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) { }: React.ComponentProps<typeof CommandPrimitive.Input>) {
return ( return (
<div <div
data-slot="command-input-wrapper" data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3" className="flex h-9 items-center gap-2 border-b px-3"
> >
<SearchIcon className="size-4 shrink-0 opacity-50" /> <SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input <CommandPrimitive.Input
data-slot="command-input" data-slot="command-input"
className={cn( className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50", "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className className,
)} )}
{...props} {...props}
/> />
</div> </div>
) );
} }
function CommandList({ function CommandList({
className, className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.List>) { }: React.ComponentProps<typeof CommandPrimitive.List>) {
return ( return (
<CommandPrimitive.List <CommandPrimitive.List
data-slot="command-list" data-slot="command-list"
className={cn( className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CommandEmpty({ function CommandEmpty({
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) { }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return ( return (
<CommandPrimitive.Empty <CommandPrimitive.Empty
data-slot="command-empty" data-slot="command-empty"
className="py-6 text-center text-sm" className="py-6 text-center text-sm"
{...props} {...props}
/> />
) );
} }
function CommandGroup({ function CommandGroup({
className, className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) { }: React.ComponentProps<typeof CommandPrimitive.Group>) {
return ( return (
<CommandPrimitive.Group <CommandPrimitive.Group
data-slot="command-group" data-slot="command-group"
className={cn( className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium", "text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CommandSeparator({ function CommandSeparator({
className, className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) { }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return ( return (
<CommandPrimitive.Separator <CommandPrimitive.Separator
data-slot="command-separator" data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)} className={cn("bg-border -mx-1 h-px", className)}
{...props} {...props}
/> />
) );
} }
function CommandItem({ function CommandItem({
className, className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) { }: React.ComponentProps<typeof CommandPrimitive.Item>) {
return ( return (
<CommandPrimitive.Item <CommandPrimitive.Item
data-slot="command-item" data-slot="command-item"
className={cn( className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CommandShortcut({ function CommandShortcut({
className, className,
...props ...props
}: React.ComponentProps<"span">) { }: React.ComponentProps<"span">) {
return ( return (
<span <span
data-slot="command-shortcut" data-slot="command-shortcut"
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", "text-muted-foreground ml-auto text-xs tracking-widest",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
Command, Command,
CommandDialog, CommandDialog,
CommandInput, CommandInput,
CommandList, CommandList,
CommandEmpty, CommandEmpty,
CommandGroup, CommandGroup,
CommandItem, CommandItem,
CommandShortcut, CommandShortcut,
CommandSeparator, CommandSeparator,
} };

View File

@@ -1,200 +1,200 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { ContextMenu as ContextMenuPrimitive } from "radix-ui" import { ContextMenu as ContextMenuPrimitive } from "radix-ui";
import { Check, ChevronRight, Circle } from "lucide-react" import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const ContextMenu = ContextMenuPrimitive.Root const ContextMenu = ContextMenuPrimitive.Root;
const ContextMenuTrigger = ContextMenuPrimitive.Trigger const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
const ContextMenuGroup = ContextMenuPrimitive.Group const ContextMenuGroup = ContextMenuPrimitive.Group;
const ContextMenuPortal = ContextMenuPrimitive.Portal const ContextMenuPortal = ContextMenuPrimitive.Portal;
const ContextMenuSub = ContextMenuPrimitive.Sub const ContextMenuSub = ContextMenuPrimitive.Sub;
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
const ContextMenuSubTrigger = React.forwardRef< const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>, React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & { React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, children, ...props }, ref) => ( >(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger <ContextMenuPrimitive.SubTrigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRight className="ml-auto h-4 w-4" /> <ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger> </ContextMenuPrimitive.SubTrigger>
)) ));
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
const ContextMenuSubContent = React.forwardRef< const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>, React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent> React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent <ContextMenuPrimitive.SubContent
ref={ref} ref={ref}
className={cn( className={cn(
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin)", "z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin)",
className className,
)} )}
{...props} {...props}
/> />
)) ));
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
const ContextMenuContent = React.forwardRef< const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>, React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content> React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal> <ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content <ContextMenuPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"z-50 max-h-(--radix-context-menu-content-available-height) min-w-32 overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin)", "z-50 max-h-(--radix-context-menu-content-available-height) min-w-32 overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin)",
className className,
)} )}
{...props} {...props}
/> />
</ContextMenuPrimitive.Portal> </ContextMenuPrimitive.Portal>
)) ));
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
const ContextMenuItem = React.forwardRef< const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>, React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & { React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item <ContextMenuPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
/> />
)) ));
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
const ContextMenuCheckboxItem = React.forwardRef< const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>, React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem> React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => ( >(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem <ContextMenuPrimitive.CheckboxItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className className,
)} )}
checked={checked} checked={checked}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator> <ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator> </ContextMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</ContextMenuPrimitive.CheckboxItem> </ContextMenuPrimitive.CheckboxItem>
)) ));
ContextMenuCheckboxItem.displayName = ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName ContextMenuPrimitive.CheckboxItem.displayName;
const ContextMenuRadioItem = React.forwardRef< const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>, React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem> React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem <ContextMenuPrimitive.RadioItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className className,
)} )}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator> <ContextMenuPrimitive.ItemIndicator>
<Circle className="h-4 w-4 fill-current" /> <Circle className="h-4 w-4 fill-current" />
</ContextMenuPrimitive.ItemIndicator> </ContextMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</ContextMenuPrimitive.RadioItem> </ContextMenuPrimitive.RadioItem>
)) ));
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
const ContextMenuLabel = React.forwardRef< const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>, React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & { React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label <ContextMenuPrimitive.Label
ref={ref} ref={ref}
className={cn( className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground", "px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
/> />
)) ));
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
const ContextMenuSeparator = React.forwardRef< const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>, React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator> React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator <ContextMenuPrimitive.Separator
ref={ref} ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)} className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props} {...props}
/> />
)) ));
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
const ContextMenuShortcut = ({ const ContextMenuShortcut = ({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLSpanElement>) => { }: React.HTMLAttributes<HTMLSpanElement>) => {
return ( return (
<span <span
className={cn( className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground", "ml-auto text-xs tracking-widest text-muted-foreground",
className className,
)} )}
{...props} {...props}
/> />
) );
} };
ContextMenuShortcut.displayName = "ContextMenuShortcut" ContextMenuShortcut.displayName = "ContextMenuShortcut";
export { export {
ContextMenu, ContextMenu,
ContextMenuTrigger, ContextMenuTrigger,
ContextMenuContent, ContextMenuContent,
ContextMenuItem, ContextMenuItem,
ContextMenuCheckboxItem, ContextMenuCheckboxItem,
ContextMenuRadioItem, ContextMenuRadioItem,
ContextMenuLabel, ContextMenuLabel,
ContextMenuSeparator, ContextMenuSeparator,
ContextMenuShortcut, ContextMenuShortcut,
ContextMenuGroup, ContextMenuGroup,
ContextMenuPortal, ContextMenuPortal,
ContextMenuSub, ContextMenuSub,
ContextMenuSubContent, ContextMenuSubContent,
ContextMenuSubTrigger, ContextMenuSubTrigger,
ContextMenuRadioGroup, ContextMenuRadioGroup,
} };

View File

@@ -1,129 +1,136 @@
import { eachMonthOfInterval, endOfYear, format, startOfYear } from "date-fns" import { eachMonthOfInterval, endOfYear, format, startOfYear } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react" import { Calendar as CalendarIcon } from "lucide-react";
import * as React from "react" import * as React from "react";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar" import { Calendar } from "@/components/ui/calendar";
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover" } from "@/components/ui/popover";
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select" } from "@/components/ui/select";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
interface DatePickerProps { interface DatePickerProps {
date: Date | undefined date: Date | undefined;
setDate: (date: Date | undefined) => void setDate: (date: Date | undefined) => void;
} }
export function DatePicker({ date, setDate }: DatePickerProps) { export function DatePicker({ date, setDate }: DatePickerProps) {
const [month, setMonth] = React.useState<number>(date ? date.getMonth() : new Date().getMonth()) const [month, setMonth] = React.useState<number>(
const [year, setYear] = React.useState<number>(date ? date.getFullYear() : new Date().getFullYear()) date ? date.getMonth() : new Date().getMonth(),
);
const [year, setYear] = React.useState<number>(
date ? date.getFullYear() : new Date().getFullYear(),
);
const years = React.useMemo(() => { const years = React.useMemo(() => {
const currentYear = new Date().getFullYear() const currentYear = new Date().getFullYear();
return Array.from({ length: currentYear - 1900 + 1 }, (_, i) => currentYear - i) return Array.from(
}, []) { length: currentYear - 1900 + 1 },
(_, i) => currentYear - i,
);
}, []);
const months = React.useMemo(() => { const months = React.useMemo(() => {
if (year) { if (year) {
return eachMonthOfInterval({ return eachMonthOfInterval({
start: startOfYear(new Date(year, 0, 1)), start: startOfYear(new Date(year, 0, 1)),
end: endOfYear(new Date(year, 0, 1)) end: endOfYear(new Date(year, 0, 1)),
}) });
} }
return [] return [];
}, [year]) }, [year]);
React.useEffect(() => { React.useEffect(() => {
if (date) { if (date) {
setMonth(date.getMonth()) setMonth(date.getMonth());
setYear(date.getFullYear()) setYear(date.getFullYear());
} }
}, [date]) }, [date]);
const handleYearChange = (selectedYear: string) => { const handleYearChange = (selectedYear: string) => {
const newYear = Number.parseInt(selectedYear, 10) const newYear = Number.parseInt(selectedYear, 10);
setYear(newYear) setYear(newYear);
if (date) { if (date) {
const newDate = new Date(date) const newDate = new Date(date);
newDate.setFullYear(newYear) newDate.setFullYear(newYear);
setDate(newDate) setDate(newDate);
} }
} };
const handleMonthChange = (selectedMonth: string) => { const handleMonthChange = (selectedMonth: string) => {
const newMonth = Number.parseInt(selectedMonth, 10) const newMonth = Number.parseInt(selectedMonth, 10);
setMonth(newMonth) setMonth(newMonth);
if (date) { if (date) {
const newDate = new Date(date) const newDate = new Date(date);
newDate.setMonth(newMonth) newDate.setMonth(newMonth);
setDate(newDate) setDate(newDate);
} else { } else {
setDate(new Date(year, newMonth, 1)) setDate(new Date(year, newMonth, 1));
} }
} };
return ( return (
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant={"outline"} variant={"outline"}
className={cn( className={cn(
"w-full justify-start text-left font-normal", "w-full justify-start text-left font-normal",
!date && "text-muted-foreground" !date && "text-muted-foreground",
)} )}
> >
<CalendarIcon className="mr-2 h-4 w-4" /> <CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "PPP") : <span>Pick a date</span>} {date ? format(date, "PPP") : <span>Pick a date</span>}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start"> <PopoverContent className="w-auto p-0" align="start">
<div className="flex justify-between p-2 space-x-1"> <div className="flex justify-between p-2 space-x-1">
<Select onValueChange={handleYearChange} value={year.toString()}> <Select onValueChange={handleYearChange} value={year.toString()}>
<SelectTrigger className="w-[120px]"> <SelectTrigger className="w-[120px]">
<SelectValue placeholder="Year" /> <SelectValue placeholder="Year" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{years.map((y) => ( {years.map((y) => (
<SelectItem key={y} value={y.toString()}> <SelectItem key={y} value={y.toString()}>
{y} {y}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
<Select onValueChange={handleMonthChange} value={month.toString()}> <Select onValueChange={handleMonthChange} value={month.toString()}>
<SelectTrigger className="w-[120px]"> <SelectTrigger className="w-[120px]">
<SelectValue placeholder="Month" /> <SelectValue placeholder="Month" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{months.map((m, index) => ( {months.map((m, index) => (
<SelectItem key={`${index + 1}`} value={index.toString()}> <SelectItem key={`${index + 1}`} value={index.toString()}>
{format(m, "MMMM")} {format(m, "MMMM")}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<Calendar <Calendar
mode="single" mode="single"
selected={date} selected={date}
onSelect={setDate} onSelect={setDate}
month={new Date(year, month)} month={new Date(year, month)}
onMonthChange={(newMonth) => { onMonthChange={(newMonth) => {
setMonth(newMonth.getMonth()) setMonth(newMonth.getMonth());
setYear(newMonth.getFullYear()) setYear(newMonth.getFullYear());
}} }}
initialFocus initialFocus
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
) );
} }

View File

@@ -1,143 +1,143 @@
"use client" "use client";
import { XIcon } from "lucide-react" import { XIcon } from "lucide-react";
import { Dialog as DialogPrimitive } from "radix-ui" import { Dialog as DialogPrimitive } from "radix-ui";
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Dialog({ function Dialog({
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) { }: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} /> return <DialogPrimitive.Root data-slot="dialog" {...props} />;
} }
function DialogTrigger({ function DialogTrigger({
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) { }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} /> return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
} }
function DialogPortal({ function DialogPortal({
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) { }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} /> return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
} }
function DialogClose({ function DialogClose({
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) { }: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} /> return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
} }
function DialogOverlay({ function DialogOverlay({
className, className,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) { }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return ( return (
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
data-slot="dialog-overlay" data-slot="dialog-overlay"
className={cn( className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DialogContent({ function DialogContent({
className, className,
children, children,
showCloseButton = true, showCloseButton = true,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & { }: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean showCloseButton?: boolean;
}) { }) {
return ( return (
<DialogPortal data-slot="dialog-portal"> <DialogPortal data-slot="dialog-portal">
<DialogOverlay /> <DialogOverlay />
<DialogPrimitive.Content <DialogPrimitive.Content
data-slot="dialog-content" data-slot="dialog-content"
className={cn( className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg", "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
{showCloseButton && ( {showCloseButton && (
<DialogPrimitive.Close <DialogPrimitive.Close
data-slot="dialog-close" data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
> >
<XIcon /> <XIcon />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
)} )}
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
) );
} }
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="dialog-header" data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)} className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props} {...props}
/> />
) );
} }
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="dialog-footer" data-slot="dialog-footer"
className={cn( className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DialogTitle({ function DialogTitle({
className, className,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) { }: React.ComponentProps<typeof DialogPrimitive.Title>) {
return ( return (
<DialogPrimitive.Title <DialogPrimitive.Title
data-slot="dialog-title" data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)} className={cn("text-lg leading-none font-semibold", className)}
{...props} {...props}
/> />
) );
} }
function DialogDescription({ function DialogDescription({
className, className,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) { }: React.ComponentProps<typeof DialogPrimitive.Description>) {
return ( return (
<DialogPrimitive.Description <DialogPrimitive.Description
data-slot="dialog-description" data-slot="dialog-description"
className={cn("text-start text-muted-foreground text-sm", className)} className={cn("text-start text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
) );
} }
export { export {
Dialog, Dialog,
DialogClose, DialogClose,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogOverlay, DialogOverlay,
DialogPortal, DialogPortal,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} };

View File

@@ -1,118 +1,118 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul" import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Drawer = ({ const Drawer = ({
shouldScaleBackground = true, shouldScaleBackground = true,
...props ...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => ( }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root <DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground} shouldScaleBackground={shouldScaleBackground}
{...props} {...props}
/> />
) );
Drawer.displayName = "Drawer" Drawer.displayName = "Drawer";
const DrawerTrigger = DrawerPrimitive.Trigger const DrawerTrigger = DrawerPrimitive.Trigger;
const DrawerPortal = DrawerPrimitive.Portal const DrawerPortal = DrawerPrimitive.Portal;
const DrawerClose = DrawerPrimitive.Close const DrawerClose = DrawerPrimitive.Close;
const DrawerOverlay = React.forwardRef< const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>, React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay> React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay <DrawerPrimitive.Overlay
ref={ref} ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)} className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props} {...props}
/> />
)) ));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
const DrawerContent = React.forwardRef< const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>, React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<DrawerPortal> <DrawerPortal>
<DrawerOverlay /> <DrawerOverlay />
<DrawerPrimitive.Content <DrawerPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background", "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className className,
)} )}
{...props} {...props}
> >
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" /> <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children} {children}
</DrawerPrimitive.Content> </DrawerPrimitive.Content>
</DrawerPortal> </DrawerPortal>
)) ));
DrawerContent.displayName = "DrawerContent" DrawerContent.displayName = "DrawerContent";
const DrawerHeader = ({ const DrawerHeader = ({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props} {...props}
/> />
) );
DrawerHeader.displayName = "DrawerHeader" DrawerHeader.displayName = "DrawerHeader";
const DrawerFooter = ({ const DrawerFooter = ({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn("mt-auto flex flex-col gap-2 p-4", className)} className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props} {...props}
/> />
) );
DrawerFooter.displayName = "DrawerFooter" DrawerFooter.displayName = "DrawerFooter";
const DrawerTitle = React.forwardRef< const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>, React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title> React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Title <DrawerPrimitive.Title
ref={ref} ref={ref}
className={cn( className={cn(
"text-lg font-semibold leading-none tracking-tight", "text-lg font-semibold leading-none tracking-tight",
className className,
)} )}
{...props} {...props}
/> />
)) ));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
const DrawerDescription = React.forwardRef< const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>, React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description> React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Description <DrawerPrimitive.Description
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)) ));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
export { export {
Drawer, Drawer,
DrawerPortal, DrawerPortal,
DrawerOverlay, DrawerOverlay,
DrawerTrigger, DrawerTrigger,
DrawerClose, DrawerClose,
DrawerContent, DrawerContent,
DrawerHeader, DrawerHeader,
DrawerFooter, DrawerFooter,
DrawerTitle, DrawerTitle,
DrawerDescription, DrawerDescription,
} };

View File

@@ -1,201 +1,201 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui" import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
import { Check, ChevronRight, Circle } from "lucide-react" import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef< const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, children, ...props }, ref) => ( >(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRight className="ml-auto" /> <ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
)) ));
DropdownMenuSubTrigger.displayName = DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef< const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
ref={ref} ref={ref}
className={cn( className={cn(
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)", "z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)",
className className,
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuSubContent.displayName = DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => ( >(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal> <DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content <DropdownMenuPrimitive.Content
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-32 overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md", "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-32 overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)",
className className,
)} )}
{...props} {...props}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
)) ));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef< const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>, React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0", "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef< const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => ( >(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className className,
)} )}
checked={checked} checked={checked}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
)) ));
DropdownMenuCheckboxItem.displayName = DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef< const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className className,
)} )}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" /> <Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
)) ));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef< const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>, React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
ref={ref} ref={ref}
className={cn( className={cn(
"px-2 py-1.5 text-sm font-semibold", "px-2 py-1.5 text-sm font-semibold",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef< const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>, React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
ref={ref} ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)} className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props} {...props}
/> />
)) ));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ const DropdownMenuShortcut = ({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLSpanElement>) => { }: React.HTMLAttributes<HTMLSpanElement>) => {
return ( return (
<span <span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)} className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props} {...props}
/> />
) );
} };
DropdownMenuShortcut.displayName = "DropdownMenuShortcut" DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export { export {
DropdownMenu, DropdownMenu,
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuRadioItem, DropdownMenuRadioItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuShortcut, DropdownMenuShortcut,
DropdownMenuGroup, DropdownMenuGroup,
DropdownMenuPortal, DropdownMenuPortal,
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuRadioGroup, DropdownMenuRadioGroup,
} };

View File

@@ -1,50 +1,56 @@
'use client'; "use client";
import * as SliderPrimitive from '@radix-ui/react-slider'; import * as SliderPrimitive from "@radix-ui/react-slider";
import * as React from 'react'; import * as React from "react";
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
interface DualRangeSliderProps extends React.ComponentProps<typeof SliderPrimitive.Root> { interface DualRangeSliderProps
labelPosition?: 'top' | 'bottom'; extends React.ComponentProps<typeof SliderPrimitive.Root> {
label?: (value: number | undefined) => React.ReactNode; labelPosition?: "top" | "bottom";
label?: (value: number | undefined) => React.ReactNode;
} }
const DualRangeSlider = React.forwardRef< const DualRangeSlider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>, React.ElementRef<typeof SliderPrimitive.Root>,
DualRangeSliderProps DualRangeSliderProps
>(({ className, label, labelPosition = 'top', ...props }, ref) => { >(({ className, label, labelPosition = "top", ...props }, ref) => {
const initialValue = Array.isArray(props.value) ? props.value : [props.min, props.max]; const initialValue = Array.isArray(props.value)
? props.value
: [props.min, props.max];
return ( return (
<SliderPrimitive.Root <SliderPrimitive.Root
ref={ref} ref={ref}
className={cn('relative flex w-full touch-none select-none items-center', className)} className={cn(
{...props} "relative flex w-full touch-none select-none items-center",
> className,
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary"> )}
<SliderPrimitive.Range className="absolute h-full bg-primary" /> {...props}
</SliderPrimitive.Track> >
{initialValue.map((value, index) => ( <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<React.Fragment key={`${index + 1}`}> <SliderPrimitive.Range className="absolute h-full bg-primary" />
<SliderPrimitive.Thumb className="relative block h-4 w-4 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"> </SliderPrimitive.Track>
{label && ( {initialValue.map((value, index) => (
<span <React.Fragment key={`${index + 1}`}>
className={cn( <SliderPrimitive.Thumb className="relative block h-4 w-4 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50">
'absolute flex w-full justify-center', {label && (
labelPosition === 'top' && '-top-7', <span
labelPosition === 'bottom' && 'top-4', className={cn(
)} "absolute flex w-full justify-center",
> labelPosition === "top" && "-top-7",
{label(value)} labelPosition === "bottom" && "top-4",
</span> )}
)} >
</SliderPrimitive.Thumb> {label(value)}
</React.Fragment> </span>
))} )}
</SliderPrimitive.Root> </SliderPrimitive.Thumb>
); </React.Fragment>
))}
</SliderPrimitive.Root>
);
}); });
DualRangeSlider.displayName = 'DualRangeSlider'; DualRangeSlider.displayName = "DualRangeSlider";
export { DualRangeSlider }; export { DualRangeSlider };

View File

@@ -0,0 +1,55 @@
import * as React from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
const FloatingInput = React.forwardRef<
HTMLInputElement,
React.InputHTMLAttributes<HTMLInputElement>
>(({ className, ...props }, ref) => {
return (
<Input
placeholder=" "
className={cn("peer", className)}
ref={ref}
{...props}
/>
);
});
FloatingInput.displayName = "FloatingInput";
const FloatingLabel = React.forwardRef<
React.ElementRef<typeof Label>,
React.ComponentPropsWithoutRef<typeof Label>
>(({ className, ...props }, ref) => {
return (
<Label
className={cn(
"peer-focus:secondary peer-focus:dark:secondary absolute start-2 top-2 z-10 origin-[0] -translate-y-4 scale-75 transform bg-background px-2 text-sm text-gray-500 duration-300 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-2 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 dark:bg-background rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4 cursor-text",
className,
)}
ref={ref}
{...props}
/>
);
});
FloatingLabel.displayName = "FloatingLabel";
type FloatingLabelInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
label?: string;
};
const FloatingLabelInput = React.forwardRef<
React.ElementRef<typeof FloatingInput>,
React.PropsWithoutRef<FloatingLabelInputProps>
>(({ id, label, ...props }, ref) => {
return (
<div className="relative">
<FloatingInput ref={ref} id={id} {...props} />
<FloatingLabel htmlFor={id}>{label}</FloatingLabel>
</div>
);
});
FloatingLabelInput.displayName = "FloatingLabelInput";
export { FloatingInput, FloatingLabel, FloatingLabelInput };

View File

@@ -1,178 +1,179 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { Label as LabelPrimitive, Slot as SlotPrimitive } from "radix-ui" import { Label as LabelPrimitive, Slot as SlotPrimitive } from "radix-ui";
import { import {
Controller, Controller,
FormProvider, FormProvider,
useFormContext, useFormContext,
type ControllerProps, type ControllerProps,
type FieldPath, type FieldPath,
type FieldValues, type FieldValues,
} from "react-hook-form" } from "react-hook-form";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label";
const Form = FormProvider const Form = FormProvider;
type FormFieldContextValue< type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = { > = {
name: TName name: TName;
} };
const FormFieldContext = React.createContext<FormFieldContextValue>( const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue {} as FormFieldContextValue,
) );
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({ >({
...props ...props
}: ControllerProps<TFieldValues, TName>) => { }: ControllerProps<TFieldValues, TName>) => {
return ( return (
<FormFieldContext.Provider value={{ name: props.name }}> <FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} /> <Controller {...props} />
</FormFieldContext.Provider> </FormFieldContext.Provider>
) );
} };
const useFormField = () => { const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext) const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext) const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext() const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState) const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) { if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>") throw new Error("useFormField should be used within <FormField>");
} }
const { id } = itemContext const { id } = itemContext;
return { return {
id, id,
name: fieldContext.name, name: fieldContext.name,
formItemId: `${id}-form-item`, formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`, formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`, formMessageId: `${id}-form-item-message`,
...fieldState, ...fieldState,
} };
} };
type FormItemContextValue = { type FormItemContextValue = {
id: string id: string;
} };
const FormItemContext = React.createContext<FormItemContextValue>( const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue {} as FormItemContextValue,
) );
const FormItem = React.forwardRef< const FormItem = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const id = React.useId() const id = React.useId();
return ( return (
<FormItemContext.Provider value={{ id }}> <FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} /> <div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider> </FormItemContext.Provider>
) );
}) });
FormItem.displayName = "FormItem" FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef< const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField() const { error, formItemId } = useFormField();
return ( return (
<Label <Label
ref={ref} ref={ref}
className={cn(error && "text-destructive", className)} className={cn(error && "text-destructive", className)}
htmlFor={formItemId} htmlFor={formItemId}
{...props} {...props}
/> />
) );
}) });
FormLabel.displayName = "FormLabel" FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef< const FormControl = React.forwardRef<
React.ElementRef<typeof SlotPrimitive.Slot>, React.ElementRef<typeof SlotPrimitive.Slot>,
React.ComponentPropsWithoutRef<typeof SlotPrimitive.Slot> React.ComponentPropsWithoutRef<typeof SlotPrimitive.Slot>
>(({ ...props }, ref) => { >(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField() const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return ( return (
<SlotPrimitive.Slot <SlotPrimitive.Slot
ref={ref} ref={ref}
id={formItemId} id={formItemId}
aria-describedby={ aria-describedby={
!error !error
? `${formDescriptionId}` ? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}` : `${formDescriptionId} ${formMessageId}`
} }
aria-invalid={!!error} aria-invalid={!!error}
{...props} {...props}
/> />
) );
}) });
FormControl.displayName = "FormControl" FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef< const FormDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField() const { formDescriptionId } = useFormField();
return ( return (
<p <p
ref={ref} ref={ref}
id={formDescriptionId} id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)} className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props} {...props}
/> />
) );
}) });
FormDescription.displayName = "FormDescription" FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef< const FormMessage = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => { >(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField() const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? "") : children const body = error ? String(error?.message ?? "") : children;
if (!body) { if (!body) {
return null return null;
} }
return ( return (
<p <p
ref={ref} ref={ref}
id={formMessageId} id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)} className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props} {...props}
> >
{body} {body}
</p> </p>
) );
}) });
FormMessage.displayName = "FormMessage" FormMessage.displayName = "FormMessage";
export { export {
useFormField, useFormField,
Form, Form,
FormItem, FormItem,
FormLabel, FormLabel,
FormControl, FormControl,
FormDescription, FormDescription,
FormMessage, FormMessage,
FormField, FormField,
} };

View File

@@ -1,29 +1,29 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { HoverCard as HoverCardPrimitive } from "radix-ui" import { HoverCard as HoverCardPrimitive } from "radix-ui";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const HoverCard = HoverCardPrimitive.Root const HoverCard = HoverCardPrimitive.Root;
const HoverCardTrigger = HoverCardPrimitive.Trigger const HoverCardTrigger = HoverCardPrimitive.Trigger;
const HoverCardContent = React.forwardRef< const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>, React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content <HoverCardPrimitive.Content
ref={ref} ref={ref}
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-hover-card-content-transform-origin)", "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-hover-card-content-transform-origin)",
className className,
)} )}
{...props} {...props}
/> />
)) ));
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
export { HoverCard, HoverCardTrigger, HoverCardContent } export { HoverCard, HoverCardTrigger, HoverCardContent };

View File

@@ -1,71 +1,71 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp" import { OTPInput, OTPInputContext } from "input-otp";
import { Minus } from "lucide-react" import { Minus } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const InputOTP = React.forwardRef< const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>, React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput> React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => ( >(({ className, containerClassName, ...props }, ref) => (
<OTPInput <OTPInput
ref={ref} ref={ref}
containerClassName={cn( containerClassName={cn(
"flex items-center gap-2 has-disabled:opacity-50", "flex items-center gap-2 has-disabled:opacity-50",
containerClassName containerClassName,
)} )}
className={cn("disabled:cursor-not-allowed", className)} className={cn("disabled:cursor-not-allowed", className)}
{...props} {...props}
/> />
)) ));
InputOTP.displayName = "InputOTP" InputOTP.displayName = "InputOTP";
const InputOTPGroup = React.forwardRef< const InputOTPGroup = React.forwardRef<
React.ElementRef<"div">, React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center", className)} {...props} /> <div ref={ref} className={cn("flex items-center", className)} {...props} />
)) ));
InputOTPGroup.displayName = "InputOTPGroup" InputOTPGroup.displayName = "InputOTPGroup";
const InputOTPSlot = React.forwardRef< const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">, React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number } React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => { >(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext) const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index] const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
return ( return (
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md", "relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-1 ring-ring", isActive && "z-10 ring-1 ring-ring",
className className,
)} )}
{...props} {...props}
> >
{char} {char}
{hasFakeCaret && ( {hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center"> <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" /> <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
</div> </div>
)} )}
</div> </div>
) );
}) });
InputOTPSlot.displayName = "InputOTPSlot" InputOTPSlot.displayName = "InputOTPSlot";
const InputOTPSeparator = React.forwardRef< const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">, React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => ( >(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}> <div ref={ref} role="separator" {...props}>
<Minus /> <Minus />
</div> </div>
)) ));
InputOTPSeparator.displayName = "InputOTPSeparator" InputOTPSeparator.displayName = "InputOTPSeparator";
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View File

@@ -1,21 +1,21 @@
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) { function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return ( return (
<input <input
type={type} type={type}
data-slot="input" data-slot="input"
className={cn( className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { Input } export { Input };

View File

@@ -1,26 +1,26 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { Label as LabelPrimitive } from "radix-ui" import { Label as LabelPrimitive } from "radix-ui";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const labelVariants = cva( const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
) );
const Label = React.forwardRef< const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants> VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<LabelPrimitive.Root <LabelPrimitive.Root
ref={ref} ref={ref}
className={cn(labelVariants(), className)} className={cn(labelVariants(), className)}
{...props} {...props}
/> />
)) ));
Label.displayName = LabelPrimitive.Root.displayName Label.displayName = LabelPrimitive.Root.displayName;
export { Label } export { Label };

View File

@@ -1,256 +1,256 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { Menubar as MenubarPrimitive } from "radix-ui" import { Menubar as MenubarPrimitive } from "radix-ui";
import { Check, ChevronRight, Circle } from "lucide-react" import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function MenubarMenu({ function MenubarMenu({
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) { }: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu {...props} /> return <MenubarPrimitive.Menu {...props} />;
} }
function MenubarGroup({ function MenubarGroup({
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.Group>) { }: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group {...props} /> return <MenubarPrimitive.Group {...props} />;
} }
function MenubarPortal({ function MenubarPortal({
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) { }: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal {...props} /> return <MenubarPrimitive.Portal {...props} />;
} }
function MenubarRadioGroup({ function MenubarRadioGroup({
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) { }: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return <MenubarPrimitive.RadioGroup {...props} /> return <MenubarPrimitive.RadioGroup {...props} />;
} }
function MenubarSub({ function MenubarSub({
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) { }: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} /> return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
} }
const Menubar = React.forwardRef< const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>, React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root> React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<MenubarPrimitive.Root <MenubarPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm", "flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm",
className className,
)} )}
{...props} {...props}
/> />
)) ));
Menubar.displayName = MenubarPrimitive.Root.displayName Menubar.displayName = MenubarPrimitive.Root.displayName;
const MenubarTrigger = React.forwardRef< const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>, React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger <MenubarPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", "flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className className,
)} )}
{...props} {...props}
/> />
)) ));
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
const MenubarSubTrigger = React.forwardRef< const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>, React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & { React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, children, ...props }, ref) => ( >(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger <MenubarPrimitive.SubTrigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRight className="ml-auto h-4 w-4" /> <ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger> </MenubarPrimitive.SubTrigger>
)) ));
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
const MenubarSubContent = React.forwardRef< const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>, React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent> React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent <MenubarPrimitive.SubContent
ref={ref} ref={ref}
className={cn( className={cn(
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-menubar-content-transform-origin)", "z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-menubar-content-transform-origin)",
className className,
)} )}
{...props} {...props}
/> />
)) ));
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
const MenubarContent = React.forwardRef< const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>, React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content> React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>( >(
( (
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, { className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
ref ref,
) => ( ) => (
<MenubarPrimitive.Portal> <MenubarPrimitive.Portal>
<MenubarPrimitive.Content <MenubarPrimitive.Content
ref={ref} ref={ref}
align={align} align={align}
alignOffset={alignOffset} alignOffset={alignOffset}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 min-w-48 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-menubar-content-transform-origin)", "z-50 min-w-48 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-menubar-content-transform-origin)",
className className,
)} )}
{...props} {...props}
/> />
</MenubarPrimitive.Portal> </MenubarPrimitive.Portal>
) ),
) );
MenubarContent.displayName = MenubarPrimitive.Content.displayName MenubarContent.displayName = MenubarPrimitive.Content.displayName;
const MenubarItem = React.forwardRef< const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>, React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & { React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item <MenubarPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
/> />
)) ));
MenubarItem.displayName = MenubarPrimitive.Item.displayName MenubarItem.displayName = MenubarPrimitive.Item.displayName;
const MenubarCheckboxItem = React.forwardRef< const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>, React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem> React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => ( >(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem <MenubarPrimitive.CheckboxItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className className,
)} )}
checked={checked} checked={checked}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator> <MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator> </MenubarPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</MenubarPrimitive.CheckboxItem> </MenubarPrimitive.CheckboxItem>
)) ));
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
const MenubarRadioItem = React.forwardRef< const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>, React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem> React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem <MenubarPrimitive.RadioItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className className,
)} )}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator> <MenubarPrimitive.ItemIndicator>
<Circle className="h-4 w-4 fill-current" /> <Circle className="h-4 w-4 fill-current" />
</MenubarPrimitive.ItemIndicator> </MenubarPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</MenubarPrimitive.RadioItem> </MenubarPrimitive.RadioItem>
)) ));
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
const MenubarLabel = React.forwardRef< const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>, React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & { React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label <MenubarPrimitive.Label
ref={ref} ref={ref}
className={cn( className={cn(
"px-2 py-1.5 text-sm font-semibold", "px-2 py-1.5 text-sm font-semibold",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
/> />
)) ));
MenubarLabel.displayName = MenubarPrimitive.Label.displayName MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
const MenubarSeparator = React.forwardRef< const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>, React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator> React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator <MenubarPrimitive.Separator
ref={ref} ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)} className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props} {...props}
/> />
)) ));
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
const MenubarShortcut = ({ const MenubarShortcut = ({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLSpanElement>) => { }: React.HTMLAttributes<HTMLSpanElement>) => {
return ( return (
<span <span
className={cn( className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground", "ml-auto text-xs tracking-widest text-muted-foreground",
className className,
)} )}
{...props} {...props}
/> />
) );
} };
MenubarShortcut.displayname = "MenubarShortcut" MenubarShortcut.displayname = "MenubarShortcut";
export { export {
Menubar, Menubar,
MenubarMenu, MenubarMenu,
MenubarTrigger, MenubarTrigger,
MenubarContent, MenubarContent,
MenubarItem, MenubarItem,
MenubarSeparator, MenubarSeparator,
MenubarLabel, MenubarLabel,
MenubarCheckboxItem, MenubarCheckboxItem,
MenubarRadioGroup, MenubarRadioGroup,
MenubarRadioItem, MenubarRadioItem,
MenubarPortal, MenubarPortal,
MenubarSubContent, MenubarSubContent,
MenubarSubTrigger, MenubarSubTrigger,
MenubarGroup, MenubarGroup,
MenubarSub, MenubarSub,
MenubarShortcut, MenubarShortcut,
} };

View File

@@ -1,128 +1,128 @@
import * as React from "react" import * as React from "react";
import { NavigationMenu as NavigationMenuPrimitive } from "radix-ui" import { NavigationMenu as NavigationMenuPrimitive } from "radix-ui";
import { cva } from "class-variance-authority" import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react" import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const NavigationMenu = React.forwardRef< const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>, React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root <NavigationMenuPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center", "relative z-10 flex max-w-max flex-1 items-center justify-center",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<NavigationMenuViewport /> <NavigationMenuViewport />
</NavigationMenuPrimitive.Root> </NavigationMenuPrimitive.Root>
)) ));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef< const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>, React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List <NavigationMenuPrimitive.List
ref={ref} ref={ref}
className={cn( className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1", "group flex flex-1 list-none items-center justify-center space-x-1",
className className,
)} )}
{...props} {...props}
/> />
)) ));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva( const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent" "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent",
) );
const NavigationMenuTrigger = React.forwardRef< const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>, React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger <NavigationMenuPrimitive.Trigger
ref={ref} ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)} className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props} {...props}
> >
{children}{" "} {children}{" "}
<ChevronDown <ChevronDown
className="relative top-px ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180" className="relative top-px ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true" aria-hidden="true"
/> />
</NavigationMenuPrimitive.Trigger> </NavigationMenuPrimitive.Trigger>
)) ));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef< const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>, React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content <NavigationMenuPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ", "left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className className,
)} )}
{...props} {...props}
/> />
)) ));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef< const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>, React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}> <div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport <NavigationMenuPrimitive.Viewport
className={cn( className={cn(
"origin-top-center relative mt-1.5 h-(--radix-navigation-menu-viewport-height) w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-(--radix-navigation-menu-viewport-width)", "origin-top-center relative mt-1.5 h-(--radix-navigation-menu-viewport-height) w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-(--radix-navigation-menu-viewport-width)",
className className,
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
</div> </div>
)) ));
NavigationMenuViewport.displayName = NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef< const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>, React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator <NavigationMenuPrimitive.Indicator
ref={ref} ref={ref}
className={cn( className={cn(
"top-full z-1 flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in", "top-full z-1 flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className className,
)} )}
{...props} {...props}
> >
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" /> <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator> </NavigationMenuPrimitive.Indicator>
)) ));
NavigationMenuIndicator.displayName = NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName NavigationMenuPrimitive.Indicator.displayName;
export { export {
navigationMenuTriggerStyle, navigationMenuTriggerStyle,
NavigationMenu, NavigationMenu,
NavigationMenuList, NavigationMenuList,
NavigationMenuItem, NavigationMenuItem,
NavigationMenuContent, NavigationMenuContent,
NavigationMenuTrigger, NavigationMenuTrigger,
NavigationMenuLink, NavigationMenuLink,
NavigationMenuIndicator, NavigationMenuIndicator,
NavigationMenuViewport, NavigationMenuViewport,
} };

View File

@@ -61,7 +61,11 @@ const InputComponent = React.forwardRef<
HTMLInputElement, HTMLInputElement,
React.ComponentProps<"input"> React.ComponentProps<"input">
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<Input className={cn("mx-2 bg-white/10 backdrop-blur-md", className)} {...props} ref={ref} /> <Input
className={cn("mx-2 bg-white/10 backdrop-blur-md", className)}
{...props}
ref={ref}
/>
)); ));
InputComponent.displayName = "InputComponent"; InputComponent.displayName = "InputComponent";

View File

@@ -1,33 +1,33 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { Popover as PopoverPrimitive } from "radix-ui" import { Popover as PopoverPrimitive } from "radix-ui";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef< const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>, React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal> <PopoverPrimitive.Portal>
<PopoverPrimitive.Content <PopoverPrimitive.Content
ref={ref} ref={ref}
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-popover-content-transform-origin)", "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-popover-content-transform-origin)",
className className,
)} )}
{...props} {...props}
/> />
</PopoverPrimitive.Portal> </PopoverPrimitive.Portal>
)) ));
PopoverContent.displayName = PopoverPrimitive.Content.displayName PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

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