Compare commits

..

188 Commits

Author SHA1 Message Date
5592d9fbd2 fixed the logic and build the ownerResirvation page
All checks were successful
Build frontend / build (push) Successful in 1m36s
2026-08-13 13:58:07 +03:00
4b53669a35 add comission
All checks were successful
Build frontend / build (push) Successful in 1m25s
2026-08-12 17:42:51 +03:00
00dde588cb fixed getSaleProperiesById and make login type is email
All checks were successful
Build frontend / build (push) Successful in 1m46s
2026-08-11 16:04:09 +03:00
4924e7c2f3 fixed payment ,colors , add Mosque distance and fixed some elemnent
All checks were successful
Build frontend / build (push) Successful in 1m7s
2026-08-11 02:29:54 +03:00
9d3d02c88a fix icons
All checks were successful
Build frontend / build (push) Successful in 1m27s
2026-08-10 22:27:28 +03:00
1fa2fabb38 fixed NavButtom responsive
All checks were successful
Build frontend / build (push) Successful in 1m1s
2026-08-08 14:48:12 +03:00
793ec51c9a fixed NavButtom
All checks were successful
Build frontend / build (push) Successful in 59s
2026-08-08 14:22:34 +03:00
e3e9aa81a8 fixed middleware
All checks were successful
Build frontend / build (push) Successful in 1m36s
2026-08-08 13:45:58 +03:00
989a7f580f fixed middleware
All checks were successful
Build frontend / build (push) Successful in 53s
2026-08-07 00:55:23 +03:00
f7afef03d4 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 56s
2026-08-07 00:38:19 +03:00
f09561ef00 Update .gitea/workflows/deployer.yaml
Some checks failed
Build frontend / build (push) Has been cancelled
changed the repository name to amer2004/SweetHome
2026-08-06 21:38:13 +00:00
e42b0421bd fixed middleware 2026-08-07 00:37:33 +03:00
9602154ba4 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m24s
2026-08-07 00:11:52 +03:00
9b4f9979b6 restored the git action 2026-08-07 00:06:40 +03:00
b9a4c6157a edited middle ware and calender component 2026-08-06 23:51:50 +03:00
4d1b744516 add Middleware + fixed all pages and fixed add propertise 2026-08-06 22:26:35 +03:00
7da4bf9f2b fixed owner and customer register make reuseable component and completed it 2026-08-05 23:27:34 +03:00
7973c15e3f fixd larg lines in HomePage , add cookies rather than localStorage,make canvas background Login page 2026-08-04 12:51:47 +03:00
4ebb75ddd9 first commit 2026-08-01 15:03:30 +03:00
30693c6495 fixed some changes in the ccode
All checks were successful
Build frontend / build (push) Successful in 2m13s
2026-07-26 00:09:45 +03:00
491f10a1e0 fixed some changes in the ccode
All checks were successful
Build frontend / build (push) Successful in 1m25s
2026-07-25 14:58:54 +03:00
f91dfaacad fixed some changes in the ccode
All checks were successful
Build frontend / build (push) Successful in 55s
2026-07-25 14:54:27 +03:00
b4bb3ec279 fixed some changes in the ccode
All checks were successful
Build frontend / build (push) Successful in 1m10s
2026-07-25 13:26:48 +03:00
3ee4a93365 edited the edit property and need to test
All checks were successful
Build frontend / build (push) Successful in 1m36s
2026-07-25 12:48:37 +03:00
689170ebce fixed the app dialog
All checks were successful
Build frontend / build (push) Successful in 1m49s
2026-07-21 10:36:05 +03:00
46ecf74572 edited the payment dialog
All checks were successful
Build frontend / build (push) Successful in 1m10s
2026-07-21 01:15:16 +03:00
66db009562 edited the payment dialog
All checks were successful
Build frontend / build (push) Successful in 1m2s
2026-07-21 01:08:29 +03:00
83a06e8007 edited the payment dialog
All checks were successful
Build frontend / build (push) Successful in 1m0s
2026-07-21 00:43:49 +03:00
4166a5d652 it must be fixed
All checks were successful
Build frontend / build (push) Successful in 1m28s
2026-07-20 22:12:51 +03:00
ca6b12bfb9 fixing add property
All checks were successful
Build frontend / build (push) Successful in 57s
2026-07-16 10:56:59 -07:00
2374e80380 GetPlatformNumber​ api
All checks were successful
Build frontend / build (push) Successful in 1m32s
2026-07-16 09:40:51 -07:00
747e42dc00 Edit owner
All checks were successful
Build frontend / build (push) Successful in 1m9s
2026-07-15 20:03:38 +03:00
038df6397d Edit add property
All checks were successful
Build frontend / build (push) Successful in 58s
2026-07-15 19:42:59 +03:00
b5f5fabfc8 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m18s
2026-07-15 19:25:49 +03:00
8ed623bbe2 Edit haram payment 2026-07-15 19:25:39 +03:00
47550dbfac FilterRentProperties​ API
All checks were successful
Build frontend / build (push) Successful in 2m8s
2026-07-15 04:38:32 -07:00
7176031485 Edit payment
All checks were successful
Build frontend / build (push) Successful in 52s
2026-07-13 21:51:04 +03:00
9a7e7fb5f8 Edit payment
All checks were successful
Build frontend / build (push) Successful in 53s
2026-07-13 21:04:14 +03:00
c01ada480d Edit payment with haram
All checks were successful
Build frontend / build (push) Successful in 1m31s
2026-07-13 20:20:17 +03:00
c365503382 payment with haram
Some checks failed
Build frontend / build (push) Failing after 1m39s
2026-07-11 05:21:38 -07:00
bb47b05858 Added api for add property
All checks were successful
Build frontend / build (push) Successful in 1m39s
2026-07-04 12:24:13 +03:00
3bef951d5c Added city and document type
All checks were successful
Build frontend / build (push) Successful in 54s
2026-07-04 02:53:34 +03:00
2d2e2427a2 Edit payment
All checks were successful
Build frontend / build (push) Successful in 58s
2026-07-04 02:05:13 +03:00
3b66bb53f1 Added payment types
All checks were successful
Build frontend / build (push) Successful in 1m56s
2026-07-04 01:49:04 +03:00
c7698741c7 Added translation to payments
All checks were successful
Build frontend / build (push) Successful in 1m43s
2026-07-02 10:38:55 +03:00
dab5b62fb7 Added translation to all site with edit style
All checks were successful
Build frontend / build (push) Successful in 1m40s
2026-07-01 15:43:58 +03:00
3052209df8 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m5s
2026-06-30 17:17:03 +03:00
409cb41dd6 Edit style 2026-06-30 17:17:00 +03:00
3a3fa5f3fd OwnerStatistics api
All checks were successful
Build frontend / build (push) Successful in 1m41s
2026-06-30 06:55:57 -07:00
03b39317c9 Sticky top bar
All checks were successful
Build frontend / build (push) Successful in 53s
2026-06-29 01:07:50 +03:00
dc64178f94 Edit account book for owner
All checks were successful
Build frontend / build (push) Successful in 1m6s
2026-06-28 23:22:35 +03:00
253e1b7542 Edit style navbar, bottom nav
All checks were successful
Build frontend / build (push) Successful in 47s
2026-06-28 16:57:02 +03:00
d7ef0977ad Delete scroll
All checks were successful
Build frontend / build (push) Successful in 49s
2026-06-28 16:16:30 +03:00
4f58519bac Added dark and light theme
All checks were successful
Build frontend / build (push) Successful in 56s
Added navbar
2026-06-28 16:08:46 +03:00
df5801ee25 Edit properties for owner
All checks were successful
Build frontend / build (push) Successful in 57s
2026-06-28 15:16:32 +03:00
9dcbb940c6 Edit button nav
All checks were successful
Build frontend / build (push) Successful in 1m26s
Edit add properties for owner
2026-06-28 14:55:23 +03:00
12cb804b58 removing console.log
All checks were successful
Build frontend / build (push) Successful in 49s
2026-06-24 06:53:18 -07:00
5738d0369e payment api
All checks were successful
Build frontend / build (push) Successful in 56s
2026-06-24 06:36:47 -07:00
c0fdc7ef03 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m30s
2026-06-24 15:04:59 +03:00
389dca56d2 Edit payments 2026-06-24 15:04:56 +03:00
6e4d629cbe re added the api url
All checks were successful
Build frontend / build (push) Successful in 48s
2026-06-24 01:32:21 +03:00
13b83e9aea removed the descption on the profile
All checks were successful
Build frontend / build (push) Successful in 49s
2026-06-24 00:15:21 +03:00
4a9380313b fixed the support and removed the send general report no need for that also fixed the client layout
All checks were successful
Build frontend / build (push) Successful in 51s
2026-06-24 00:07:09 +03:00
8d3a745cc3 fixed the api url
All checks were successful
Build frontend / build (push) Successful in 56s
2026-06-23 23:45:48 +03:00
199478f237 added the elmeent on the nav and fixed the payments transactions
All checks were successful
Build frontend / build (push) Successful in 1m27s
2026-06-23 23:32:07 +03:00
61e527fab3 removing console.log
All checks were successful
Build frontend / build (push) Successful in 54s
2026-06-17 06:46:57 -07:00
d0b626ac8c Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m20s
2026-06-17 06:01:32 -07:00
792bed6250 remove profile editing keep 2026-06-17 06:01:24 -07:00
bf45a48504 fixing the build
All checks were successful
Build frontend / build (push) Successful in 59s
2026-06-16 19:03:15 +03:00
eaa4206b0b fixing the build
Some checks failed
Build frontend / build (push) Failing after 50s
2026-06-16 18:58:48 +03:00
c5d9ad7b70 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome 2026-06-16 18:58:06 +03:00
46945464ae fixing the build 2026-06-16 18:57:46 +03:00
64d422ddd8 Hide rent-type fields on buy/sell tabs and disable auto-search on tab click
Some checks failed
Build frontend / build (push) Failing after 56s
- Property type and rent period fields now only display when 'Rent' tab is active
- Adjusted grid column spans to fill layout when fields are hidden
- Tab switching no longer triggers an automatic search; search runs only on button click
2026-06-16 15:18:29 +03:00
224c29bc19 Added payments button
Some checks failed
Build frontend / build (push) Failing after 1m9s
2026-06-16 14:12:26 +03:00
f892fc4a4d Added terms with api
Some checks failed
Build frontend / build (push) Failing after 1m30s
2026-06-16 13:59:17 +03:00
01ac4f8d6c add report on reservation api
Some checks failed
Build frontend / build (push) Failing after 48s
2026-06-15 10:46:18 -07:00
f2724a5cd2 GetMyTransactions api
Some checks failed
Build frontend / build (push) Failing after 1m2s
2026-06-15 10:18:15 -07:00
bef133ad5b Fixing the map problem
Some checks failed
Build frontend / build (push) Failing after 1m1s
2026-06-14 18:58:06 +03:00
a9eb1cc684 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
Some checks failed
Build frontend / build (push) Failing after 1m18s
2026-06-14 08:33:02 -07:00
13b563e35e fixing SendGeneralReport 2026-06-14 08:22:59 -07:00
5d593d593f Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
Some checks failed
Build frontend / build (push) Failing after 1m30s
2026-06-14 18:04:26 +03:00
51850b85c2 Added blocked page with api 2026-06-14 18:04:05 +03:00
8cacf464d1 privacy screen api
Some checks failed
Build frontend / build (push) Failing after 1m27s
2026-06-14 07:45:13 -07:00
91de3d47b7 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
Some checks failed
Build frontend / build (push) Failing after 1m0s
2026-06-12 12:47:58 -07:00
71b1a71904 Delete admin
Some checks failed
Build frontend / build (push) Failing after 1m20s
2026-06-10 19:50:49 +03:00
34da1314d4 The footer only appears in homepage
All checks were successful
Build frontend / build (push) Successful in 1m1s
2026-06-09 23:37:47 +03:00
ce6caf08eb Added Active or DeActive for owner with API
All checks were successful
Build frontend / build (push) Successful in 1m10s
2026-06-09 18:21:52 +03:00
845ba2436a SendGeneralReport api 2026-06-09 03:38:37 -07:00
471332b59f fixing agent api
All checks were successful
Build frontend / build (push) Successful in 57s
2026-06-06 03:55:53 -07:00
53a83494b7 owner account
All checks were successful
Build frontend / build (push) Successful in 1m16s
2026-06-05 18:33:51 -07:00
6bc0c8ba27 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m31s
2026-06-04 23:33:25 +03:00
74dc12171d test
All checks were successful
Build frontend / build (push) Successful in 2m22s
2026-06-04 23:09:48 +03:00
9fdeadaa61 Added bottom navbar 2026-06-04 22:54:34 +03:00
d11f105dfc if the user role is admin dont show the booking section
All checks were successful
Build frontend / build (push) Successful in 50s
2026-05-30 13:45:32 +03:00
8d4ac3ddd6 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m16s
2026-05-30 13:24:31 +03:00
b4196c340d removed the contact information 2026-05-30 13:24:22 +03:00
ddf5367f92 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m17s
Co-authored-by: Copilot <copilot@github.com>
2026-05-28 18:30:39 +03:00
417b6cb393 Edit AddProperty 2026-05-28 17:42:51 +03:00
819d5ea802 added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 43s
2026-05-26 23:04:00 +03:00
f1a14c2f7a added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 45s
2026-05-26 23:00:19 +03:00
29de55196a added a swicher on the properties
Some checks failed
Build frontend / build (push) Failing after 40s
2026-05-26 22:53:18 +03:00
01b966c1d2 added the descption
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-26 22:40:54 +03:00
e1204bf69e added the descption
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-26 21:54:22 +03:00
a0a78079c5 added the descption
All checks were successful
Build frontend / build (push) Successful in 52s
2026-05-26 21:49:22 +03:00
e201b1cd8a added the descption
All checks were successful
Build frontend / build (push) Successful in 42s
2026-05-26 21:28:14 +03:00
3466d4715a added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 42s
2026-05-26 21:16:10 +03:00
88884ed2d7 added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 46s
2026-05-26 21:00:14 +03:00
e21dc53227 added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-26 20:54:25 +03:00
82e39e6f90 added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 43s
2026-05-26 20:28:51 +03:00
caddf55811 added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 46s
2026-05-26 20:24:37 +03:00
a3657e5c2f added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-26 19:53:28 +03:00
b8d779a1d0 added the descption
All checks were successful
Build frontend / build (push) Successful in 43s
2026-05-26 19:25:10 +03:00
c53a8eeb6d added the descption
All checks were successful
Build frontend / build (push) Successful in 42s
2026-05-26 19:18:49 +03:00
9979877e1c added the descption
All checks were successful
Build frontend / build (push) Successful in 45s
2026-05-26 19:04:57 +03:00
9fda2618c8 added the descption
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-26 18:43:47 +03:00
bf044cef45 added the descption
All checks were successful
Build frontend / build (push) Successful in 43s
2026-05-26 18:22:21 +03:00
3f24696c48 added the descption
All checks were successful
Build frontend / build (push) Successful in 45s
2026-05-26 18:01:18 +03:00
9c79e0648b added the descption
All checks were successful
Build frontend / build (push) Successful in 47s
2026-05-26 17:49:39 +03:00
13a0649806 added the descption
All checks were successful
Build frontend / build (push) Successful in 45s
2026-05-26 17:31:01 +03:00
1ae3a635f7 added the descption
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-26 17:06:01 +03:00
0e61dd5789 added the descption
All checks were successful
Build frontend / build (push) Successful in 46s
2026-05-26 17:00:57 +03:00
50a4816327 added the calendre and the price toggle
All checks were successful
Build frontend / build (push) Successful in 1m7s
2026-05-26 16:38:16 +03:00
09bbf07d8c edited the edit properrty
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-26 04:35:03 +03:00
439f69419f fixed the details
All checks were successful
Build frontend / build (push) Successful in 43s
2026-05-26 04:16:53 +03:00
71ce933c6a fixed the details
All checks were successful
Build frontend / build (push) Successful in 45s
2026-05-26 03:22:14 +03:00
5045e69191 fixed the details
All checks were successful
Build frontend / build (push) Successful in 1m36s
2026-05-26 02:59:20 +03:00
f91c78f759 added the allowedPaymentPeriod
All checks were successful
Build frontend / build (push) Successful in 42s
2026-05-26 02:35:41 +03:00
80ea9af86b fixed the details
All checks were successful
Build frontend / build (push) Successful in 42s
2026-05-26 02:34:18 +03:00
4c350d7589 fixed the details
All checks were successful
Build frontend / build (push) Successful in 42s
2026-05-26 01:48:12 +03:00
ca94c5fd87 fixed the details
All checks were successful
Build frontend / build (push) Successful in 43s
2026-05-26 01:44:23 +03:00
c40486b99e fixed the details
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-26 01:20:05 +03:00
9f6492cf41 fixed the details
All checks were successful
Build frontend / build (push) Successful in 43s
2026-05-26 00:20:20 +03:00
085c60ec33 editing the add rent property mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 50s
2026-05-25 23:55:02 +03:00
2c4a163cdb editing the add rent property mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-25 23:18:03 +03:00
8d48fcae99 fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-25 23:07:29 +03:00
af54bded13 fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-25 22:54:38 +03:00
4988302bc1 fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 45s
2026-05-25 22:50:38 +03:00
427dc74e7f fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-25 22:42:53 +03:00
7b333f9b53 fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 42s
2026-05-25 22:37:05 +03:00
291e82a1b4 fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 42s
2026-05-25 22:28:26 +03:00
9d8ccbac3c fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 41s
2026-05-25 22:19:03 +03:00
7a3248aec5 fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 49s
2026-05-25 22:14:37 +03:00
aff9912ac2 fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 1m1s
2026-05-25 21:53:57 +03:00
50d636a577 fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 1m11s
2026-05-25 21:37:37 +03:00
00ccf5f262 the best in the west is mouaz
All checks were successful
Build frontend / build (push) Successful in 55s
2026-05-25 21:27:39 +03:00
a5577765ed Edit register for tenant
All checks were successful
Build frontend / build (push) Successful in 1m19s
2026-05-10 16:21:32 +03:00
4bedd24315 Edit register
All checks were successful
Build frontend / build (push) Successful in 47s
2026-05-06 20:08:26 +03:00
378d2d0f37 Edit register for owner
All checks were successful
Build frontend / build (push) Successful in 1m19s
2026-05-06 18:54:05 +03:00
5936e0f553 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m23s
2026-05-05 17:03:06 -07:00
84f5bbe9db Fixed owner reservation approval and rejection API endpoints 2026-05-05 17:02:21 -07:00
46cbce1d88 Added API to map with markers in main page
All checks were successful
Build frontend / build (push) Successful in 53s
2026-05-02 17:20:35 +03:00
1198c18004 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m28s
2026-04-28 13:17:57 -07:00
481f683e64 fixed booking date selection and reservation request 2026-04-28 12:57:06 -07:00
5d3ead55ca Added API for rating
All checks were successful
Build frontend / build (push) Successful in 54s
2026-04-26 13:46:30 +03:00
97126c5776 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 54s
2026-04-22 10:52:19 +03:00
1e167c447a Edit profits for owner 2026-04-22 10:52:08 +03:00
dd0a9c401d readdded the getuserId function
All checks were successful
Build frontend / build (push) Successful in 58s
2026-04-17 14:40:47 +03:00
32f6c7af5a fixed the api request
All checks were successful
Build frontend / build (push) Successful in 1m7s
2026-04-16 22:49:15 +03:00
7e0d5eaf8d edited the api request
All checks were successful
Build frontend / build (push) Successful in 40s
2026-04-16 22:40:59 +03:00
beccd8b24f added debugging on the admin confirm
All checks were successful
Build frontend / build (push) Successful in 41s
2026-04-16 22:33:19 +03:00
7e9a9d79f2 there is no endpoint in name /Reservations/GetReservations
All checks were successful
Build frontend / build (push) Successful in 41s
2026-04-16 22:13:14 +03:00
39f494aecb fixed some things
All checks were successful
Build frontend / build (push) Successful in 42s
2026-04-16 22:06:57 +03:00
485baffdc2 fixed some things
All checks were successful
Build frontend / build (push) Successful in 55s
2026-04-16 21:30:22 +03:00
c46173d7c6 fixed some things
All checks were successful
Build frontend / build (push) Successful in 45s
2026-04-16 21:18:31 +03:00
04fa34107b linked the admin confirm deposte
All checks were successful
Build frontend / build (push) Successful in 1m9s
2026-04-16 21:15:21 +03:00
5a7d0ef265 Added confirm button for admin
All checks were successful
Build frontend / build (push) Successful in 46s
2026-04-15 12:28:01 +03:00
0ba435fd7e Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m56s
2026-04-15 12:10:45 +03:00
9c2a748ae9 Added API for notifications and edit style 2026-04-15 12:07:39 +03:00
db949aaeba Fix build errors: corrected import paths, added missing RatingList component, fixed syntax errors in rating components
All checks were successful
Build frontend / build (push) Successful in 54s
2026-04-14 14:23:17 +00:00
ae600ad41b Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
Some checks failed
Build frontend / build (push) Failing after 43s
2026-04-13 00:38:59 +03:00
16b1c7c6f6 Edit home 2026-04-13 00:25:29 +03:00
f761ab6f48 Removed double <<
Some checks failed
Build frontend / build (push) Failing after 46s
2026-04-12 20:50:02 +00:00
78138e6445 Commited by hamza on openclaw's belalf
Some checks failed
Build frontend / build (push) Failing after 54s
2026-04-12 20:44:04 +00:00
0891974440 fix: properly enrich reservations with property data using status integer codes
All checks were successful
Build frontend / build (push) Successful in 1m21s
2026-04-05 19:11:59 +00:00
f925af0272 fix: access property via propertyInformation nested object
All checks were successful
Build frontend / build (push) Successful in 48s
2026-04-05 18:46:47 +00:00
2346f518ce feat: enrich reservation pages with property details via GetRentPropertyById
All checks were successful
Build frontend / build (push) Successful in 43s
2026-04-05 18:40:09 +00:00
149058ddfc fix: route حجزات nav links to new reservations pages
All checks were successful
Build frontend / build (push) Successful in 43s
2026-04-05 18:09:11 +00:00
e6249e845e fix: match backend typos GetOwnerResevationRequests (missing r)
All checks were successful
Build frontend / build (push) Successful in 42s
2026-04-05 17:52:08 +00:00
3bdb99f2e5 feat: add user reservations page and owner reservation requests page
All checks were successful
Build frontend / build (push) Successful in 49s
2026-04-05 14:42:11 +00:00
2a1f00740f disable export report in owner
All checks were successful
Build frontend / build (push) Successful in 1m8s
2026-04-04 22:47:05 +03:00
50836d3ec7 Edit AddPropertyForm
All checks were successful
Build frontend / build (push) Successful in 43s
2026-04-04 21:50:31 +03:00
d850f921b5 Edit add admin and privacy
All checks were successful
Build frontend / build (push) Successful in 49s
2026-04-04 21:01:02 +03:00
77dd052951 Added add admin and privacy in sidebar for admin
All checks were successful
Build frontend / build (push) Successful in 1m20s
2026-04-04 20:56:49 +03:00
1207dbe20d removed the validation from the email
All checks were successful
Build frontend / build (push) Successful in 43s
2026-04-02 16:05:20 +03:00
c9f52f64cb removed the validation from the email
All checks were successful
Build frontend / build (push) Successful in 1m23s
2026-04-02 16:00:05 +03:00
5fd22f0e01 disabled the validation on email
All checks were successful
Build frontend / build (push) Successful in 1m9s
2026-04-02 14:44:23 +03:00
2998a6bd75 fix: import useMapEvents as hook instead of dynamic component
All checks were successful
Build frontend / build (push) Successful in 45s
2026-04-01 19:07:22 +00:00
571c85f14f fix: detect auth changes via polling and visibility change
All checks were successful
Build frontend / build (push) Successful in 52s
2026-04-01 15:29:41 +00:00
156 changed files with 24015 additions and 13830 deletions

View File

@ -10,7 +10,7 @@ jobs:
- name: Clone repository - name: Clone repository
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
repository: Rahaf/SweetHome repository: amer2004/SweetHome
github-server-url: http://45.93.137.91:3000 github-server-url: http://45.93.137.91:3000
- name: Stopping server - name: Stopping server

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"i18n-ally-next.localesPaths": ["app/i18n"]
}

View File

@ -5,8 +5,7 @@ import { useTranslation } from "react-i18next";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { NavLink, MobileNavLink } from "./components/NavLinks"; import { NavLink, MobileNavLink } from "./components/NavLinks";
import { FavoritesProvider } from '@/app/contexts/FavoritesContext'; import BottomNav from "./components/BottomNav";
import FloatingSidebar from '@/app/components/FloatingSidebar';
import { import {
Globe, Globe,
LogIn, LogIn,
@ -17,24 +16,17 @@ import {
Building, Building,
PlusCircle, PlusCircle,
Heart, Heart,
MessageCircle,
Settings, Settings,
Edit,
Phone, Phone,
Mail, Mail,
MapPin,
Camera,
Shield,
Bell, Bell,
Home,
ChevronDown,
Menu, Menu,
X, X,
TrendingUp, TrendingUp,
CalendarDays, CalendarDays,
Clock,
Users,
DollarSign, DollarSign,
Star,
FileText,
} from "lucide-react"; } from "lucide-react";
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
@ -43,6 +35,9 @@ import { UserRole, UserRoleLabels } from "./enums/UserRole";
import "./i18n/config"; import "./i18n/config";
import NotificationHandler from "./components/NotificationHandler"; import NotificationHandler from "./components/NotificationHandler";
import { useNotifications } from "@/app/contexts/NotificationsContext";
import Loading from "./loading";
export default function ClientLayout({ children }) { export default function ClientLayout({ children }) {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const pathname = usePathname(); const pathname = usePathname();
@ -54,6 +49,8 @@ export default function ClientLayout({ children }) {
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const menuRef = useRef(null); const menuRef = useRef(null);
const { unreadCount } = useNotifications();
useEffect(() => { useEffect(() => {
setIsMounted(true); setIsMounted(true);
const savedLanguage = localStorage.getItem("language") || "ar"; const savedLanguage = localStorage.getItem("language") || "ar";
@ -77,9 +74,7 @@ export default function ClientLayout({ children }) {
name: authUser.name || authUser.email, name: authUser.name || authUser.email,
email: authUser.email, email: authUser.email,
phone: authUser.phone, phone: authUser.phone,
role: AuthService.isAdmin() ? UserRole.ADMIN role: AuthService.isOwner() ? UserRole.OWNER : AuthService.isAgent() ? UserRole.AGENT : UserRole.CUSTOMER,
: AuthService.isOwner() ? UserRole.OWNER
: UserRole.CUSTOMER,
}); });
} else { } else {
setUser(null); setUser(null);
@ -125,17 +120,14 @@ export default function ClientLayout({ children }) {
window.location.href = "/"; window.location.href = "/";
}; };
const isAuthPage = [ const isAuthPage = ["/login", "/blocked", "/register", "/forgot-password", "/auth/choose-role"].includes(pathname);
"/login",
"/register",
"/forgot-password",
"/auth/choose-role",
].includes(pathname);
const isProfilePage = pathname === "/profile"; const isProfilePage = pathname === "/profile";
const isHomePage = pathname === "/";
const isOwner = user?.role === UserRole.OWNER; const isOwner = user?.role === UserRole.OWNER;
const isAdmin = user?.role === UserRole.ADMIN; const isAgent = user?.role === UserRole.AGENT;
const isOwnerOrAgent = isOwner || isAgent;
const isCustomer = user?.role === UserRole.CUSTOMER; const isCustomer = user?.role === UserRole.CUSTOMER;
const isAuthenticated = !!user; const isAuthenticated = !!user;
@ -148,122 +140,99 @@ export default function ClientLayout({ children }) {
if (!isMounted) { if (!isMounted) {
return ( return (
<div className="min-h-screen flex items-center justify-center"> <Loading/>
<div className="text-center">
<div className="w-16 h-16 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-gray-600">جاري التحميل...</p>
</div>
</div>
); );
} }
return ( return (
<> <div className="min-h-screen flex flex-col">
{!isAuthPage && ( {!isAuthPage && !isAuthenticated && (
<nav className="fixed top-0 left-0 right-0 bg-white/95 backdrop-blur-sm border-b border-gray-200 z-50 transition-all duration-300 shadow-sm"> <nav
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${isHomePage ? "bg-transparent backdrop-blur-sm" : "bg-white/95 backdrop-blur-sm border-b border-gray-200 shadow-sm"}`}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div <div className={`flex justify-between items-center h-20 ${currentLanguage === "ar" ? "flex-row-reverse" : ""}`}>
className={`flex justify-between items-center h-20 ${currentLanguage === "ar" ? "flex-row-reverse" : ""}`}
>
<div className="flex items-center"> <div className="flex items-center">
<Link href="/" className="flex items-center space-x-3 group"> <Link href="/" className="flex items-center space-x-3 group">
<div className="relative w-[150px] h-[60px]"> <div className="relative w-[150px] h-[60px]">
<Image <Image src="/logo.png" alt={t("logoAlt")} fill className="object-contain" priority />
src="/logo.png"
alt={t("logoAlt")}
fill
className="object-contain"
priority
/>
</div> </div>
<span className="text-3xl font-bold text-gray-800 hidden md:block"> <span className={`text-3xl font-bold hidden md:block ${isHomePage ? "text-white" : "text-gray-800"}`}>
{t("brandNamePart1")} {t("brandNamePart1")}
<span className="text-amber-600"> <span className={isHomePage ? "text-amber-400" : "text-amber-600"}>{t("brandNamePart2")}</span>
{t("brandNamePart2")}
</span>
</span> </span>
</Link> </Link>
</div> </div>
<div className="hidden md:flex items-center space-x-4"> <div className="hidden md:flex items-center space-x-4">
<div <div className={`flex items-center space-x-1 ${currentLanguage === "ar" ? "flex-row-reverse space-x-reverse" : ""}`}>
className={`flex items-center space-x-1 ${currentLanguage === "ar" ? "flex-row-reverse space-x-reverse" : ""}`}
>
{/* Download App Dropdown */} {/* Download App Dropdown */}
<div className="relative group"> <div className="relative group">
<button className="flex items-center gap-2 px-3 py-2 text-gray-700 hover:text-amber-600 hover:bg-amber-50 rounded-lg transition-all"> <button
className={`flex items-center gap-2 px-3 py-2 rounded-lg transition-all ${isHomePage ? "text-white/80 hover:text-amber-300 hover:bg-white/10" : "text-gray-700 hover:text-amber-600 hover:bg-amber-50"}`}
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M11 2a3 3 0 0 0-3 3v6.5a.5.5 0 0 0 1 0V5a2 2 0 1 1 4 0v6.5a.5.5 0 0 0 1 0V5a3 3 0 0 0-3-3z"/> <path d="M11 2a3 3 0 0 0-3 3v6.5a.5.5 0 0 0 1 0V5a2 2 0 1 1 4 0v6.5a.5.5 0 0 0 1 0V5a3 3 0 0 0-3-3z" />
<path d="M1.5 12.5A1.5 1.5 0 0 0 3 14h10a1.5 1.5 0 0 0 0-3H3a1.5 1.5 0 0 0-1.5 1.5z"/> <path d="M1.5 12.5A1.5 1.5 0 0 0 3 14h10a1.5 1.5 0 0 0 0-3H3a1.5 1.5 0 0 0-1.5 1.5z" />
</svg>
<span className="text-sm font-semibold">{t("downloadApp")}</span>
<svg className="w-4 h-4 transition-transform group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
<span className="text-sm font-semibold">تحميل التطبيق</span>
<svg className="w-4 h-4 transition-transform group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"/></svg>
</button> </button>
<div className="absolute right-0 mt-2 w-64 bg-white rounded-xl shadow-xl border border-gray-200 overflow-hidden z-50 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 translate-y-2 group-hover:translate-y-0"> <div className="absolute right-0 mt-2 w-64 bg-white rounded-xl shadow-xl border border-gray-200 overflow-hidden z-50 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 translate-y-2 group-hover:translate-y-0">
<div className="p-2"> <div className="p-2">
<a href="/files/SweetHome.apk" download <a href="/files/SweetHome.apk" download className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-green-50 transition-colors">
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-green-50 transition-colors">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center"> <div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="#16a34a" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="#16a34a" viewBox="0 0 16 16">
<path d="M2.76 3.061a.5.5 0 0 1 .679.2l1.283 2.352A8.9 8.9 0 0 1 8 5a8.9 8.9 0 0 1 3.278.613l1.283-2.352a.5.5 0 1 1 .878.478l-1.252 2.295C14.475 7.266 16 9.477 16 12H0c0-2.523 1.525-4.734 3.813-5.966L2.56 3.74a.5.5 0 0 1 .2-.678ZM5 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2m6 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/> <path d="M2.76 3.061a.5.5 0 0 1 .679.2l1.283 2.352A8.9 8.9 0 0 1 8 5a8.9 8.9 0 0 1 3.278.613l1.283-2.352a.5.5 0 1 1 .878.478l-1.252 2.295C14.475 7.266 16 9.477 16 12H0c0-2.523 1.525-4.734 3.813-5.966L2.56 3.74a.5.5 0 0 1 .2-.678ZM5 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2m6 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2" />
</svg> </svg>
</div> </div>
<div> <div>
<p className="font-semibold text-gray-900 text-sm">Android</p> <p className="font-semibold text-gray-900 text-sm">{t("android")}</p>
<p className="text-xs text-green-600">تحميل APK</p> <p className="text-xs text-green-600">{t("downloadAPK")}</p>
</div> </div>
</a> </a>
<div className="flex items-center gap-3 px-4 py-3 rounded-lg opacity-50 cursor-not-allowed"> <div className="flex items-center gap-3 px-4 py-3 rounded-lg opacity-50 cursor-not-allowed">
<div className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center"> <div className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#6b7280" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#6b7280" viewBox="0 0 16 16">
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43Zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.385 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282Z"/> <path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43Zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.385 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282Z" />
</svg> </svg>
</div> </div>
<div> <div>
<p className="font-semibold text-gray-400 text-sm">iOS</p> <p className="font-semibold text-gray-400 text-sm">{t("ios")}</p>
<p className="text-xs text-gray-400">قريباً</p> <p className="text-xs text-gray-400">{t("comingSoon")}</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<NavLink href="/">الرئيسية</NavLink> <NavLink light={isHomePage} href="/">
<NavLink href="/properties">عقاراتنا</NavLink> {t("home")}
</NavLink>
{isAdmin && ( <NavLink light={isHomePage} href="/properties">
<NavLink href="/admin"> {t("ourProperties")}
<span className="flex items-center gap-2"> </NavLink>
<Shield className="w-4 h-4" />
الإدارة
</span>
</NavLink>
)}
{isOwner && ( {isOwner && (
<> <>
<NavLink href="/owner/properties"> <NavLink light={isHomePage} href="/owner/properties">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<Building className="w-4 h-4" /> <Building className="w-4 h-4" />
عقاراتي {t("myProperties")}
</span> </span>
</NavLink> </NavLink>
<NavLink href="/owner/bookings"> <NavLink light={isHomePage} href="/owner/reservations">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<Calendar className="w-4 h-4" /> <Calendar className="w-4 h-4" />
الحجوزات {t("reservations")}
</span> </span>
</NavLink> </NavLink>
{/* <NavLink href="/owner/calendar"> <NavLink light={isHomePage} href="/owner/profits">
<span className="flex items-center gap-2">
<CalendarDays className="w-4 h-4" />
التقويم
</span>
</NavLink> */}
<NavLink href="/owner/profits">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<TrendingUp className="w-4 h-4" /> <TrendingUp className="w-4 h-4" />
الأرباح {t("profits")}
</span> </span>
</NavLink> </NavLink>
</> </>
@ -271,16 +240,16 @@ export default function ClientLayout({ children }) {
{!user && !isAuthPage && ( {!user && !isAuthPage && (
<> <>
<NavLink href="/login"> <NavLink light={isHomePage} href="/login">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<LogIn className="w-4 h-4" /> <LogIn className="w-4 h-4" />
تسجيل الدخول {t("login")}
</span> </span>
</NavLink> </NavLink>
<NavLink href="/auth/choose-role"> <NavLink light={isHomePage} href="/auth/choose-role">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<UserPlus className="w-4 h-4" /> <UserPlus className="w-4 h-4" />
إنشاء حساب {t("createAccount")}
</span> </span>
</NavLink> </NavLink>
</> </>
@ -319,49 +288,57 @@ export default function ClientLayout({ children }) {
> >
<div className="bg-gradient-to-r from-amber-500 to-amber-600 p-4 text-white"> <div className="bg-gradient-to-r from-amber-500 to-amber-600 p-4 text-white">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center text-xl font-bold"> <div className="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center text-xl font-bold">{getUserInitial()}</div>
{getUserInitial()}
</div>
<div> <div>
<p className="font-bold"> <p className="font-bold">{user?.name || t("userLabel")}</p>
{user?.name || "مستخدم"} <p className="text-xs text-amber-100">{user?.email || ""}</p>
</p> <p className="text-xs text-amber-100 mt-1">{t(UserRoleLabels[user?.role] ? `userRole.${user?.role.toLowerCase()}` : "visitor")}</p>
<p className="text-xs text-amber-100">
{user?.email || ""}
</p>
<p className="text-xs text-amber-100 mt-1">
{UserRoleLabels[user?.role] || 'زائر'}
</p>
</div> </div>
</div> </div>
</div> </div>
<div className="p-2"> <div className="p-2">
<Link <Link href="/profile" className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors" onClick={() => setShowUserMenu(false)}>
href="/profile"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<UserCircle className="w-5 h-5 text-amber-500" /> <UserCircle className="w-5 h-5 text-amber-500" />
<div> <div>
<p className="font-medium">الملف الشخصي</p> <p className="font-medium">{t("profile")}</p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">{t("viewAndEditInfo")}</p>
عرض وتعديل معلوماتك </div>
</p> </Link>
<Link href="/favorites" className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors" onClick={() => setShowUserMenu(false)}>
<Heart className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">{t("favorites")}</p>
<p className="text-xs text-gray-500">{t("savedProperties")}</p>
</div> </div>
</Link> </Link>
<Link <Link
href="/favorites" href="/booked-properties"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors" className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)} onClick={() => setShowUserMenu(false)}
> >
<Heart className="w-5 h-5 text-amber-500" /> <CalendarDays className="w-5 h-5 text-amber-500" />
<div> <div>
<p className="font-medium">المفضلة</p> <p className="font-medium">{t("myBookedProperties")}</p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">{t("viewAndRateBooked")}</p>
العقارات المحفوظة </div>
</p> </Link>
<Link href="/my-rates" className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors" onClick={() => setShowUserMenu(false)}>
<Star className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">{t("myRatings")}</p>
<p className="text-xs text-gray-500">{t("ratingsReceived")}</p>
</div>
</Link>
<Link href="/settings" className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors" onClick={() => setShowUserMenu(false)}>
<Settings className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">{t("settings")}</p>
<p className="text-xs text-gray-500">{t("accountAndSecurity")}</p>
</div> </div>
</Link> </Link>
@ -376,10 +353,8 @@ export default function ClientLayout({ children }) {
> >
<Building className="w-5 h-5 text-amber-500" /> <Building className="w-5 h-5 text-amber-500" />
<div> <div>
<p className="font-medium">عقاراتي</p> <p className="font-medium">{t("myProperties")}</p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">{t("manageYourProperties")}</p>
إدارة عقاراتك
</p>
</div> </div>
</Link> </Link>
@ -390,24 +365,20 @@ export default function ClientLayout({ children }) {
> >
<PlusCircle className="w-5 h-5 text-amber-500" /> <PlusCircle className="w-5 h-5 text-amber-500" />
<div> <div>
<p className="font-medium">إضافة عقار</p> <p className="font-medium">{t("addProperty")}</p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">{t("addNewProperty")}</p>
أضف عقاراً جديداً
</p>
</div> </div>
</Link> </Link>
<Link <Link
href="/owner/bookings" href="/owner/reservations"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors" className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)} onClick={() => setShowUserMenu(false)}
> >
<Calendar className="w-5 h-5 text-amber-500" /> <Calendar className="w-5 h-5 text-amber-500" />
<div> <div>
<p className="font-medium">الحجوزات</p> <p className="font-medium">{t("reservations")}</p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">{t("manageReservations")}</p>
إدارة حجوزاتك
</p>
</div> </div>
</Link> </Link>
@ -418,10 +389,8 @@ export default function ClientLayout({ children }) {
> >
<CalendarDays className="w-5 h-5 text-amber-500" /> <CalendarDays className="w-5 h-5 text-amber-500" />
<div> <div>
<p className="font-medium">التقويم</p> <p className="font-medium">{t("calendar")}</p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">{t("propertyAvailability")}</p>
جدول توفر العقارات
</p>
</div> </div>
</Link> </Link>
@ -432,86 +401,20 @@ export default function ClientLayout({ children }) {
> >
<TrendingUp className="w-5 h-5 text-amber-500" /> <TrendingUp className="w-5 h-5 text-amber-500" />
<div> <div>
<p className="font-medium">الأرباح</p> <p className="font-medium">{t("profits")}</p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">{t("statisticsAndProfits")}</p>
إحصائيات وأرباح
</p>
</div>
</Link>
</>
)}
{isAdmin && (
<>
<div className="border-t border-gray-100 my-2"></div>
<Link
href="/admin"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<Shield className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">لوحة التحكم</p>
<p className="text-xs text-gray-500">
إدارة المنصة
</p>
</div> </div>
</Link> </Link>
<Link <Link
href="/admin/users" href="/owner/account-book"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<Users className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">المستخدمين</p>
<p className="text-xs text-gray-500">
إدارة المستخدمين
</p>
</div>
</Link>
<Link
href="/admin/properties"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<Building className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">العقارات</p>
<p className="text-xs text-gray-500">
إدارة جميع العقارات
</p>
</div>
</Link>
<Link
href="/admin/bookings"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<Calendar className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">الحجوزات</p>
<p className="text-xs text-gray-500">
إدارة الحجوزات
</p>
</div>
</Link>
<Link
href="/admin/ledger"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors" className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)} onClick={() => setShowUserMenu(false)}
> >
<DollarSign className="w-5 h-5 text-amber-500" /> <DollarSign className="w-5 h-5 text-amber-500" />
<div> <div>
<p className="font-medium">دفتر الحسابات</p> <p className="font-medium">{t("accountBook")}</p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">{t("financialRecords")}</p>
إدارة المعاملات المالية
</p>
</div> </div>
</Link> </Link>
</> </>
@ -522,16 +425,46 @@ export default function ClientLayout({ children }) {
<div className="border-t border-gray-100 my-2"></div> <div className="border-t border-gray-100 my-2"></div>
<Link <Link
href="/tenant/bookings" href="/reservations"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors" className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)} onClick={() => setShowUserMenu(false)}
> >
<Calendar className="w-5 h-5 text-amber-500" /> <Calendar className="w-5 h-5 text-amber-500" />
<div> <div>
<p className="font-medium">حجوزاتي</p> <p className="font-medium">{t("myBookings")}</p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">{t("viewBookings")}</p>
عرض حجوزاتك </div>
</p> </Link>
<Link
href="/booked-properties"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<CalendarDays className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">{t("myBookedProperties")}</p>
<p className="text-xs text-gray-500">{t("viewAndRateBooked")}</p>
</div>
</Link>
<Link
href="/my-rates"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<Star className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">{t("myRatings")}</p>
<p className="text-xs text-gray-500">{t("ratingsReceived")}</p>
</div>
</Link>
<Link href="/reports" className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors" onClick={() => setShowUserMenu(false)}>
<FileText className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">{t("reports")}</p>
<p className="text-xs text-gray-500">{t("submitAndTrackReports")}</p>
</div> </div>
</Link> </Link>
</> </>
@ -539,16 +472,11 @@ export default function ClientLayout({ children }) {
<div className="border-t border-gray-100 my-2"></div> <div className="border-t border-gray-100 my-2"></div>
<button <button onClick={logout} className="w-full flex items-center gap-3 px-4 py-3 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
onClick={logout}
className="w-full flex items-center gap-3 px-4 py-3 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
<LogOut className="w-5 h-5" /> <LogOut className="w-5 h-5" />
<div> <div>
<p className="font-medium">تسجيل الخروج</p> <p className="font-medium">{t("logout")}</p>
<p className="text-xs text-red-400"> <p className="text-xs text-red-400">{t("endSession")}</p>
إنهاء الجلسة الحالية
</p>
</div> </div>
</button> </button>
</div> </div>
@ -563,9 +491,7 @@ export default function ClientLayout({ children }) {
<motion.button <motion.button
whileHover={{ scale: 1.1, rotate: 360 }} whileHover={{ scale: 1.1, rotate: 360 }}
whileTap={{ scale: 0.9 }} whileTap={{ scale: 0.9 }}
onClick={() => onClick={() => changeLanguage(currentLanguage === "en" ? "ar" : "en")}
changeLanguage(currentLanguage === "en" ? "ar" : "en")
}
className="flex items-center justify-center w-10 h-10 bg-gray-100 hover:bg-gray-200 rounded-full transition-colors" className="flex items-center justify-center w-10 h-10 bg-gray-100 hover:bg-gray-200 rounded-full transition-colors"
> >
<Globe className="w-5 h-5 text-gray-700" /> <Globe className="w-5 h-5 text-gray-700" />
@ -586,11 +512,7 @@ export default function ClientLayout({ children }) {
className="inline-flex items-center justify-center p-2 rounded-md text-gray-700 hover:text-amber-600 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-amber-500" className="inline-flex items-center justify-center p-2 rounded-md text-gray-700 hover:text-amber-600 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-amber-500"
> >
<span className="sr-only">{t("openMainMenu")}</span> <span className="sr-only">{t("openMainMenu")}</span>
{isMobileMenuOpen ? ( {isMobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
<X className="w-6 h-6" />
) : (
<Menu className="w-6 h-6" />
)}
</button> </button>
</div> </div>
</div> </div>
@ -601,79 +523,66 @@ export default function ClientLayout({ children }) {
initial={{ opacity: 0, y: -10 }} initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }} exit={{ opacity: 0, y: -10 }}
className="md:hidden bg-white border-t border-gray-200 shadow-lg" className={`md:hidden border-t shadow-lg ${isHomePage ? "bg-black/60 backdrop-blur-md border-white/10" : "bg-white border-gray-200"}`}
> >
<div className="px-2 pt-2 pb-3 space-y-1"> <div className="px-2 pt-2 pb-3 space-y-1">
<MobileNavLink href="/" onClick={closeMobileMenu}> <MobileNavLink light={isHomePage} href="/" onClick={closeMobileMenu}>
{t("home")} {t("home")}
</MobileNavLink> </MobileNavLink>
<MobileNavLink href="/properties" onClick={closeMobileMenu}> <MobileNavLink light={isHomePage} href="/properties" onClick={closeMobileMenu}>
{t("ourProducts")} {t("ourProducts")}
</MobileNavLink> </MobileNavLink>
{/* Download App - Mobile */} {/* Download App - Mobile */}
<div className="border-t border-gray-200 my-2"></div> <div className={`border-t my-2 ${isHomePage ? "border-white/10" : "border-gray-200"}`}></div>
<p className="px-3 py-1 text-xs text-gray-400 font-medium">تحميل التطبيق</p> <p className={`px-3 py-1 text-xs font-medium ${isHomePage ? "text-white/50" : "text-gray-400"}`}>{t("downloadApp")}</p>
<a href="/files/SweetHome.apk" download onClick={closeMobileMenu} <a
className="flex items-center gap-2 px-3 py-2 rounded-md text-green-600 hover:bg-green-50 transition-colors"> href="/files/SweetHome.apk"
download
onClick={closeMobileMenu}
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors ${isHomePage ? "text-green-400 hover:bg-white/10" : "text-green-600 hover:bg-green-50"}`}
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<path d="M2.76 3.061a.5.5 0 0 1 .679.2l1.283 2.352A8.9 8.9 0 0 1 8 5a8.9 8.9 0 0 1 3.278.613l1.283-2.352a.5.5 0 1 1 .878.478l-1.252 2.295C14.475 7.266 16 9.477 16 12H0c0-2.523 1.525-4.734 3.813-5.966L2.56 3.74a.5.5 0 0 1 .2-.678ZM5 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2m6 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/> <path d="M2.76 3.061a.5.5 0 0 1 .679.2l1.283 2.352A8.9 8.9 0 0 1 8 5a8.9 8.9 0 0 1 3.278.613l1.283-2.352a.5.5 0 1 1 .878.478l-1.252 2.295C14.475 7.266 16 9.477 16 12H0c0-2.523 1.525-4.734 3.813-5.966L2.56 3.74a.5.5 0 0 1 .2-.678ZM5 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2m6 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2" />
</svg> </svg>
<span className="text-sm font-medium">Android - تحميل APK</span> <span className="text-sm font-medium">
{t("android")} - {t("downloadAPK")}
</span>
</a> </a>
<div className="flex items-center gap-2 px-3 py-2 rounded-md text-gray-400 cursor-not-allowed opacity-50"> <div className="flex items-center gap-2 px-3 py-2 rounded-md text-gray-400 cursor-not-allowed opacity-50">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43Zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.385 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282Z"/> <path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43Zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.385 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282Z" />
</svg> </svg>
<span className="text-sm">iOS - قريباً</span> <span className="text-sm">
{t("ios")} - {t("comingSoon")}
</span>
</div> </div>
<div className="border-t border-gray-200 my-2"></div> <div className={`border-t my-2 ${isHomePage ? "border-white/10" : "border-gray-200"}`}></div>
{isAdmin && (
<MobileNavLink href="/admin" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<Shield className="w-4 h-4" />
الإدارة
</span>
</MobileNavLink>
)}
{isOwner && ( {isOwner && (
<> <>
<MobileNavLink <MobileNavLink light={isHomePage} href="/owner/properties" onClick={closeMobileMenu}>
href="/owner/properties"
onClick={closeMobileMenu}
>
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<Building className="w-4 h-4" /> <Building className="w-4 h-4" />
عقاراتي {t("myProperties")}
</span> </span>
</MobileNavLink> </MobileNavLink>
<MobileNavLink <MobileNavLink light={isHomePage} href="/owner/reservations" onClick={closeMobileMenu}>
href="/owner/bookings"
onClick={closeMobileMenu}
>
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<Calendar className="w-4 h-4" /> <Calendar className="w-4 h-4" />
الحجوزات {t("reservations")}
</span> </span>
</MobileNavLink> </MobileNavLink>
<MobileNavLink <MobileNavLink light={isHomePage} href="/owner/calendar" onClick={closeMobileMenu}>
href="/owner/calendar"
onClick={closeMobileMenu}
>
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<CalendarDays className="w-4 h-4" /> <CalendarDays className="w-4 h-4" />
التقويم {t("calendar")}
</span> </span>
</MobileNavLink> </MobileNavLink>
<MobileNavLink <MobileNavLink light={isHomePage} href="/owner/profits" onClick={closeMobileMenu}>
href="/owner/profits"
onClick={closeMobileMenu}
>
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<TrendingUp className="w-4 h-4" /> <TrendingUp className="w-4 h-4" />
الأرباح {t("profits")}
</span> </span>
</MobileNavLink> </MobileNavLink>
</> </>
@ -681,20 +590,17 @@ export default function ClientLayout({ children }) {
{!user && ( {!user && (
<> <>
<div className="border-t border-gray-200 my-2"></div> <div className={`border-t my-2 ${isHomePage ? "border-white/10" : "border-gray-200"}`}></div>
<MobileNavLink href="/login" onClick={closeMobileMenu}> <MobileNavLink light={isHomePage} href="/login" onClick={closeMobileMenu}>
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<LogIn className="w-4 h-4" /> <LogIn className="w-4 h-4" />
تسجيل الدخول {t("login")}
</span> </span>
</MobileNavLink> </MobileNavLink>
<MobileNavLink <MobileNavLink light={isHomePage} href="/auth/choose-role" onClick={closeMobileMenu}>
href="/auth/choose-role"
onClick={closeMobileMenu}
>
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<UserPlus className="w-4 h-4" /> <UserPlus className="w-4 h-4" />
إنشاء حساب {t("createAccount")}
</span> </span>
</MobileNavLink> </MobileNavLink>
</> </>
@ -705,76 +611,108 @@ export default function ClientLayout({ children }) {
</nav> </nav>
)} )}
{isHomePage && (
<div className="fixed inset-0 -z-10">
<motion.div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: "url(/hero.jpg)" }}
initial={{ scale: 1.1 }}
animate={{ scale: 1 }}
transition={{ duration: 1.5, ease: "easeOut" }}
/>
<div className="absolute inset-0 bg-gradient-to-r from-black/70 via-black/60 to-black/50" />
</div>
)}
<main <main
className={`${!isAuthPage && !isProfilePage ? "pt-20" : ""} min-h-screen bg-gradient-to-b from-gray-50 to-white ${currentLanguage === "ar" ? "text-right" : "text-left"}`} className={`${!isAuthPage && !isProfilePage && !isAuthenticated ? "pt-20" : ""} flex-1 flex flex-col ${isHomePage ? "bg-transparent" : "bg-gray-50"} ${currentLanguage === "ar" ? "text-right" : "text-left"} ${isHomePage ? "relative" : ""} ${isAuthenticated && !isAuthPage ? "pb-20" : ""}`}
> >
<FavoritesProvider> {!isAuthPage && isAuthenticated && (
{children} <div className={`flex items-center justify-between px-4 py-2 sticky top-0 z-20 bg-transparent backdrop-blur-3xl ${isHomePage ? "" : "border-b border-gray-200/20"}`}>
{!isAdmin && <FloatingSidebar isRTL={currentLanguage === 'ar'} />} <div className="flex items-center gap-2">
</FavoritesProvider> <span
className={`text-sm font-semibold px-3 py-1 rounded-lg ${!isAuthenticated ? (isHomePage ? "bg-white/20 text-white" : "bg-gray-200 text-gray-600") : isOwner ? (isHomePage ? "bg-amber-500/30 text-amber-200" : "bg-amber-100 text-amber-700") : isHomePage ? "bg-blue-500/30 text-blue-200" : "bg-blue-100 text-blue-700"}`}
>
{!isAuthenticated ? t("visitor") : isOwner ? t("owner") : t("customer")}
</span>
</div>
<div className="flex items-center gap-3">
<Link
href="/favorites"
className={`relative group flex items-center justify-center w-9 h-9 rounded-full transition-colors hover:bg-white/10 ${pathname === "/favorites" ? "text-amber-400" : "nav-icon"}`}
>
<Heart className="w-5 h-5" />
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
{t("favorites")}
</div>
</Link>
<Link
href="/notifications"
className={`relative group flex items-center justify-center w-9 h-9 rounded-full transition-colors hover:bg-white/10 ${pathname === "/notifications" ? "text-amber-400" : "nav-icon"}`}
>
<Bell className="w-5 h-5" />
{isMounted && unreadCount > 0 && (
<div className="absolute -top-1 -right-1 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">{unreadCount > 9 ? "9+" : unreadCount}</div>
)}
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
{t("notifications")}
</div>
</Link>
<Link
href="/settings"
className={`relative group flex items-center justify-center w-9 h-9 rounded-full transition-colors hover:bg-white/10 ${pathname === "/settings" ? "text-amber-400" : "nav-icon"}`}
>
<Settings className="w-5 h-5" />
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
{t("settings")}
</div>
</Link>
<button
onClick={() => changeLanguage(currentLanguage === "ar" ? "en" : "ar")}
className="relative group flex items-center justify-center w-9 h-9 rounded-full transition-colors hover:bg-white/10 nav-icon"
>
<Globe className="w-5 h-5" />
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
{currentLanguage === "ar" ? t("switchToEnglish") : t("switchToArabic")}
</div>
</button>
</div>
</div>
)}
<div className="flex-1">{children}</div>
{isAuthenticated && !isAuthPage && <BottomNav isOwner={isOwner} isOwnerOrAgent={isOwnerOrAgent} />}
</main> </main>
{!isAuthPage && !isProfilePage && ( {pathname === "/" && (
<footer className="bg-gray-900 text-white py-12"> <footer className="py-20" style={{backgroundColor:"#0f172a"}}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div <div className={`grid grid-cols-1 md:grid-cols-4 gap-8 ${currentLanguage === "ar" ? "text-right" : "text-left"}`}>
className={`grid grid-cols-1 md:grid-cols-4 gap-8 ${currentLanguage === "ar" ? "text-right" : "text-left"}`}
>
<div className="space-y-4"> <div className="space-y-4">
<div <div className={`flex items-center ${currentLanguage === "ar" ? "flex-row-reverse" : "space-x-3"}`}>
className={`flex items-center ${currentLanguage === "ar" ? "flex-row-reverse" : "space-x-3"}`}
>
<div className="relative w-10 h-10"> <div className="relative w-10 h-10">
<Image <Image src="/logo.png" alt={t("logoAlt")} fill className="object-contain" sizes="40px" />
src="/logo.png"
alt={t("logoAlt")}
fill
className="object-contain"
sizes="40px"
/>
</div> </div>
<span className="text-3xl font-bold"> <span className="text-3xl font-bold">
{t("brandNamePart1")} {t("brandNamePart1")}
<span className="text-amber-400"> <span className="text-amber-400">{t("brandNamePart2")}</span>
{t("brandNamePart2")}
</span>
</span> </span>
</div> </div>
<p className="text-gray-400 text-sm"> <p className="text-gray-400 text-sm">{t("footerDescription")}</p>
{t("footerDescription")}
</p>
</div> </div>
<div> <div>
<h3 className="text-lg font-semibold mb-4"> <h3 className="text-lg font-semibold mb-4">{t("quickLinks")}</h3>
{t("quickLinks")}
</h3>
<ul className="space-y-2"> <ul className="space-y-2">
<li> <li>
<Link <Link href="/" className="text-gray-400 hover:text-white transition-colors block py-1">
href="/"
className="text-gray-400 hover:text-white transition-colors block py-1"
>
{t("home")} {t("home")}
</Link> </Link>
</li> </li>
<li> <li>
<Link <Link href="/properties" className="text-gray-400 hover:text-white transition-colors block py-1">
href="/properties"
className="text-gray-400 hover:text-white transition-colors block py-1"
>
{t("ourProducts")} {t("ourProducts")}
</Link> </Link>
</li> </li>
{isAdmin && (
<li>
<Link
href="/admin"
className="text-gray-400 hover:text-white transition-colors block py-1"
>
الإدارة
</Link>
</li>
)}
</ul> </ul>
</div> </div>
<div> <div>
@ -782,7 +720,9 @@ export default function ClientLayout({ children }) {
<ul className="space-y-3 text-gray-400"> <ul className="space-y-3 text-gray-400">
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<Phone className="w-5 h-5" /> <Phone className="w-5 h-5" />
<span dir="ltr" className="text-right">{t("phone")}</span> <span dir="ltr" className="text-right">
{t("phone")}
</span>
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<Mail className="w-5 h-5" /> <Mail className="w-5 h-5" />
@ -800,6 +740,6 @@ export default function ClientLayout({ children }) {
</footer> </footer>
)} )}
<NotificationHandler /> <NotificationHandler />
</> </div>
); );
} }

View File

@ -0,0 +1,57 @@
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from "../enums";
export function mapApiProperty(t, item, index) {
const info = item.propertyInformation || {};
const dailyPrice = item.dailyRent ?? 0;
const monthlyPrice = item.monthlyRent ?? 0;
const salePrice = item.price ?? 0;
const isRentListing = Boolean(item.dailyRent != null || item.monthlyRent != null);
const price = isRentListing ? dailyPrice || monthlyPrice || 0 : salePrice;
const priceUnit = isRentListing ? (monthlyPrice ? "monthly" : "daily") : "sale";
const propType = BuildingTypeKeys[info.buildingType] ?? BuildingTypeKeys[item.type] ?? (item.type || "apartment");
const status = PropertyStatusKeys[info.status] ?? PropertyStatusKeys[item.status] ?? "available";
const features = [];
if (item.isSmokeAllow) features.push(t("smoke-allowed"));
if (item.isVisitorAllow) features.push(t("visitors-allowed"));
if (item.specializedFor) features.push(t("specialized"));
if (info.numberOfBedRooms) features.push(`${info.numberOfBedRooms} ${t("rooms")}`);
if (info.numberOfBathRooms) features.push(`${info.numberOfBathRooms} ${t("bathrooms")}`);
const apiBase = typeof window !== "undefined" ? process.env.NEXT_PUBLIC_API_URL || "https://45.93.137.91.nip.io/api" : "";
const rawImages = Array.isArray(info.images) ? info.images : [];
const images = rawImages.length > 0 ? rawImages.map((img) => (img.startsWith("http") ? img : `${apiBase}${img.startsWith("/") ? "" : "/Pictures/"}${img}`)) : ["/property-placeholder.jpg"];
const ownerSource = info.ownerType == null && item.ownerType == null ? "all" : [info.ownerType, item.ownerType].find((value) => value != null) === 1 ? "agency" : "owner";
return {
id: item.id ?? index + 1,
title: info.address || t("propertyWithId", { id: item.id || index + 1 }),
description: info.description || "",
type: propType,
price: price,
priceUSD: price,
priceUnit,
listingType: isRentListing ? "rent" : "sale",
location: {
city: extractCity(info.address) || "damascus",
district: info.address || "",
address: info.address || "",
lat: parseFloat(info.cordsX) || 0,
lng: parseFloat(info.cordsY) || 0,
},
bedrooms: info.numberOfBedRooms || 0,
bathrooms: info.numberOfBathRooms || 0,
area: info.space || 0,
features,
images,
status,
rating: item.rating || 4.5,
isNew: false,
allowedIdentities: ["syrian", "passport"],
priceDisplay: {
daily: dailyPrice,
monthly: monthlyPrice,
sale: salePrice,
},
ownerSource,
bookings: [],
_raw: item,
};
}

View File

@ -0,0 +1,20 @@
import toast from "react-hot-toast";
export const handleImageUpload = (side, file,t,setIdImagePreviews,setIdImages) => {
if (!file) return;
if (!file.type.startsWith("image/")) {
toast.error(t("register.selectValidImage"));
return;
}
if (file.size > 5 * 1024 * 1024) {
toast.error(t("register.imageSizeLimit"));
return;
}
const reader = new FileReader();
reader.onloadend = () => {
setIdImagePreviews((prev) => ({ ...prev, [side]: reader.result }));
};
reader.readAsDataURL(file);
setIdImages((prev) => ({ ...prev, [side]: file }));
toast.success(t("register.imageUploadSuccess"), { style: { background: "#dcfce7", color: "#166534" } });
};

View File

@ -0,0 +1,283 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import { Mail, Phone, Shield, ChevronLeft, Loader2, CheckCircle, XCircle, Send, Key } from "lucide-react";
import toast, { Toaster } from "react-hot-toast";
import AuthService from "../services/AuthService";
import { sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone } from "../utils/api";
import InteractiveBackground from "../components/Animation/Background";
export default function AccountVerificationPage() {
const router = useRouter();
const { t } = useTranslation();
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [emailCode, setEmailCode] = useState("");
const [phoneCode, setPhoneCode] = useState("");
const [sendingEmailOTP, setSendingEmailOTP] = useState(false);
const [sendingPhoneOTP, setSendingPhoneOTP] = useState(false);
const [verifyingEmail, setVerifyingEmail] = useState(false);
const [verifyingPhone, setVerifyingPhone] = useState(false);
const [emailVerified, setEmailVerified] = useState(false);
const [phoneVerified, setPhoneVerified] = useState(false);
const [showEmailInput, setShowEmailInput] = useState(false);
const [showPhoneInput, setShowPhoneInput] = useState(false);
useEffect(() => {
const authUser = AuthService.getUser();
if (authUser) {
setUser(authUser);
setIsLoading(false);
} else {
router.push("/login");
}
}, [router]);
const handleSendEmailOTP = async () => {
setSendingEmailOTP(true);
try {
await sendEmailOTP();
toast.success(t("accountVerification.emailOtpSent"));
setShowEmailInput(true);
} catch (err) {
toast.error(err.message || t("accountVerification.otpSendFailed"));
} finally {
setSendingEmailOTP(false);
}
};
const handleVerifyEmail = async () => {
if (!emailCode.trim()) {
toast.error(t("accountVerification.otpRequired"));
return;
}
setVerifyingEmail(true);
try {
await verifyEmail(emailCode.trim());
setEmailVerified(true);
setShowEmailInput(false);
toast.success(t("accountVerification.emailVerifiedSuccess"));
} catch (err) {
toast.error(err.message || t("accountVerification.otpInvalid"));
} finally {
setVerifyingEmail(false);
}
};
const handleSendPhoneOTP = async () => {
setSendingPhoneOTP(true);
try {
await sendPhoneOTP();
toast.success(t("accountVerification.phoneOtpSent"));
setShowPhoneInput(true);
} catch (err) {
toast.error(err.message || t("accountVerification.otpSendFailed"));
} finally {
setSendingPhoneOTP(false);
}
};
const handleVerifyPhone = async () => {
if (!phoneCode.trim()) {
toast.error(t("accountVerification.otpRequired"));
return;
}
setVerifyingPhone(true);
try {
await verifyPhone(phoneCode.trim());
setPhoneVerified(true);
setShowPhoneInput(false);
toast.success(t("accountVerification.phoneVerifiedSuccess"));
} catch (err) {
toast.error(err.message || t("accountVerification.otpInvalid"));
} finally {
setVerifyingPhone(false);
}
};
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1 },
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
return (
<div className="min-h-screen py-8" dir="rtl">
<Toaster position="top-center" reverseOrder={false} />
<InteractiveBackground />
<div className="relative z-10 container mx-auto px-4 max-w-2xl">
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="flex items-center gap-3 mb-8">
<Link href="/settings" className="p-2 rounded-xl hover:bg-gray-200 transition-colors text-gray-600">
<ChevronLeft className="w-5 h-5" />
</Link>
<h1 className="text-2xl font-bold text-gray-900">{t("accountVerification.title")}</h1>
</motion.div>
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="space-y-6">
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
<Mail className="w-5 h-5 text-amber-600" />
<h2 className="text-lg font-semibold text-gray-800">{t("accountVerification.emailSection")}</h2>
{emailVerified && (
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
<CheckCircle className="w-3 h-3" />
{t("accountVerification.verified")}
</span>
)}
</div>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<p className="text-sm text-gray-600">{user?.email || t("accountVerification.emailNotRegistered")}</p>
<p className="text-xs text-gray-400 mt-1">{emailVerified ? t("accountVerification.emailVerifiedDesc") : t("accountVerification.emailVerifyDesc")}</p>
</div>
{!emailVerified && !showEmailInput && (
<button
onClick={handleSendEmailOTP}
disabled={sendingEmailOTP}
className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50"
>
{sendingEmailOTP ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
{sendingEmailOTP ? t("accountVerification.sending") : t("accountVerification.sendOtp")}
</button>
)}
</div>
{showEmailInput && (
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} className="space-y-3">
<p className="text-xs text-gray-500">{t("accountVerification.enter6DigitCode")}</p>
<div className="flex gap-2">
<input
type="text"
value={emailCode}
onChange={(e) => setEmailCode(e.target.value)}
placeholder={t("accountVerification.otpPlaceholder")}
maxLength={6}
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
/>
<button
onClick={handleVerifyEmail}
disabled={verifyingEmail}
className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2"
>
{verifyingEmail ? <Loader2 className="w-4 h-4 animate-spin" /> : <Key className="w-4 h-4" />}
{t("accountVerification.verify")}
</button>
</div>
<button onClick={handleSendEmailOTP} disabled={sendingEmailOTP} className="text-xs text-amber-600 hover:text-amber-700">
{t("accountVerification.resendCode")}
</button>
</motion.div>
)}
{emailVerified && (
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
<CheckCircle className="w-5 h-5" />
<span className="text-sm font-medium">{t("accountVerification.emailVerifiedSuccess")}</span>
</div>
)}
</div>
</motion.div>
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
<Phone className="w-5 h-5 text-amber-600" />
<h2 className="text-lg font-semibold text-gray-800">{t("accountVerification.phoneSection")}</h2>
{phoneVerified && (
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
<CheckCircle className="w-3 h-3" />
{t("accountVerification.verified")}
</span>
)}
</div>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<p className="text-sm text-gray-600">{user?.phone || t("accountVerification.phoneNotRegistered")}</p>
<p className="text-xs text-gray-400 mt-1">{phoneVerified ? t("accountVerification.phoneVerifiedDesc") : t("accountVerification.phoneVerifyDesc")}</p>
</div>
{!phoneVerified && !showPhoneInput && (
<button
onClick={handleSendPhoneOTP}
disabled={sendingPhoneOTP}
className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50"
>
{sendingPhoneOTP ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
{sendingPhoneOTP ? t("accountVerification.sending") : t("accountVerification.sendOtp")}
</button>
)}
</div>
{showPhoneInput && (
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} className="space-y-3">
<p className="text-xs text-gray-500">{t("accountVerification.enter6DigitCodePhone")}</p>
<div className="flex gap-2">
<input
type="text"
value={phoneCode}
onChange={(e) => setPhoneCode(e.target.value)}
placeholder={t("accountVerification.otpPlaceholder")}
maxLength={6}
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
/>
<button
onClick={handleVerifyPhone}
disabled={verifyingPhone}
className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2"
>
{verifyingPhone ? <Loader2 className="w-4 h-4 animate-spin" /> : <Key className="w-4 h-4" />}
{t("accountVerification.verify")}
</button>
</div>
<button onClick={handleSendPhoneOTP} disabled={sendingPhoneOTP} className="text-xs text-amber-600 hover:text-amber-700">
{t("accountVerification.resendCode")}
</button>
</motion.div>
)}
{phoneVerified && (
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
<CheckCircle className="w-5 h-5" />
<span className="text-sm font-medium">{t("accountVerification.phoneVerifiedSuccess")}</span>
</div>
)}
</div>
</motion.div>
<motion.div variants={itemVariants} className="bg-gradient-to-br from-amber-500 to-amber-600 rounded-2xl p-6 text-white">
<div className="flex items-center gap-3 mb-3">
<Shield className="w-6 h-6" />
<h3 className="text-lg font-semibold">{t("accountVerification.whyVerify")}</h3>
</div>
<ul className="space-y-2 text-sm " style={{ color: "#0f172a", fontWeight: "400" }}>
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" />
{t("accountVerification.reason1")}
</li>
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" />
{t("accountVerification.reason2")}
</li>
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" />
{t("accountVerification.reason3")}
</li>
</ul>
</motion.div>
</motion.div>
</div>
</div>
);
}

View File

@ -1,27 +0,0 @@
'use client';
import { motion } from 'framer-motion';
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
import Link from 'next/link';
export default function Error({ error, reset }) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4">
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center max-w-md">
<div className="w-20 h-20 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-6">
<AlertTriangle className="w-10 h-10 text-red-500" />
</div>
<h2 className="text-2xl font-bold text-white mb-2">حدث خطأ</h2>
<p className="text-gray-400 mb-8">نعتذر، حدث خطأ أثناء تحميل الصفحة</p>
<div className="flex gap-3 justify-center">
<button onClick={reset} className="flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors">
<RefreshCw className="w-5 h-5" /> إعادة المحاولة
</button>
<Link href="/" className="flex items-center gap-2 bg-white/10 text-gray-300 px-6 py-3 rounded-xl font-medium hover:bg-white/20 transition-colors">
<Home className="w-5 h-5" /> الرئيسية
</Link>
</div>
</motion.div>
</div>
);
}

View File

@ -1,14 +0,0 @@
'use client';
import { motion } from 'framer-motion';
export default function Loading() {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-400 text-lg">جاري التحميل...</p>
</motion.div>
</div>
);
}

View File

@ -1,230 +0,0 @@
'use client';
import { motion } from 'framer-motion';
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import Link from 'next/link';
import {
Home,
Calendar,
Users,
DollarSign,
TrendingUp,
Bell,
Frown
} from 'lucide-react';
import DashboardStats from '../components/admin/DashboardStats';
import PropertiesTable from '../components/admin/PropertiesTable';
import BookingRequests from '../components/admin/BookingRequests';
import UsersList from '../components/admin/UsersList';
import LedgerBook from '../components/admin/LedgerBook';
import AddPropertyForm from '../components/admin/AddPropertyForm';
import { PropertyProvider } from '../contexts/PropertyContext';
import AuthService from '../services/AuthService';
import '../i18n/config';
export default function AdminPage() {
const { t, i18n } = useTranslation();
const [activeTab, setActiveTab] = useState('dashboard');
const [showAddProperty, setShowAddProperty] = useState(false);
const [notifications, setNotifications] = useState(3);
const [isAdmin, setIsAdmin] = useState(false);
const [checked, setChecked] = useState(false);
useEffect(() => {
setIsAdmin(AuthService.isAuthenticated() && AuthService.isAdmin());
setChecked(true);
}, []);
// ─── 404 for non-admins ───
if (checked && !isAdmin) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center max-w-md"
>
<div className="mb-6">
<svg viewBox="0 0 200 180" className="w-72 h-52 mx-auto">
<circle cx="100" cy="70" r="60" fill="#fef3c7" />
<circle cx="80" cy="60" r="8" fill="#92400e" />
<circle cx="120" cy="60" r="8" fill="#92400e" />
<path d="M80 85 Q100 75 120 85" stroke="#92400e" strokeWidth="3" fill="none" strokeLinecap="round" />
<text x="100" y="140" textAnchor="middle" fontSize="16" fontWeight="bold" fill="#6b7280">عذراً!</text>
<text x="100" y="160" textAnchor="middle" fontSize="12" fill="#9ca3af">الصفحة غير موجودة</text>
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">404 - الصفحة غير موجودة</h2>
<p className="text-gray-500 mb-8">عذراً، لا يمكنك الوصول إلى هذه الصفحة</p>
<Link
href="/"
className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors"
>
<Home className="w-5 h-5" />
العودة للرئيسية
</Link>
</motion.div>
</div>
);
}
if (!checked) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin" />
</div>
);
}
const tabs = [
{ id: 'dashboard', label: 'لوحة التحكم', icon: Home },
{ id: 'properties', label: 'العقارات', icon: Home },
{ id: 'bookings', label: 'طلبات الحجز', icon: Calendar, badge: notifications },
{ id: 'users', label: 'المستخدمين', icon: Users },
{ id: 'ledger', label: 'دفتر الحسابات', icon: DollarSign },
// { id: 'reports', label: 'التقارير', icon: TrendingUp }
];
return (
<PropertyProvider>
<div className={`min-h-screen bg-gray-50 p-4 md:p-6 ${i18n.language === 'ar' ? 'text-right' : 'text-left'}`}>
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 mb-2">
{t('adminDashboard')}
</h1>
<p className="text-gray-600">
إدارة العقارات، الحجوزات، والحسابات المالية
</p>
</div>
<button className="relative p-2 hover:bg-gray-100 rounded-lg">
<Bell className="w-6 h-6 text-gray-600" />
{notifications > 0 && (
<span className="absolute top-0 right-0 w-4 h-4 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
{notifications}
</span>
)}
</button>
</div>
</motion.div>
<div className="mb-6 border-b border-gray-200">
<div className="flex flex-wrap gap-2">
{tabs.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`
px-4 py-3 font-medium text-sm rounded-t-lg transition-all relative
${activeTab === tab.id
? 'bg-white border-t border-x border-gray-300 text-blue-700'
: 'text-gray-700 hover:text-blue-600 hover:bg-gray-100'
}
`}
>
<div className="flex items-center gap-2">
<Icon className="w-4 h-4" />
<span>{tab.label}</span>
{tab.badge && (
<span className="bg-red-500 text-white text-xs px-2 py-0.5 rounded-full">
{tab.badge}
</span>
)}
</div>
</button>
);
})}
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
{activeTab === 'dashboard' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<DashboardStats />
</motion.div>
)}
{activeTab === 'properties' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<div className="flex justify-between items-center mb-6">
<div>
<h2 className="text-xl font-bold">إدارة العقارات</h2>
<p className="text-gray-600 text-sm">إضافة وتعديل العقارات مع تحديد نسب الأرباح</p>
</div>
<button
onClick={() => setShowAddProperty(true)}
className="bg-blue-700 text-white px-4 py-2 rounded-lg hover:bg-blue-800"
>
إضافة عقار جديد
</button>
</div>
<PropertiesTable />
</motion.div>
)}
{activeTab === 'bookings' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<BookingRequests />
</motion.div>
)}
{activeTab === 'users' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<UsersList />
</motion.div>
)}
{activeTab === 'ledger' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<LedgerBook userType="admin" />
</motion.div>
)}
{activeTab === 'reports' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<div className="text-center py-12 text-gray-500">
قريباً... تقارير متقدمة
</div>
</motion.div>
)}
</div>
{showAddProperty && (
<AddPropertyForm
onClose={() => setShowAddProperty(false)}
onSuccess={() => {
setShowAddProperty(false);
}}
/>
)}
</div>
</PropertyProvider>
);
}

View File

@ -6,7 +6,7 @@ import Link from 'next/link';
export default function Error({ error, reset }) { export default function Error({ error, reset }) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4"> <div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center max-w-md"> <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center max-w-md">
<div className="w-20 h-20 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-6"> <div className="w-20 h-20 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-6">
<AlertTriangle className="w-10 h-10 text-red-500" /> <AlertTriangle className="w-10 h-10 text-red-500" />

View File

@ -1,14 +0,0 @@
'use client';
import { motion } from 'framer-motion';
export default function Loading() {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-400 text-lg">جاري التحميل...</p>
</motion.div>
</div>
);
}

View File

@ -2,9 +2,12 @@
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import Link from 'next/link'; import Link from 'next/link';
import { Home, Building, ArrowLeft } from 'lucide-react'; import { Home, Building, Briefcase, ArrowLeft } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import InteractiveBackground from '@/app/components/Animation/Background';
export default function ChooseRolePage() { export default function ChooseRolePage() {
const { t, i18n } = useTranslation();
const containerVariants = { const containerVariants = {
hidden: { opacity: 0 }, hidden: { opacity: 0 },
visible: { visible: {
@ -35,7 +38,8 @@ export default function ChooseRolePage() {
}; };
return ( return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden"> <div dir={i18n.language === 'ar' ? 'rtl' : 'ltr'} className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
<InteractiveBackground/>
<div className="absolute inset-0 overflow-hidden"> <div className="absolute inset-0 overflow-hidden">
{[...Array(20)].map((_, i) => ( {[...Array(20)].map((_, i) => (
<motion.div <motion.div
@ -77,7 +81,7 @@ export default function ChooseRolePage() {
<motion.div whileHover={{ x: -5 }}> <motion.div whileHover={{ x: -5 }}>
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="w-4 h-4" />
</motion.div> </motion.div>
<span>العودة للرئيسية</span> <span>{t('backToHome')}</span>
</Link> </Link>
</motion.div> </motion.div>
@ -86,17 +90,17 @@ export default function ChooseRolePage() {
className="text-center mb-12" className="text-center mb-12"
> >
<motion.h1 <motion.h1
className="text-4xl md:text-5xl font-bold text-white mb-4" className="text-4xl md:text-5xl font-bold text-amber-50 mb-4"
animate={floatingAnimation} animate={floatingAnimation}
> >
مرحباً بك في SweetHome {t('chooseRole.welcomeToSweetHome')}
</motion.h1> </motion.h1>
<p className="text-xl text-gray-400"> <p className="text-xl text-gray-400">
اختر نوع الحساب المناسب لك {t('chooseRole.subtitle')}
</p> </p>
</motion.div> </motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<motion.div <motion.div
variants={itemVariants} variants={itemVariants}
whileHover={{ scale: 1.05, y: -5 }} whileHover={{ scale: 1.05, y: -5 }}
@ -123,23 +127,23 @@ export default function ChooseRolePage() {
<Building className="w-10 h-10 text-white" /> <Building className="w-10 h-10 text-white" />
</motion.div> </motion.div>
<h2 className="text-3xl font-bold mb-3">صاحب عقار</h2> <h2 className="text-3xl font-bold mb-3">{t('chooseRole.owner')}</h2>
<p className="text-amber-100 mb-6"> <p className="text-amber-100 mb-6">
تريد عرض عقار للإيجار أو البيع؟ انضم كمالك عقار وابدأ الآن {t('chooseRole.ownerDesc')}
</p> </p>
<ul className="space-y-2 text-sm text-amber-100"> <ul className="space-y-2 text-sm text-amber-100">
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" /> <span className="w-1.5 h-1.5 bg-white rounded-full" />
إضافة عقارات غير محدودة {t('chooseRole.ownerFeature1')}
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" /> <span className="w-1.5 h-1.5 bg-white rounded-full" />
إدارة الحجوزات بسهولة {t('chooseRole.ownerFeature2')}
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" /> <span className="w-1.5 h-1.5 bg-white rounded-full" />
تواصل مباشر مع المستأجرين {t('chooseRole.ownerFeature3')}
</li> </li>
</ul> </ul>
@ -148,7 +152,7 @@ export default function ChooseRolePage() {
whileHover={{ scale: 1.02 }} whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
> >
إنشاء حساب مالك {t('chooseRole.createOwnerAccount')}
</motion.button> </motion.button>
</div> </div>
</div> </div>
@ -181,23 +185,23 @@ export default function ChooseRolePage() {
<Home className="w-10 h-10 text-white" /> <Home className="w-10 h-10 text-white" />
</motion.div> </motion.div>
<h2 className="text-3xl font-bold mb-3">مستأجر / مشتري</h2> <h2 className="text-3xl font-bold mb-3">{t('chooseRole.tenant')}</h2>
<p className="text-blue-100 mb-6"> <p className="text-blue-100 mb-6">
تبحث عن عقار للإيجار أو الشراء؟ انضم كعميل وابحث عن منزل أحلامك {t('chooseRole.tenantDesc')}
</p> </p>
<ul className="space-y-2 text-sm text-blue-100"> <ul className="space-y-2 text-sm text-blue-100">
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" /> <span className="w-1.5 h-1.5 bg-white rounded-full" />
آلاف العقارات المتاحة {t('chooseRole.tenantFeature1')}
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" /> <span className="w-1.5 h-1.5 bg-white rounded-full" />
بحث متقدم بفلاتر ذكية {t('chooseRole.tenantFeature2')}
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" /> <span className="w-1.5 h-1.5 bg-white rounded-full" />
حجز وإستئجار فوري {t('chooseRole.tenantFeature3')}
</li> </li>
</ul> </ul>
@ -206,7 +210,65 @@ export default function ChooseRolePage() {
whileHover={{ scale: 1.02 }} whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
> >
إنشاء حساب مستأجر {t('chooseRole.createTenantAccount')}
</motion.button>
</div>
</div>
</Link>
</motion.div>
<motion.div
variants={itemVariants}
whileHover={{ scale: 1.05, y: -5 }}
whileTap={{ scale: 0.95 }}
className="group cursor-pointer"
>
<Link href="/register/agent">
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-3xl p-8 text-white shadow-2xl shadow-purple-500/20 relative overflow-hidden">
<motion.div
className="absolute -top-20 -right-20 w-40 h-40 bg-white/10 rounded-full"
animate={{
scale: [1, 1.2, 1],
rotate: [0, 90, 0],
}}
transition={{ duration: 8, repeat: Infinity }}
/>
<div className="relative z-10">
<motion.div
className="w-20 h-20 bg-white/20 rounded-2xl flex items-center justify-center mb-6 backdrop-blur-sm"
whileHover={{ rotate: 360 }}
transition={{ duration: 0.5 }}
>
<Briefcase className="w-10 h-10 text-white" />
</motion.div>
<h2 className="text-3xl font-bold mb-3">{t('chooseRole.agent')}</h2>
<p className="text-purple-100 mb-6">
{t('chooseRole.agentDesc')}
</p>
<ul className="space-y-2 text-sm text-purple-100">
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
{t('chooseRole.agentFeature1')}
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
{t('chooseRole.agentFeature2')}
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
{t('chooseRole.agentFeature3')}
</li>
</ul>
<motion.button
className="mt-8 w-full bg-white text-purple-600 py-3 rounded-xl font-bold hover:bg-purple-50 transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{t('chooseRole.createAgentAccount')}
</motion.button> </motion.button>
</div> </div>
</div> </div>
@ -218,12 +280,12 @@ export default function ChooseRolePage() {
variants={itemVariants} variants={itemVariants}
className="text-center text-gray-400 mt-8" className="text-center text-gray-400 mt-8"
> >
لديك حساب بالفعل؟{' '} {t('chooseRole.alreadyHaveAccount')}{' '}
<Link <Link
href="/login" href="/login"
className="text-amber-400 hover:text-amber-300 font-medium transition-colors" className="text-amber-400 hover:text-amber-300 font-medium transition-colors"
> >
تسجيل الدخول {t('login')}
</Link> </Link>
</motion.p> </motion.p>
</motion.div> </motion.div>

152
app/blocked/page.js Normal file
View File

@ -0,0 +1,152 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import { ShieldAlert, LogOut, MessageSquare, Send, Loader2 } from "lucide-react";
import toast, { Toaster } from "react-hot-toast";
import AuthService from "../services/AuthService";
import { sendGeneralReport } from "../utils/api";
import InteractiveBackground from "../components/Animation/Background";
export default function BlockedPage() {
const { t } = useTranslation();
const router = useRouter();
const [form, setForm] = useState({ subject: "", body: "" });
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSent, setIsSent] = useState(false);
const handleLogout = () => {
AuthService.deleteToken();
router.replace("/");
};
const updateField = (field, value) => {
setForm((current) => ({ ...current, [field]: value }));
if (isSent) setIsSent(false);
};
const handleSubmit = async (event) => {
event.preventDefault();
if (!form.subject.trim() || !form.body.trim()) {
toast.error(t("blocked.fillAllFields"));
return;
}
setIsSubmitting(true);
try {
await sendGeneralReport(form.subject.trim(), form.body.trim());
setIsSent(true);
setForm({ subject: "", body: "" });
toast.success(t("blocked.reportSent"));
} catch (error) {
toast.error(t("blocked.sendError"));
} finally {
setIsSubmitting(false);
}
};
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4" dir="rtl">
<Toaster position="top-center" reverseOrder={false} />
<InteractiveBackground />
<motion.div initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} className="w-full max-w-5xl relative z-10">
<div className="text-center mb-10">
<motion.div initial={{ scale: 0.9 }} animate={{ scale: 1 }} className="w-24 h-24 bg-red-100 rounded-3xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-red-100">
<ShieldAlert className="w-12 h-12 text-red-600" />
</motion.div>
<h1 className="text-4xl font-bold text-gray-900 mb-3">{t("blocked.title")}</h1>
<p className="text-gray-600 text-lg max-w-2xl mx-auto">{t("blocked.description")}</p>
</div>
<div className="grid md:grid-cols-2 gap-6 relative z-10 ">
<motion.div
initial={{ opacity: 0, x: -24 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 }}
className="bg-white rounded-3xl shadow-sm border border-gray-200 p-8 flex flex-col justify-between"
>
<div>
<div className="w-14 h-14 bg-red-50 rounded-2xl flex items-center justify-center mb-6">
<LogOut className="w-7 h-7 text-red-600" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-3">{t("blocked.logoutTitle")}</h2>
<p className="text-gray-600 leading-7">{t("blocked.logoutDesc")}</p>
</div>
<button
type="button"
onClick={handleLogout}
className="mt-8 w-full cursor-pointer bg-red-600 hover:bg-red-700 text-white rounded-2xl py-4 font-bold transition-colors flex items-center justify-center gap-3"
>
<LogOut className="w-5 h-5" />
{t("blocked.logoutButton")}
</button>
</motion.div>
<motion.div initial={{ opacity: 0, x: 24 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }} className="bg-white rounded-3xl shadow-sm border border-gray-200 p-8">
<div className="flex items-center gap-4 mb-6">
<div className="w-14 h-14 bg-amber-50 rounded-2xl flex items-center justify-center">
<MessageSquare className="w-7 h-7 text-amber-600" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900">{t("blocked.contactSupportTitle")}</h2>
<p className="text-gray-600 mt-1">{t("blocked.contactSupportDesc")}</p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label className="block text-sm font-bold text-gray-700 mb-2">{t("subject")}</label>
<input
type="text"
value={form.subject}
onChange={(event) => updateField("subject", event.target.value)}
placeholder={t("blocked.subjectPlaceholder")}
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-2xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400"
/>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-2">{t("blocked.messageLabel")}</label>
<textarea
value={form.body}
onChange={(event) => updateField("body", event.target.value)}
rows={6}
placeholder={t("blocked.messagePlaceholder")}
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-2xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 resize-none"
/>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-amber-500 hover:bg-amber-600 text-white rounded-2xl py-4 font-bold transition-colors flex items-center justify-center gap-3 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
{t("blocked.sending")}
</>
) : isSent ? (
<>
<Send className="w-5 h-5" />
{t("blocked.sentButton")}
</>
) : (
<>
<Send className="w-5 h-5" />
{t("blocked.sendButton")}
</>
)}
</button>
</form>
</motion.div>
</div>
</motion.div>
</div>
);
}

View File

@ -0,0 +1,220 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import {
Home, Star, MapPin, Calendar, Clock, Check, X, Loader2,
User, MessageCircle, ChevronDown, Image as ImageIcon,
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import { getUserReservations } from '../utils/api';
import AuthService from '../services/AuthService';
import PropertyRatingForm from '../components/ratings/PropertyRatingForm';
const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
const STATUS_CONFIG = {
pending: { color: 'bg-yellow-100 text-yellow-800 border-yellow-300' },
ownerConfirmed: { color: 'bg-blue-100 text-blue-800 border-blue-300' },
depositPaid: { color: 'bg-orange-100 text-orange-800 border-orange-300' },
depositConfirmed: { color: 'bg-green-100 text-green-800 border-green-300' },
completed: { color: 'bg-teal-100 text-teal-800 border-teal-300' },
cancelled: { color: 'bg-red-100 text-red-800 border-red-300' },
};
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
function StatusBadge({ code }) {
const { t } = useTranslation();
const key = STATUS_MAP[code] || 'pending';
const cfg = STATUS_CONFIG[key];
return (
<span className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium border ${cfg.color}`}>
<Clock className="w-3 h-3" /> {t(`bookingStatus.${key}`)}
</span>
);
}
function ReservationCard({ reservation: r, onRate }) {
const { t } = useTranslation();
const isCompleted = STATUS_MAP[r.status] === 'completed';
const imgSrc = r.propertyImage || r._prop?.images?.[0] || (r.propertyInfo?.images?.[0]);
const imageUrl = imgSrc ? `${API_BASE}${imgSrc}` : null;
const address = r.propertyAddress || r._prop?.address || '';
const beds = r._prop?.numberOfBedRooms ?? r.numberOfBedRooms ?? 0;
const baths = r._prop?.numberOfBathRooms ?? r.numberOfBathRooms ?? 0;
return (
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-all border border-gray-200 overflow-hidden">
<div className="p-5 space-y-4">
<div className="flex justify-between items-start">
<div className="flex items-center gap-2">
<Home className="w-5 h-5 text-amber-500" />
<span className="font-semibold text-gray-900">{t('propertyId', { id: r.propertyId || r.id })}</span>
</div>
<StatusBadge code={r.status} />
</div>
{imageUrl ? (
<div className="w-full h-44 rounded-xl overflow-hidden bg-gray-100">
<img src={imageUrl} alt="" className="w-full h-full object-cover" />
</div>
) : (
<div className="w-full h-44 rounded-xl bg-gray-100 flex items-center justify-center">
<ImageIcon className="w-12 h-12 text-gray-300" />
</div>
)}
{address && (
<div className="flex items-center gap-1 text-gray-500 text-sm">
<MapPin className="w-4 h-4 flex-shrink-0" />
{address}
</div>
)}
{(beds > 0 || baths > 0) && (
<div className="flex gap-3 text-sm text-gray-600">
{beds > 0 && <span>{beds} {t('rooms')}</span>}
{baths > 0 && <span>{baths} {t('bathrooms')}</span>}
</div>
)}
<div className="grid grid-cols-2 gap-3">
<div className="bg-gray-50 p-3 rounded-xl text-center">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
<div className="text-xs text-gray-500">{t('from')}</div>
<div className="text-sm font-medium">{new Date(r.startDate).toLocaleDateString('ar')}</div>
</div>
<div className="bg-gray-50 p-3 rounded-xl text-center">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
<div className="text-xs text-gray-500">{t('to')}</div>
<div className="text-sm font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</div>
</div>
</div>
<div className="bg-amber-50 p-3 rounded-xl text-center">
<div className="text-lg font-bold text-amber-600">{r.totalPrice?.toLocaleString() ?? '—'}</div>
<div className="text-xs text-gray-500">{t('totalPrice')}</div>
</div>
<div className="flex items-center gap-2 text-xs text-gray-400">
<User className="w-3 h-3" />
<span>{t('reservationNumber', { id: r.id })}</span>
</div>
{isCompleted && (
<button onClick={onRate}
className="w-full bg-amber-500 hover:bg-amber-600 text-white py-2.5 rounded-xl text-sm font-medium transition flex items-center justify-center gap-2">
<Star className="w-4 h-4" /> {t('rateProperty')}
</button>
)}
</div>
</motion.div>
);
}
export default function BookedPropertiesPage() {
const { t } = useTranslation();
const [reservations, setReservations] = useState([]);
const [loading, setLoading] = useState(true);
const [ratingReservation, setRatingReservation] = useState(null);
const [expandedId, setExpandedId] = useState(null);
useEffect(() => {
if (!AuthService.getToken()) {
toast.error(t('loginRequired'));
setLoading(false);
return;
}
loadReservations();
}, []);
const loadReservations = useCallback(async () => {
try {
const data = await getUserReservations();
setReservations(Array.isArray(data) ? data : []);
} catch (err) {
console.error(err);
toast.error(t('failedToLoadBookings'));
setReservations([]);
} finally {
setLoading(false);
}
}, []);
return (
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
<Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4">
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('bookedProperties')}</h1>
<p className="text-gray-600">{t('youHaveBookings', { count: reservations.length })}</p>
</div>
{reservations.length > 0 && (
<button onClick={() => setExpandedId(expandedId ? null : 'all')}
className="flex items-center gap-1 text-sm text-amber-600 hover:text-amber-700 transition">
<ChevronDown className={`w-4 h-4 transition-transform ${expandedId ? 'rotate-180' : ''}`} />
{expandedId ? t('collapseAll') : t('showAll')}
</button>
)}
</div>
</motion.div>
{reservations.length === 0 ? (
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
<Home className="w-16 h-16 text-amber-500 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-900 mb-2">{t('noBookings')}</h3>
<p className="text-gray-500">{t('noBookingsDesc')}</p>
</motion.div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{reservations.map((r, i) => (
<ReservationCard key={r.id || i} reservation={r} onRate={() => setRatingReservation(r)} />
))}
</div>
)}
{reservations.filter(r => STATUS_MAP[r.status] === 'completed').length > 0 && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}
className="mt-8 bg-amber-50 border border-amber-200 rounded-xl p-4 flex items-start gap-3">
<MessageCircle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
<div>
<p className="text-amber-800 font-medium">{t('rateProperties')}</p>
<p className="text-amber-600 text-sm">{t('ratePropertiesDesc')}</p>
</div>
</motion.div>
)}
</div>
<AnimatePresence>
{ratingReservation && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50"
onClick={() => setRatingReservation(null)}>
<motion.div initial={{ scale: 0.9, y: 20 }} animate={{ scale: 1, y: 0 }} exit={{ scale: 0.9, y: 20 }}
className="w-full max-w-lg" onClick={e => e.stopPropagation()}>
<div className="relative">
<button onClick={() => setRatingReservation(null)}
className="absolute left-2 top-2 z-10 bg-white/80 rounded-full p-1 hover:bg-white transition shadow">
<X className="w-5 h-5 text-gray-600" />
</button>
<PropertyRatingForm
reservationId={ratingReservation.id}
onSuccess={() => { setRatingReservation(null); toast.success(t('ratingSubmitted')); }}
onCancel={() => setRatingReservation(null)}
/>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}

172
app/change-password/page.js Normal file
View File

@ -0,0 +1,172 @@
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
import { useRouter } from "next/navigation";
import toast, { Toaster } from "react-hot-toast";
import { Lock, Eye, EyeOff, ArrowLeft, Shield } from "lucide-react";
import { useTranslation } from "react-i18next";
import { changePassword } from "../utils/api";
import AuthService from "../services/AuthService";
import InteractiveBackground from "../components/Animation/Background";
export default function ChangePasswordPage() {
const router = useRouter();
const { t } = useTranslation();
const [form, setForm] = useState({ oldPassword: "", newPassword: "", confirmPassword: "" });
const [show, setShow] = useState({ old: false, new: false, confirm: false });
const [isLoading, setIsLoading] = useState(false);
const handleChange = (field) => (e) => setForm({ ...form, [field]: e.target.value });
const toggleShow = (field) => setShow({ ...show, [field]: !show[field] });
const handleSubmit = async (e) => {
e.preventDefault();
if (!AuthService.isAuthenticated()) {
toast.error(t("changePassword.loginRequired"));
router.push("/login");
return;
}
if (form.newPassword !== form.confirmPassword) {
toast.error(t("changePassword.passwordsDoNotMatch"));
return;
}
if (form.newPassword.length < 6) {
toast.error(t("validation.passwordMinLength"));
return;
}
setIsLoading(true);
try {
await changePassword(form.oldPassword, form.newPassword);
toast.success(t("changePassword.changeSuccess"));
setTimeout(() => router.push("/profile"), 1200);
} catch (err) {
toast.error(err?.message || t("changePassword.changeFailed"));
} finally {
setIsLoading(false);
}
};
const inputClass =
"w-full pr-12 pl-4 py-3 bg-gray-50 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 transition-all";
return (
<div className="min-h-screen relative z-10 flex items-center justify-center p-4" style={{ backgroundColor: "#0f172a" }}>
<Toaster position="top-center" reverseOrder={false} />
<InteractiveBackground />
<motion.div initial={{ opacity: 0, y: 30 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} className="w-full max-w-md relative z-10 ">
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} className="mb-6 relative z-10">
<button onClick={() => router.push("/profile")} className="flex items-center relative z-10 gap-2 text-gray-600 hover:text-amber-600 transition-colors">
<ArrowLeft className="w-5 h-5" />
<span>{t("changePassword.backToProfile")}</span>
</button>
</motion.div>
<div className=" rounded-3xl shadow-xl overflow-hidden" style={{ backgroundColor: "#1e293b" }}>
<div className="bg-gradient-to-l from-amber-500 to-amber-600 p-8 text-center">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", stiffness: 200 }}
className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center mx-auto mb-4"
>
<Shield className="w-8 h-8 text-white" />
</motion.div>
<motion.h1 initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="text-3xl font-bold text-white mb-2">
{t("changePassword.title")}
</motion.h1>
<motion.p initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: 0.1 }} className="text-amber-100">
{t("changePassword.subtitle")}
</motion.p>
</div>
<form onSubmit={handleSubmit} className="p-8 space-y-6 " style={{ backgroundColor: "#0f172a" }}>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.currentPasswordLabel")}</label>
<div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className="w-5 h-5 text-gray-400" />
</div>
<input
type={show.old ? "text" : "password"}
value={form.oldPassword}
onChange={handleChange("oldPassword")}
className={inputClass}
placeholder={t("changePassword.currentPasswordPlaceholder")}
required
/>
<button type="button" onClick={() => toggleShow("old")} className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600">
{show.old ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.newPasswordLabel")}</label>
<div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className="w-5 h-5 text-gray-400" />
</div>
<input
type={show.new ? "text" : "password"}
value={form.newPassword}
onChange={handleChange("newPassword")}
className={inputClass}
placeholder={t("changePassword.newPasswordPlaceholder")}
required
minLength={6}
/>
<button type="button" onClick={() => toggleShow("new")} className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600">
{show.new ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.confirmNewPasswordLabel")}</label>
<div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className="w-5 h-5 text-gray-400" />
</div>
<input
type={show.confirm ? "text" : "password"}
value={form.confirmPassword}
onChange={handleChange("confirmPassword")}
className={inputClass}
placeholder={t("changePassword.confirmPasswordPlaceholder")}
required
minLength={6}
/>
<button type="button" onClick={() => toggleShow("confirm")} className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600">
{show.confirm ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<motion.button
type="submit"
disabled={isLoading}
whileHover={{ scale: isLoading ? 1 : 1.02 }}
whileTap={{ scale: isLoading ? 1 : 0.98 }}
className="w-full bg-gradient-to-l from-amber-500 to-amber-600 text-white py-3 rounded-xl font-bold text-lg hover:from-amber-600 hover:to-amber-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-amber-500/25"
>
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span>{t("changePassword.saving")}</span>
</div>
) : (
t("changePassword.changePassword")
)}
</motion.button>
</form>
</div>
</motion.div>
</div>
);
}

View File

@ -0,0 +1,113 @@
"use client";
import { useEffect, useRef } from "react";
export default function InteractiveBackground() {
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
let animationFrameId;
let width = (canvas.width = window.innerWidth);
let height = (canvas.height = window.innerHeight);
// تتبع حركة الماوس
const mouse = { x: null, y: null, radius: 180 };
const handleMouseMove = (e) => {
mouse.x = e.clientX;
mouse.y = e.clientY;
};
const handleMouseLeave = () => {
mouse.x = null;
mouse.y = null;
};
const handleResize = () => {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
};
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseleave", handleMouseLeave);
window.addEventListener("resize", handleResize);
const particles = Array.from({ length: 70 }, () => ({
x: Math.random() * width,
y: Math.random() * height,
vx: (Math.random() - 0.5) * 0.8,
vy: (Math.random() - 0.5) * 0.8,
radius: Math.random() * 2 + 1,
}));
const draw = () => {
ctx.clearRect(0, 0, width, height);
particles.forEach((p, i) => {
p.x += p.vx;
p.y += p.vy;
if (p.x < 0 || p.x > width) p.vx *= -1;
if (p.y < 0 || p.y > height) p.vy *= -1;
if (mouse.x !== null && mouse.y !== null) {
const dx = mouse.x - p.x;
const dy = mouse.y - p.y;
const dist = Math.hypot(dx, dy);
if (dist < mouse.radius) {
const angle = Math.atan2(dy, dx);
const force = (mouse.radius - dist) / mouse.radius;
p.x -= Math.cos(angle) * force * 2;
p.y -= Math.sin(angle) * force * 2;
}
}
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fillStyle = "rgba(99, 102, 241, 0.7)";
ctx.fill();
for (let j = i + 1; j < particles.length; j++) {
const p2 = particles[j];
const dist = Math.hypot(p.x - p2.x, p.y - p2.y);
if (dist < 140) {
const opacity = 1 - dist / 140;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(p2.x, p2.y);
ctx.strokeStyle = `rgba(99, 102, 241, ${opacity * 0.25})`;
ctx.lineWidth = 1;
ctx.stroke();
}
}
});
animationFrameId = requestAnimationFrame(draw);
};
draw();
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseleave", handleMouseLeave);
window.removeEventListener("resize", handleResize);
cancelAnimationFrame(animationFrameId);
};
}, []);
return (
<div className="fixed inset-0 pointer-events-none z-0 bg-slate-950 overflow-hidden">
<div className="absolute top-1/4 -left-20 w-96 h-96 bg-indigo-600/20 rounded-full blur-3xl animate-pulse" />
<div className="absolute bottom-1/4 -right-20 w-96 h-96 bg-blue-600/20 rounded-full blur-3xl animate-pulse delay-1000" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] bg-purple-600/10 rounded-full blur-3xl" />
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
</div>
);
}

View File

@ -0,0 +1,70 @@
import { motion } from "framer-motion";
export const particles = Array.from({ length: 20 }, (_, i) => ({
id: i,
x: Math.random() * 100,
y: Math.random() * 100,
size: Math.random() * 3 + 1,
duration: Math.random() * 15 + 10,
delay: Math.random() * 5,
}));
export const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1, delayChildren: 0.2 },
},
};
export const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { type: "spring", stiffness: 100 },
},
};
export default function HeaderAnimation() {
return (
<>
<div className="absolute inset-0 overflow-hidden">
{particles.map((p) => (
<motion.div
key={p.id}
className="absolute rounded-full bg-amber-500/20"
style={{
left: `${p.x}%`,
top: `${p.y}%`,
width: p.size,
height: p.size,
}}
animate={{
y: [0, -20, 0],
x: [0, 10, -10, 0],
opacity: [0.2, 0.4, 0.2],
}}
transition={{
duration: p.duration,
repeat: Infinity,
delay: p.delay,
ease: "linear",
}}
/>
))}
</div>
{/* Glow orbs */}
<motion.div
className="absolute top-20 left-20 w-64 h-64 bg-amber-500/10 rounded-full blur-3xl"
animate={{ scale: [1, 1.2, 1], x: [0, 30, 0], y: [0, -20, 0] }}
transition={{ duration: 12, repeat: Infinity }}
/>
<motion.div
className="absolute bottom-20 right-20 w-80 h-80 bg-blue-500/10 rounded-full blur-3xl"
animate={{ scale: [1, 1.3, 1], x: [0, -30, 0], y: [0, 20, 0] }}
transition={{ duration: 15, repeat: Infinity }}
/>
</>
);
}

View File

@ -0,0 +1,26 @@
export const fadeInUp = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.5 },
};
export const staggerContainer = {
animate: { transition: { staggerChildren: 0.1 } },
};
import React from "react";
export const BackgroundElements = () => {
const circles = [
{ style: { top: "20%", right: "20%", width: "256px", height: "256px" }, className: "bg-amber-500/5" },
{ style: { bottom: "10%", left: "20%", width: "320px", height: "320px" }, className: "bg-blue-500/5" },
{ style: { bottom: "40%", left: "10%", width: "320px", height: "320px" }, className: "bg-blue-500/5" },
{ style: { bottom: "60%", left: "60%", width: "320px", height: "320px" }, className: "bg-blue-500/5" }
];
return (
<>
{circles.map((circle, index) => (
<div key={index} style={circle.style} className={`absolute rounded-full pointer-events-none ${circle.className}`} />
))}
</>
);
};

110
app/components/BottomNav.js Normal file
View File

@ -0,0 +1,110 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Home, Building2, Building, CalendarCheck, CalendarDays, ClipboardList, CreditCard, BookOpenCheck } from "lucide-react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
export default function BottomNav({ isOwner, isOwnerOrAgent }) {
const { t } = useTranslation();
const pathname = usePathname();
const [isMounted, setIsMounted] = useState(false);
const bookingsHref = isOwner ? "/owner/reservations" : "/reservations";
useEffect(() => {
setIsMounted(true);
}, []);
const items = [
{ href: "/", label: t("home"), icon: Home },
{ href: "/properties", label: t("ourProperties"), icon: Building2 },
...(isOwnerOrAgent
? [
{ href: "/owner/properties", label: t("myProperties"), icon: Building },
{ href: "/owner/bookings", label: t("Myseizedproperties"), icon: CalendarCheck },
{ href: "/reservations", label: t("Mybookings"), icon: CalendarDays },
]
: [{ href: "/booked-properties", label: t("Mybookings"), icon: CalendarDays }]),
{ href: bookingsHref, label: t("Orders"), icon: ClipboardList },
{ href: "/payments", label: t("payments"), icon: CreditCard },
...(isOwnerOrAgent ? [{ href: "/owner/account-book", label: t("accountBook"), icon: BookOpenCheck }] : []),
];
const isActive = (href) => {
if (href === "/") return pathname === "/";
return pathname.startsWith(href);
};
return (
<div className="fixed bottom-0 left-0 right-0 z-50 flex justify-center px-5 pb-4 pt-4 pointer-events-none">
<nav
className="
w-fit max-w-full
rounded-3xl
bg-gray-50
px-5 py-3 sm:px-4
shadow-[0_4px_24px_rgba(251,146,60,0.25)]
pointer-events-auto
"
>
<div
className="flex items-center sm:justify-center gap-0.5 sm:gap-1 overflow-x-auto max-w-full
[&::-webkit-scrollbar]:h-1.5
[&::-webkit-scrollbar-track]:bg-gray-100
[&::-webkit-scrollbar-thumb]:bg-gray-700
[&::-webkit-scrollbar-thumb]:rounded-full hover:[&::-webkit-scrollbar-thumb]:bg-gray-700"
>
{items.map((it) => {
const Icon = it.icon;
const active = isActive(it.href);
return (
<Link
key={it.href}
href={it.href}
style={{ padding: "3px", width: "115px" }}
className={`
flex
flex-col
items-center
justify-center
rounded-xl
transition-all
duration-200
ease-out
${active ? "bg-white shadow-sm text-amber-600" : "text-gray-700 hover:bg-white/80 hover:text-amber-600 hover:shadow-md"}
`}
>
<div className="relative">
<Icon className="h-5 w-5 sm:h-6 sm:w-6" />
{it.badge > 0 && (
<div
className="
absolute -right-2 -top-2
flex h-6 w-7 sm:h-5 sm:w-5
items-center justify-center
rounded-full
bg-red-500
text-[9px] sm:text-xs
font-bold
text-white
"
>
{it.badge > 9 ? "9+" : it.badge}
</div>
)}
</div>
<div className="mt-0.5 text-[10px] sm:mt-1 text-center sm:text-xs">{it.label}</div>
</Link>
);
})}
</div>
</nav>
</div>
);
}

View File

@ -3,11 +3,16 @@
import { useState } from 'react'; import { useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import Link from 'next/link'; import Link from 'next/link';
import { Heart, Bell, CreditCard } from 'lucide-react'; import { Heart, Bell, CreditCard, Settings } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useFavorites } from '@/app/contexts/FavoritesContext'; import { useFavorites } from '@/app/contexts/FavoritesContext';
import { useNotifications } from '@/app/contexts/NotificationsContext';
import AuthService from '@/app/services/AuthService';
export default function FloatingSidebar({ isRTL }) { export default function FloatingSidebar({ isRTL }) {
const { t } = useTranslation();
const { favorites } = useFavorites(); const { favorites } = useFavorites();
const { unreadCount } = useNotifications();
const [tooltip, setTooltip] = useState(null); const [tooltip, setTooltip] = useState(null);
let timeoutId = null; let timeoutId = null;
@ -22,15 +27,16 @@ export default function FloatingSidebar({ isRTL }) {
setTooltip(null); setTooltip(null);
}; };
const side = isRTL ? 'left' : 'right'; if (!AuthService.isAuthenticated()) return null;
const positionStyle = {
[side]: 0, const positionStyle = {
left: '16px',
top: '50%', top: '50%',
transform: 'translateY(-50%)', transform: 'translateY(-50%)',
}; };
const cardVariants = { const cardVariants = {
initial: { opacity: 0, x: isRTL ? -20 : 20 }, initial: { opacity: 0, x: 20 },
animate: { opacity: 1, x: 0, transition: { duration: 0.4, ease: 'easeOut' } }, animate: { opacity: 1, x: 0, transition: { duration: 0.4, ease: 'easeOut' } },
}; };
@ -40,15 +46,24 @@ export default function FloatingSidebar({ isRTL }) {
tap: { scale: 0.95 }, tap: { scale: 0.95 },
}; };
const renderTooltip = (id, label) => {
if (tooltip !== id) return null;
return (
<div className="absolute left-full mr-3 top-1/2 -translate-y-1/2 px-3 py-2 bg-gray-800 text-white text-sm rounded-lg whitespace-nowrap z-20 shadow-lg">
{label}
</div>
);
};
return ( return (
<motion.div <motion.div
className="fixed z-50" className="fixed z-40 pointer-events-none"
style={positionStyle} style={positionStyle}
variants={cardVariants} variants={cardVariants}
initial="initial" initial="initial"
animate="animate" animate="animate"
> >
<div className="bg-white/80 backdrop-blur-md rounded-2xl shadow-lg border border-gray-200/50 py-3 px-2 flex flex-col gap-3 transition-all duration-300 hover:shadow-xl hover:bg-white/90"> <div className="bg-white/90 backdrop-blur-md rounded-2xl shadow-lg border border-gray-200/60 py-4 px-3 flex flex-col gap-4 transition-all duration-300 hover:shadow-xl hover:bg-white/95 max-h-[75vh] overflow-y-auto pointer-events-auto">
<motion.div <motion.div
className="relative group" className="relative group"
variants={buttonVariants} variants={buttonVariants}
@ -60,35 +75,22 @@ export default function FloatingSidebar({ isRTL }) {
> >
<Link <Link
href="/favorites" href="/favorites"
className="flex items-center justify-center w-12 h-12 rounded-xl transition-colors" className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
> >
<div className="relative"> <div className="relative">
<Heart className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" /> <Heart className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
{favorites.length > 0 && ( {favorites.length > 0 && (
<motion.span <motion.span
initial={{ scale: 0 }} initial={{ scale: 0 }}
animate={{ scale: 1 }} animate={{ scale: 1 }}
className="absolute -right-1 -top-1 w-5 h-5 bg-gradient-to-r from-amber-500 to-amber-600 text-white text-xs rounded-full flex items-center justify-center shadow-md" className="absolute -right-1 -top-1 w-6 h-6 bg-linear-to-r from-amber-500 to-amber-600 text-white text-xs rounded-full flex items-center justify-center shadow-md"
> >
{favorites.length} {favorites.length}
</motion.span> </motion.span>
)} )}
</div> </div>
</Link> </Link>
{tooltip === 'favorites' && ( {renderTooltip('favorites', t('favorites'))}
<div
className={`absolute ${isRTL ? 'right-full mr-2' : 'left-full ml-2'} top-1/2 -translate-y-1/2 px-2 py-1 bg-gray-800 text-white text-xs rounded-lg whitespace-nowrap z-20 shadow-lg flex items-center`}
>
<span className="relative">
المفضلة
<span
className={`absolute ${isRTL ? 'right-full -mr-1' : 'left-full -ml-1'} top-1/2 -translate-y-1/2 w-0 h-0 border-t-4 border-b-4 border-transparent ${
isRTL ? 'border-r-4 border-r-gray-800' : 'border-l-4 border-l-gray-800'
}`}
></span>
</span>
</div>
)}
</motion.div> </motion.div>
<motion.div <motion.div
className="relative group" className="relative group"
@ -101,33 +103,22 @@ export default function FloatingSidebar({ isRTL }) {
> >
<Link <Link
href="/notifications" href="/notifications"
className="flex items-center justify-center w-12 h-12 rounded-xl transition-colors" className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
> >
<div className="relative"> <div className="relative">
<Bell className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" /> <Bell className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
<motion.span {unreadCount > 0 && (
initial={{ scale: 0 }} <motion.span
animate={{ scale: 1 }} initial={{ scale: 0 }}
className="absolute -right-1 -top-1 w-5 h-5 bg-gradient-to-r from-red-500 to-red-600 text-white text-xs rounded-full flex items-center justify-center shadow-md" animate={{ scale: 1 }}
> className="absolute -right-1 -top-1 w-6 h-6 bg-linear-to-r from-red-500 to-red-600 text-white text-xs rounded-full flex items-center justify-center shadow-md"
3 >
</motion.span> {unreadCount}
</motion.span>
)}
</div> </div>
</Link> </Link>
{tooltip === 'notifications' && ( {renderTooltip('notifications', t('notifications'))}
<div
className={`absolute ${isRTL ? 'right-full mr-2' : 'left-full ml-2'} top-1/2 -translate-y-1/2 px-2 py-1 bg-gray-800 text-white text-xs rounded-lg whitespace-nowrap z-20 shadow-lg flex items-center`}
>
<span className="relative">
الإشعارات
<span
className={`absolute ${isRTL ? 'right-full -mr-1' : 'left-full -ml-1'} top-1/2 -translate-y-1/2 w-0 h-0 border-t-4 border-b-4 border-transparent ${
isRTL ? 'border-r-4 border-r-gray-800' : 'border-l-4 border-l-gray-800'
}`}
></span>
</span>
</div>
)}
</motion.div> </motion.div>
<motion.div <motion.div
className="relative group" className="relative group"
@ -140,24 +131,28 @@ export default function FloatingSidebar({ isRTL }) {
> >
<Link <Link
href="/payments" href="/payments"
className="flex items-center justify-center w-12 h-12 rounded-xl transition-colors" className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
> >
<CreditCard className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" /> <CreditCard className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
</Link> </Link>
{tooltip === 'payments' && ( {renderTooltip('payments', t('payments'))}
<div </motion.div>
className={`absolute ${isRTL ? 'right-full mr-2' : 'left-full ml-2'} top-1/2 -translate-y-1/2 px-2 py-1 bg-gray-800 text-white text-xs rounded-lg whitespace-nowrap z-20 shadow-lg flex items-center`} <motion.div
> className="relative group"
<span className="relative"> variants={buttonVariants}
المدفوعات initial="rest"
<span whileHover="hover"
className={`absolute ${isRTL ? 'right-full -mr-1' : 'left-full -ml-1'} top-1/2 -translate-y-1/2 w-0 h-0 border-t-4 border-b-4 border-transparent ${ whileTap="tap"
isRTL ? 'border-r-4 border-r-gray-800' : 'border-l-4 border-l-gray-800' onMouseEnter={() => showTooltip('settings')}
}`} onMouseLeave={hideTooltip}
></span> >
</span> <Link
</div> href="/settings"
)} className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
>
<Settings className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
</Link>
{renderTooltip('settings', t('settings'))}
</motion.div> </motion.div>
</div> </div>
</motion.div> </motion.div>

View File

@ -0,0 +1,17 @@
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import { fadeInUp } from "./Animation/OwnerPageAnimation";
import Link from "next/link";
export default function HavAccount() {
const { t } = useTranslation();
return (
<>
<motion.p variants={fadeInUp} className="text-center text-gray-400 mt-4">
{t("register.haveAccount")}
<Link href="/login" className="text-blue-400 hover:text-blue-300 font-medium transition-colors">
{t("register.loginLink")}
</Link>
</motion.p>
</>
);
}

View File

@ -0,0 +1,17 @@
import { motion } from 'framer-motion';
import { itemVariants } from '../Animation/HeaderLoginAnimation';
import Link from 'next/link';
import { useTranslation } from 'react-i18next';
export default function CreateNewAccount() {
const {t}=useTranslation()
return (
<>
<motion.p variants={itemVariants} className="text-center text-gray-400 mt-6">
{t("noAccount")}{" "}
<Link href="/auth/choose-role" className="text-amber-400 hover:text-amber-300 font-medium transition-colors">
{t("createNewAccount")}
</Link>
</motion.p>
</>
);
}

View File

@ -0,0 +1,45 @@
import { motion } from "framer-motion";
import { itemVariants } from "../Animation/HeaderLoginAnimation";
import { ArrowLeft, KeyRound,Home } from "lucide-react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
//Back Link
export function BackLink() {
const { t } = useTranslation();
return (
<>
<motion.div variants={itemVariants} className="absolute -top-16 left-0">
<Link href="/" className="group flex items-center gap-2 text-gray-400 hover:text-white transition-colors">
<motion.div whileHover={{ x: -5 }} transition={{ type: "spring", stiffness: 400 }}>
<ArrowLeft className="w-4 h-4" />
</motion.div>
<span>{t("backToHome")}</span>
</Link>
</motion.div>
</>
);
}
export function HeaderForm({ step }) {
const { t } = useTranslation();
return (
<>
<div className="bg-gradient-to-r from-amber-700 to-amber-400 p-8 text-center relative overflow-hidden">
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.2, type: "spring" }} className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full" />
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.3, type: "spring" }} className="absolute -bottom-10 -left-10 w-40 h-40 bg-white/10 rounded-full" />
<motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: 0.2 }} className="relative z-10">
<motion.div
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ duration: 2, repeat: Infinity }}
className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm"
>
{step === "otp" ? <KeyRound className="w-10 h-10 text-white" /> : <Home className="w-10 h-10 text-white" />}
</motion.div>
<h1 className="text-3xl font-bold text-white mb-2">SweetHome</h1>
<p className="text-amber-100">{step === "otp" ? t("enterOTP") : t("welcomeBack")}</p>
</motion.div>
</div>
</>
);
}

View File

@ -0,0 +1,127 @@
"use client";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion";
import toast, { Toaster } from "react-hot-toast";
import { useRouter } from "next/navigation";
import AuthService from "../../services/AuthService";
import InteractiveBackground from "../Animation/Background"
import { BackLink, HeaderForm } from "./HeaderLogin";
import HeaderAnimation, { containerVariants, itemVariants } from "../Animation/HeaderLoginAnimation";
import TabsLogin from "./Tabs";
import LoginForm from "./LoginForm";
import CreateNewAccount from "./CreateNewAccount";
import PolicyAndPrivecy from "./PolicyAndPrivecy";
import OtpComponent from "./OtpComponent";
export default function Login() {
const router = useRouter();
const { t } = useTranslation();
const [step, setStep] = useState("login");
const [loginMethod, setLoginMethod] = useState("email");
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [formData, setFormData] = useState({
credential: "", //email or phoneNumber
password: "",
rememberMe: false,
});
const [errors, setErrors] = useState({});
const validateForm = () => {
const newErrors = {};
if (!formData.credential) {
newErrors.credential = loginMethod === "email" ? t("emailRequired") : t("phoneRequired");
}
if (!formData.password) {
newErrors.password = t("passwordRequired");
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleLogin = async (e) => {
e.preventDefault();
if (!validateForm()) return;
setIsLoading(true);
setErrors({});
try {
const response = await AuthService.login({
loginMethod,
credential: formData.credential,
password: formData.password,
});
switch (response.type) {
case "SUCCESS":
toast.success(t("loginSuccess"));
router.push("/");
break;
case "OTP_REQUIRED":
toast(t("otpRequired"));
setStep("otp");
break;
default:
toast.error("Login failed the email or password is wrong");
}
} catch (err) {
console.error("[Login] Error:", err);
toast.error(err.message || t("connectionError"), {
style: { background: "#fee2e2", color: "#991b1b" },
});
} finally {
setIsLoading(false);
}
};
// Auto-detect login method from input
const handleCredentialChange = (value) => {
setFormData({ ...formData, credential: value });
if (errors.credential) setErrors({ ...errors, credential: null });
};
const handelTabs = (type) => {
setLoginMethod(type);
setFormData({ ...formData, credential: "" });
};
return (
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
<Toaster position="top-center" reverseOrder={false} />
<InteractiveBackground />
{/* Particles */}
<HeaderAnimation />
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="relative w-full max-w-md z-10">
{/* Back link */}
<BackLink />
<motion.div variants={itemVariants} className="bg-white/10 backdrop-blur-2xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden">
{/* Header */}
<HeaderForm step={step} />
<div className="p-8">
<AnimatePresence mode="wait">
{step === "login" ? (
<motion.form key="login" initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: 20 }} onSubmit={handleLogin} className="space-y-6">
{/* Login method tabs */}
<TabsLogin loginMethod={loginMethod} handelTabs={handelTabs} />
{/* Form Login */}
<LoginForm
loginMethod={loginMethod}
errors={errors}
formData={formData}
isLoading={isLoading}
isSuccess={isSuccess}
setFormData={setFormData}
handleCredentialChange={handleCredentialChange}
/>
</motion.form>
) : (
// /* OTP Verification Step */
<OtpComponent isLoading={isLoading} setIsLoading={setIsLoading} isSuccess={isSuccess} setIsSuccess={setIsSuccess} loginMethod={loginMethod} formData={formData} setStep={setStep} />
)}
</AnimatePresence>
<CreateNewAccount />
</div>
</motion.div>
<PolicyAndPrivecy />
</motion.div>
</div>
);
}

View File

@ -0,0 +1,141 @@
import { EyeOff, Mail, Phone, Eye, LogIn, Lock, Loader2, CheckCircle } from "lucide-react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion";
import Link from "next/link";
import { useState } from "react";
export default function LoginForm({ loginMethod, errors, formData, isLoading, isSuccess, setFormData, handleCredentialChange }) {
const { t } = useTranslation();
const [showPassword, setShowPassword] = useState(false);
return (
<>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">{loginMethod === "email" ? t("emailLabel") : t("phoneLabel")}</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
{loginMethod === "email" ? (
<Mail className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
) : (
<Phone className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
)}
</div>
{loginMethod === "email" ? (
<>
<input
type="email"
// type={loginMethod === 'email' ? 'email' : 'tel'}
value={formData.credential}
onChange={(e) => handleCredentialChange(e.target.value)}
className={`w-full pr-12 pl-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${
errors.credential ? "border-red-500" : "border-gray-700"
}`}
placeholder={t("emailPlaceholder")}
dir="ltr"
/>
</>
) : (
<>
<input
type="text"
value={formData.credential}
onChange={(e) => handleCredentialChange(e.target.value)}
className={`w-full pr-12 pl-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${
errors.credential ? "border-red-500" : "border-gray-700"
}`}
placeholder={t("phonePlaceholder")}
dir="ltr"
/>
</>
)}
</div>
{errors.credential && (
<motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1">
{errors.credential}
</motion.p>
)}
</div>
{/* Password */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">{t("password")}</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 transition-colors ${errors.password ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
</div>
<input
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => {
setFormData({
...formData,
password: e.target.value,
});
if (errors.password) setErrors({ ...errors, password: null });
}}
className={`w-full pr-12 pl-12 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${
errors.password ? "border-red-500" : "border-gray-700"
}`}
placeholder={t("passwordPlaceholder")}
/>
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
{showPassword ? <EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300 transition-colors" /> : <Eye className="w-5 h-5 text-gray-400 hover:text-gray-300 transition-colors" />}
</button>
</div>
{errors.password && (
<motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1">
{errors.password}
</motion.p>
)}
</div>
{/* Remember + Forgot */}
<div className="flex items-center justify-between">
<label className="flex items-center gap-2 cursor-pointer group">
<input
type="checkbox"
checked={formData.rememberMe}
onChange={(e) =>
setFormData({
...formData,
rememberMe: e.target.checked,
})
}
className="w-4 h-4 rounded border-gray-600 bg-white/5 text-amber-500 focus:ring-amber-500 focus:ring-offset-0"
/>
<span className="text-sm text-gray-400 group-hover:text-white transition-colors">{t("rememberMe")}</span>
</label>
<Link href="/forgot-password" className="text-sm text-amber-400 hover:text-amber-300 transition-colors">
{t("forgotPassword")}
</Link>
</div>
{/* Submit */}
<motion.button
type="submit"
disabled={isLoading || isSuccess}
className="relative w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-4 rounded-xl font-bold text-lg overflow-hidden group disabled:opacity-50 disabled:cursor-not-allowed"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<span className="relative z-10 flex items-center justify-center gap-2">
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
{t("loggingIn")}{" "}
</>
) : isSuccess ? (
<>
<CheckCircle className="w-5 h-5" /> {t("success")}{" "}
</>
) : (
<>
{" "}
<LogIn className="w-5 h-5" />
{t("login")}
</>
)}
</span>
</motion.button>
</>
);
}

View File

@ -0,0 +1,143 @@
"use client";
import { CheckCircle, KeyRound, Loader2, Shield } from "lucide-react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion";
import { sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone, getOwnerByUserId, getCustomerByUserId } from "@/app/utils/api";
import AuthService from "@/app/services/AuthService";
import { useState } from "react";
import { useRouter } from "next/navigation";
import toast from "react-hot-toast";
export default function OtpComponent({ isLoading, setIsLoading, isSuccess, setIsSuccess, loginMethod, formData, setStep }) {
const { t } = useTranslation();
const router = useRouter();
const [otpCode, setOtpCode] = useState("");
const [otpError, setOtpError] = useState("");
const handleVerifyOTP = async (e) => {
e.preventDefault();
if (!otpCode || otpCode.length < 4) {
setOtpError(t("otpRequired"));
return;
}
setIsLoading(true);
setOtpError("");
try {
const verifyFn = loginMethod === "email" ? verifyEmail : verifyPhone;
const result = await verifyFn(otpCode);
if (result?.ok) {
try {
await AuthService.cacheCurrentUser(getOwnerByUserId, getCustomerByUserId);
} catch (err) {
console.warn("Failed to cache user", err);
}
}
setIsSuccess(true);
toast.success(t("verifySuccess"));
setTimeout(() => {
router.push("/");
}, 1500);
} catch (err) {
console.error("[OTP] Error:", err);
setOtpError(err.message || t("verifyError"));
} finally {
setIsLoading(false);
}
};
const resendOTP = async () => {
try {
if (loginMethod === "email") {
await sendEmailOTP();
} else {
await sendPhoneOTP();
}
toast.success(t("resendSuccess"), {
style: { background: "#dcfce7", color: "#166534" },
});
} catch (err) {
console.error("[OTP] Resend failed:", err);
toast.error(t("resendFailed"));
}
};
return (
<motion.form key="otp" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} onSubmit={handleVerifyOTP} className="space-y-6">
<div className="text-center mb-4">
<Shield className="w-12 h-12 text-amber-500 mx-auto mb-3" />
<p className="text-amber-300 text-sm">
{t("sendOTPTo")}{" "}
<span className="text-amber-50 font-medium" dir="ltr">
{formData?.credential}
</span>
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">{t("otpLabel")}</label>
<input
type="text"
value={otpCode}
onChange={(e) => {
setOtpCode(e.target.value);
if (otpError) setOtpError("");
}}
className={`w-full px-4 py-4 bg-gray-800 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white text-center text-2xl tracking-[0.5em] placeholder-gray-500 transition-all ${
otpError ? "border-red-500" : "border-gray-700"
}`}
placeholder="______"
maxLength={6}
dir="ltr"
/>
{otpError && (
<motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1 text-center">
{otpError}
</motion.p>
)}
</div>
<motion.button
type="submit"
disabled={isLoading || isSuccess}
className="relative w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-4 rounded-xl font-bold text-lg disabled:opacity-50 disabled:cursor-not-allowed"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<span className="flex items-center justify-center gap-2">
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
{t("verifying")}
</>
) : isSuccess ? (
<>
<CheckCircle className="w-5 h-5" />
{t("success")}
</>
) : (
<>
<KeyRound className="w-5 h-5" />
{t("verify")}
</>
)}
</span>
</motion.button>
<div className="flex items-center justify-between text-sm">
<button
type="button"
onClick={() => {
setStep("login");
setOtpCode("");
setOtpError("");
}}
className="text-gray-400 hover:text-white transition-colors"
>
{t("back")}
</button>
<button type="button" onClick={resendOTP} className="text-amber-400 hover:text-amber-300 transition-colors">
{t("resendCode")}
</button>
</div>
</motion.form>
);
}

View File

@ -0,0 +1,21 @@
import { motion } from "framer-motion";
import { itemVariants } from "../Animation/HeaderLoginAnimation";
import { useTranslation } from "react-i18next";
import Link from "next/link";
export default function PolicyAndPrivecy() {
const { t } = useTranslation();
return (
<>
<motion.p variants={itemVariants} className="text-center text-gray-500 text-xs mt-4">
{t("agreeTerms")}{" "}
<Link href="/terms" className="text-amber-400 hover:text-amber-300 transition-colors">
{t("termsOfUse")}
</Link>{" "}
{t("and")}{" "}
<Link href="/privacy" className="text-amber-400 hover:text-amber-300 transition-colors">
{t("privacyPolicy")}
</Link>
</motion.p>
</>
);
}

View File

@ -0,0 +1,36 @@
import { Mail, Phone } from "lucide-react";
import { useTranslation } from "react-i18next";
export default function TabsLogin({ loginMethod, handelTabs }) {
const { t } = useTranslation();
return (
<>
<div className="flex gap-2 bg-white/5 p-1 rounded-xl">
<button
type="button"
onClick={() => {
handelTabs("email");
}}
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
loginMethod === "email" ? "bg-amber-500 text-white shadow-lg" : "text-gray-400 hover:text-white"
}`}
>
<Mail className="w-4 h-4" />
{t("emailMethod")}
</button>
<button
type="button"
onClick={() => {
handelTabs("phone");
}}
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
loginMethod === "phone" ? "bg-amber-500 text-white shadow-lg" : "text-gray-400 hover:text-white"
}`}
>
<Phone className="w-4 h-4" />
{t("phoneMethod")}
</button>
</div>
</>
);
}

View File

@ -3,7 +3,7 @@ import { usePathname } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { LogIn, UserPlus } from 'lucide-react'; import { LogIn, UserPlus } from 'lucide-react';
export function NavLink({ href, children }) { export function NavLink({ href, children, light }) {
const pathname = usePathname(); const pathname = usePathname();
const isActive = pathname === href || pathname.startsWith(href + '/'); const isActive = pathname === href || pathname.startsWith(href + '/');
return ( return (
@ -11,19 +11,19 @@ export function NavLink({ href, children }) {
href={href} href={href}
className={`px-5 py-3 rounded-lg font-semibold transition-all duration-300 text-lg relative group ${ className={`px-5 py-3 rounded-lg font-semibold transition-all duration-300 text-lg relative group ${
isActive isActive
? 'text-amber-600 bg-amber-50' ? light ? 'text-amber-300 bg-white/10' : 'text-amber-600 bg-amber-50'
: 'text-gray-700 hover:text-amber-600 hover:bg-gray-100' : light ? 'text-white/90 hover:text-amber-300 hover:bg-white/10' : 'text-gray-700 hover:text-amber-600 hover:bg-gray-100'
}`} }`}
> >
{children} {children}
<span className={`absolute bottom-0 left-1/2 transform -translate-x-1/2 h-0.5 transition-all duration-300 ${ <span className={`absolute bottom-0 left-1/2 transform -translate-x-1/2 h-0.5 transition-all duration-300 ${
isActive ? 'w-4/5 bg-amber-600' : 'w-0 bg-amber-600 group-hover:w-4/5' isActive ? 'w-4/5 bg-amber-400' : 'w-0 bg-amber-400 group-hover:w-4/5'
}`}></span> }`}></span>
</Link> </Link>
); );
} }
export function MobileNavLink({ href, children, onClick }) { export function MobileNavLink({ href, children, onClick, light }) {
const pathname = usePathname(); const pathname = usePathname();
const isActive = pathname === href || pathname.startsWith(href + '/'); const isActive = pathname === href || pathname.startsWith(href + '/');
return ( return (
@ -32,8 +32,8 @@ export function MobileNavLink({ href, children, onClick }) {
onClick={onClick} onClick={onClick}
className={`block px-3 py-2 rounded-md text-base font-medium transition-colors ${ className={`block px-3 py-2 rounded-md text-base font-medium transition-colors ${
isActive isActive
? 'text-amber-600 bg-amber-50' ? light ? 'text-amber-300 bg-white/10' : 'text-amber-600 bg-amber-50'
: 'text-gray-700 hover:text-amber-600 hover:bg-gray-50' : light ? 'text-white/80 hover:text-amber-300 hover:bg-white/10' : 'text-gray-700 hover:text-amber-600 hover:bg-gray-50'
}`} }`}
> >
{children} {children}

View File

@ -1,10 +1,11 @@
"use client"; "use client";
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
import { initializeApp, getApps } from "firebase/app"; import { initializeApp, getApps } from "firebase/app";
import { getMessaging, getToken, onMessage } from "firebase/messaging"; import { getMessaging, getToken, onMessage } from "firebase/messaging";
import AuthService from "../services/AuthService"; import AuthService from "../services/AuthService";
import { setFCMToken } from "../utils/api";
//فاير بيز
const firebaseConfig = { const firebaseConfig = {
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY", apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
authDomain: "sweet-home-b2766.firebaseapp.com", authDomain: "sweet-home-b2766.firebaseapp.com",
@ -15,6 +16,7 @@ const firebaseConfig = {
measurementId: "G-M2V95NBJLX", measurementId: "G-M2V95NBJLX",
}; };
//عم نعمل تهيئة للاعدادات
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]; const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
export default function NotificationHandler() { export default function NotificationHandler() {
@ -23,22 +25,40 @@ export default function NotificationHandler() {
const initialized = useRef(false); const initialized = useRef(false);
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { function checkAuth() {
if (initialized.current) return; if (initialized.current) return;
initialized.current = true;
if (!AuthService.getToken()) return; if (!AuthService.getToken()) return;
initialized.current = true;
if ("Notification" in window) { if ("Notification" in window) {
//عم نشوف اذا حط سماح او رفض
if (Notification.permission === "default") { if (Notification.permission === "default") {
setShowPrompt(true); setShowPrompt(true);
} else if (Notification.permission === "granted") { } else if (Notification.permission === "granted") {
setupFCM(); setupFCM();
} }
} }
}
// Check immediately
checkAuth();
// Also check when auth token changes (login via client-side navigation)
const interval = setInterval(() => {
if (!initialized.current && AuthService.getToken()) {
checkAuth();
}
}, 1000); }, 1000);
return () => clearTimeout(timer); // Check on route change (visibility)
const onVisibility = () => {
if (document.visibilityState === "visible") checkAuth();
};
document.addEventListener("visibilitychange", onVisibility);
return () => {
clearInterval(interval);
document.removeEventListener("visibilitychange", onVisibility);
};
}, []); }, []);
async function setupFCM() { async function setupFCM() {
@ -52,21 +72,7 @@ export default function NotificationHandler() {
}); });
if (fcmToken) { if (fcmToken) {
console.log("[FCM] Token:", fcmToken.substring(0, 20) + "..."); await setFCMToken(fcmToken, 2);
const authToken = AuthService.getToken();
if (authToken) {
const apiBase = "https://45.93.137.91.nip.io/api";
await fetch(`${apiBase}/User/SetFCMToken`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify({ token: fcmToken, deviceType: 2 }),
});
console.log("[FCM] Token sent to backend");
}
} }
onMessage(messaging, (payload) => { onMessage(messaging, (payload) => {
@ -85,7 +91,6 @@ export default function NotificationHandler() {
// This MUST be synchronous from a user gesture // This MUST be synchronous from a user gesture
const permission = await Notification.requestPermission(); const permission = await Notification.requestPermission();
console.log("[FCM] Permission result:", permission);
if (permission === "granted") { if (permission === "granted") {
await setupFCM(); await setupFCM();

View File

@ -0,0 +1,25 @@
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
export default function ButtomStepOne({ type }) {
const router = useRouter();
const { t } = useTranslation();
return (
<>
<>
<button
type="button"
onClick={() => router.push("/auth/choose-role")}
className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors"
>
{t("register.cancel")}
</button>
<button
type="submit"
className={`flex-1 bg-gradient-to-r ${type === "owner" ? " from-amber-500 to-amber-600" : " from-blue-500 to-blue-600"} text-white py-3 px-4 rounded-xl font-medium ${type === "onwer" ? "hover:from-amber-600 hover:to-amber-700" : "hover:from-blue-600 hover:to-blue-700"} transition-all`}
>
{t("register.next")}
</button>
</>
</>
);
}

View File

@ -0,0 +1,27 @@
import { Loader2 } from "lucide-react";
import { useTranslation } from "react-i18next";
export default function ButtomStepTwo({ setStep, isLoading, formData, type }) {
const { t } = useTranslation();
return (
<>
<button type="button" onClick={() => setStep(1)} className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">
{t("register.previous")}
</button>
<button
type="submit"
disabled={isLoading || !formData.agreeTerms}
className={`flex-1 bg-gradient-to-r${type === "owner" ? " from-amber-500 to-amber-600" : " from-blue-500 to-blue-600"} text-amber-50 py-3 px-4 rounded-xl font-medium${type === "onwer" ? "hover:from-amber-600 hover:to-amber-700" : "hover:from-blue-600 hover:to-blue-700"} transition-all disabled:opacity-50 disabled:cursor-not-allowed`}
>
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<Loader2 className="w-5 h-5 animate-spin" />
<span>{t("register.registering")}</span>
</div>
) : (
t("register.createAccount")
)}
</button>
</>
);
}

View File

@ -0,0 +1,6 @@
export default function FormOwner(){
return(<>
</>)
}

View File

@ -0,0 +1,44 @@
"use client";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion";
import { Building, Home } from "lucide-react";
const data = [
{
colors: "from-amber-500 to-amber-600",
titleKey: "register.owner.step1Title",
title2Key: "register.owner.step1bTitle",
descKey: "register.owner.step1Desc",
desc2Key: "register.owner.step1bDesc",
icon: <Building className="w-10 h-10 text-white" />,
},
{
colors: "from-blue-500 to-blue-600",
titleKey: "register.tenant.step1Title",
title2Key: "register.tenant.step2Title",
descKey: "register.tenant.step1Desc",
desc2Key: "register.tenant.step2Desc",
icon: <Home className="w-10 h-10 text-white" />,
},
];
export default function HeaderOfForm({ step = 1, num = 0 }) {
const { t } = useTranslation();
const currentData = data[num] || data[0];
return (
<div className={`bg-gradient-to-r ${currentData.colors} p-8 text-center relative overflow-hidden`}>
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.2, type: "spring" }} className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full" />
<motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="relative z-10">
<motion.div
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ duration: 2, repeat: Infinity }}
className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm"
>
{data[num].icon}
</motion.div>
<h1 className="text-3xl font-bold text-white mb-2">{t(step === 1 ? t(currentData.titleKey) : t(currentData.title2Key))}</h1>
<p className="text-amber-100">{t(step === 1 ? t(currentData.descKey) : t(currentData.desc2Key))}</p>
</motion.div>
</div>
);
}

View File

@ -0,0 +1,28 @@
import Link from "next/link";
import { motion } from "framer-motion";
import { ArrowLeft } from "lucide-react";
import { useTranslation } from "react-i18next";
export default function HeaderOfSteps({ step, colors, numberStep }) {
const { t } = useTranslation();
return (
<>
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<Link href="/auth/choose-role" className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors group">
<motion.div whileHover={{ x: -5 }}>
<ArrowLeft className="w-4 h-4" />
</motion.div>
<span>{t("forgotPassword.backToLogin")}</span>
</Link>
<span className="text-sm text-gray-200">{t("register.stepOf2", { step: step })}</span>
</div>
<div className="flex gap-2">
{numberStep.map((s) => (
<motion.div key={s} className={`h-2 flex-1 rounded-full ${step >= s ? colors : "bg-gray-800"}`} animate={{ scaleX: step >= s ? 1 : 0.5 }} />
))}
</div>
</div>
</>
);
}

View File

@ -0,0 +1,104 @@
import { KeyRound, Loader2, Shield } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion";
import toast, { Toaster } from "react-hot-toast";
import { sendEmailOTP, verifyEmail } from "@/app/utils/api";
import AuthService from "@/app/services/AuthService";
import { useRouter } from "next/navigation";
export default function OtpDialog({ formData, setShowOtpModal, type }) {
const { t } = useTranslation();
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [otpCode, setOtpCode] = useState("");
const handleVerifyOTP = async () => {
if (!otpCode || otpCode.length < 4) {
toast.error(t("register.otpRequired"));
return;
}
setIsLoading(true);
try {
const res = await verifyEmail(otpCode);
if (res.status === 200) {
AuthService.deleteToken();
toast.success(res.message || t("register.emailVerified"));
setShowOtpModal(false);
setTimeout(() => router.push("/login"), 1500);
} else {
toast.error(res.message || res.data?.message || t("accountVerification.otpInvalid"));
}
} catch (err) {
toast.error(err.message || t("register.verificationError"));
} finally {
setIsLoading(false);
}
};
const handleResendOTP = async () => {
setIsLoading(true);
try {
await sendEmailOTP();
toast.success(t("register.newOtpSent"));
} catch (err) {
toast.error(t("register.resendOtpFailed"));
} finally {
setIsLoading(false);
}
};
return (
<>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<motion.div initial={{ scale: 0.9, y: 20 }} animate={{ scale: 1, y: 0 }} exit={{ scale: 0.9, y: 20 }} className="bg-gray-900 border border-white/10 rounded-2xl w-full max-w-md p-6 shadow-2xl">
<div className="text-center mb-6">
<div className="w-16 h-16 bg-amber-500/20 rounded-full flex items-center justify-center mx-auto mb-3">
<Shield className={`w-8 h-8 ${type === "owner" ? "text-amber-500" : "text-blue-500"} `} />
</div>
<h2 className="text-xl font-bold text-white">{t("register.otpTitle")}</h2>
<p className="text-gray-400 text-sm mt-1">{t("register.otpSentTo")}</p>
<p className={`${type === "owner" ? "text-amber-500" : "text-blue-500"} font-medium text-sm`}>{formData.email}</p>
</div>
<div className="mb-6">
<label className="block text-sm font-medium text-gray-300 mb-2">{t("register.otpLabel")}</label>
<div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<KeyRound className="w-5 h-5 text-gray-400" />
</div>
<input
type="text"
value={otpCode}
maxLength={6}
onChange={(e) => setOtpCode(e.target.value)}
className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 text-white text-center tracking-[0.5em] text-xl"
placeholder="------"
/>
</div>
</div>
<div className="flex gap-3">
<button
onClick={handleVerifyOTP}
disabled={isLoading || !otpCode}
className={`flex-1 bg-gradient-to-r ${type === "owner" ? "from-amber-500 to-amber-600" : "from-blue-500 to-blue-600"} text-white py-3 rounded-xl font-medium ${type === "owner" ? "hover:from-amber-600 hover:to-amber-700" : "hover:from-blue-600 hover:to-blue-700"} disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2`}
>
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
<span>{t("register.verifying")}</span>
</>
) : (
t("register.verify")
)}
</button>
</div>
<button
onClick={handleResendOTP}
disabled={isLoading}
className={`w-full text-center ${type === "owner" ? "text-amber-400 hover:text-amber-300" : "text-blue-400 hover:text-blue-300"} text-sm mt-3 disabled:opacity-50`}
>
{t("register.resendOtp")}
</button>
</motion.div>
</motion.div>
</>
);
}

View File

@ -0,0 +1,162 @@
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useTranslation } from "react-i18next";
import toast, { Toaster } from "react-hot-toast";
import { addOwner, loginWithEmail } from "../../utils/api";
import AuthService from "../../services/AuthService";
import { OwnerType } from "../../enums";
import OtpDialog from "./OtpDialog";
import HeaderOfSteps from "./HeaderOfSteps";
import HeaderOfForm from "./HeaderOfForm";
import { BackgroundElements, fadeInUp, staggerContainer } from "../Animation/OwnerPageAnimation";
import { validateStep1, validateStep2 } from "../../validations/OwnerValidatoin";
import OwnerFormStepOne from "./OwnerFormStepOne";
import ButtomStepOne from "./ButtomStepOne";
import ButtomStepTwo from "./ButtomStepTwo";
import OwnerFormStepTwo from "./OwnerFormStepTwo";
import InteractiveBackground from "../Animation/Background";
export default function Owner() {
const { t } = useTranslation();
const [step, setStep] = useState(1);
const [showOtpModal, setShowOtpModal] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({
firstName: "",
lastName: "",
email: "",
phone: "",
whatsapp: "",
phone2: "",
nationalNumber: "",
password: "",
confirmPassword: "",
ownerType: OwnerType.PERSON,
agreeTerms: false,
});
const [idImages, setIdImages] = useState({ front: null, back: null, license: null });
const [idImagePreviews, setIdImagePreviews] = useState({ front: "", back: "", license: "" });
const [errors, setErrors] = useState({});
const isCompany = Number(formData.ownerType) === OwnerType.REAL_ESTATE_AGENCY;
const handleNextStep = () => {
if (validateStep1(formData, setErrors, t)) {
setStep(2);
window.scrollTo({ top: 0, behavior: "smooth" });
} else {
toast.error(t("validation.correctErrors"));
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateStep2(idImages, setErrors, t)) {
toast.error(t("register.completeRequiredImages"));
return;
}
if (!formData.agreeTerms) {
toast.error(t("validation.agreeToTerms"));
return;
}
setIsLoading(true);
const payload = {
firstName: formData.firstName,
lastName: formData.lastName,
email: formData.email,
phoneNumber: formData.phone || "",
whatsAppNumber: formData.whatsapp,
phone: formData.phone2,
nationalNumber: formData.nationalNumber,
password: formData.password,
ownerType: formData.ownerType,
};
try {
const licenseImage = isCompany ? idImages.license : null;
const res = await addOwner(payload, idImages.front, idImages.back,licenseImage);
if (res.status === 200 || res.ok) {
toast.success(res.message || t("register.accountCreated")||res.data[0].message , { duration: 4000 });
const loginRes = await loginWithEmail(formData.email, formData.password);
if (loginRes.status === 206) {
const otpToken = loginRes.data;
if (otpToken) AuthService.addToken(otpToken);
toast(loginRes.message || t("register.otpSentEmail"), { icon: "📧" });
setShowOtpModal(true);
} else if (loginRes.status === 200) {
const loginToken = loginRes.data;
if (loginToken) AuthService.addToken(loginToken);
toast.success(loginRes.message || t("register.loginSuccess"));
}
} else {
toast.error(res.data[0].message || res.data?.message);
}
} catch (err) {
toast.error(err.message || t("register.registrationError"));
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
<Toaster position="top-center" reverseOrder={false} />
<div className="absolute inset-0 overflow-hidden">
<BackgroundElements />
<InteractiveBackground />
</div>
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.5 }} className="relative z-10 w-full max-w-2xl">
<HeaderOfSteps step={step} numberStep={[1, 2]} colors={"bg-amber-500"} />
<motion.div
key={step}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
className="bg-white/5 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden"
>
<HeaderOfForm step={step} num={0} />
<div className="p-8">
<motion.form
variants={staggerContainer}
initial="initial"
animate="animate"
onSubmit={
step === 1
? (e) => {
e.preventDefault();
handleNextStep();
}
: handleSubmit
}
className="space-y-6"
>
{step === 1 && (
<OwnerFormStepOne
setErrors={setErrors}
setFormData={setFormData}
formData={formData}
errors={errors}
isCompany={isCompany}
setIdImagePreviews={setIdImagePreviews}
setIdImages={setIdImages}
type={"owner"}
/>
)}
{step === 2 && (
<>
<OwnerFormStepTwo idImagePreviews={idImagePreviews} errors={errors} setIdImagePreviews={setIdImagePreviews} setIdImages={setIdImages} setFormData={setFormData} formData={formData} />
</>
)}
<motion.div variants={fadeInUp} className="flex gap-3 pt-4">
{step === 1 ? ( <> <ButtomStepOne type={"owner"} /></>) : ( <> <ButtomStepTwo formData={formData} isLoading={isLoading} setStep={setStep} type={"owner"} /> </>)}
</motion.div>
</motion.form>
</div>
</motion.div>
</motion.div>
<AnimatePresence>{showOtpModal && <OtpDialog formData={formData} setShowOtpModal={setShowOtpModal} type={"owner"} />}</AnimatePresence>
</div>
);
}

View File

@ -0,0 +1,271 @@
import { useTranslation } from "react-i18next";
import { fadeInUp } from "../Animation/OwnerPageAnimation";
import { motion, AnimatePresence } from "framer-motion";
import { User, Mail, Phone, Lock, Eye, EyeOff, MessageCircle, Camera, X, CheckCircle, XCircle } from "lucide-react";
import { CustomerTypeLabels, OwnerTypeLabels } from "@/app/enums";
import { useRef, useState } from "react";
import { handleImageUpload } from "@/app/HelperFunction/UploadImage";
import UploadImage from "./UploadImage";
export default function OwnerFormStepOne({ formData, errors, setFormData, setErrors, isCompany, idImagePreviews, setIdImagePreviews, setIdImages, type }) {
const { t } = useTranslation();
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const fileInputLicenseRef = useRef(null);
return (
<>
<motion.div variants={fadeInUp} className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.firstName")} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<User className={`w-5 h-5 ${errors.firstName ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
</div>
<input
type="text"
value={formData.firstName}
onChange={(e) => {
setFormData({ ...formData, firstName: e.target.value });
setErrors({ ...errors, firstName: null });
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.firstName ? "border-red-500" : "border-gray-700"}`}
placeholder={t("register.firstName")}
/>
</div>
{errors.firstName && <p className="text-red-500 text-sm mt-1">{errors.firstName}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.lastName")} <span className="text-red-500">*</span>
</label>
<input
type="text"
value={formData.lastName}
onChange={(e) => {
setFormData({ ...formData, lastName: e.target.value });
setErrors({ ...errors, lastName: null });
}}
className={`w-full px-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.lastName ? "border-red-500" : "border-gray-700"}`}
placeholder={t("register.lastName")}
/>
{errors.lastName && <p className="text-red-500 text-sm mt-1">{errors.lastName}</p>}
</div>
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.email")} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Mail className={`w-5 h-5 ${errors.email ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
</div>
<input
type="email"
value={formData.email}
onChange={(e) => {
setFormData({ ...formData, email: e.target.value });
setErrors({ ...errors, email: null });
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.email ? "border-red-500" : "border-gray-700"}`}
placeholder={t("register.emailPlaceholder")}
/>
</div>
{errors.email && <p className="text-red-500 text-sm mt-1">{errors.email}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.phone")} <span className="text-gray-500">({t("register.optional")})</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Phone className="w-5 h-5 text-gray-400 group-focus-within:text-amber-500" />
</div>
<input
type="tel"
value={formData.phone}
onChange={(e) => {
setFormData({ ...formData, phone: e.target.value });
setErrors({ ...errors, phone: null });
}}
className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all"
placeholder={t("register.phoneOptionalPlaceholder")}
/>
</div>
{errors.phone && <p className="text-red-500 text-sm mt-1">{errors.phone}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.whatsapp")} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<MessageCircle className={`w-5 h-5 ${errors.whatsapp ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
</div>
<input
type="tel"
value={formData.whatsapp}
onChange={(e) => {
setFormData({ ...formData, whatsapp: e.target.value });
setErrors({ ...errors, whatsapp: null });
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.whatsapp ? "border-red-500" : "border-gray-700"}`}
placeholder={t("register.whatsappPlaceholder")}
/>
</div>
{errors.whatsapp && <p className="text-red-500 text-sm mt-1">{errors.whatsapp}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.phoneSecondary")} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Phone className={`w-5 h-5 ${errors.phone2 ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
</div>
<input
type="tel"
value={formData.phone2}
onChange={(e) => {
setFormData({ ...formData, phone2: e.target.value });
setErrors({ ...errors, phone2: null });
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.phone2 ? "border-red-500" : "border-gray-700"}`}
placeholder={t("register.phoneSecondaryPlaceholder")}
maxLength={7}
/>
</div>
{errors.phone2 && <p className="text-red-500 text-sm mt-1">{errors.phone2}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.nationalNumber")} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<User className={`w-5 h-5 ${errors.nationalNumber ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
</div>
<input
type="text"
value={formData.nationalNumber}
onChange={(e) => {
setFormData({ ...formData, nationalNumber: e.target.value });
setErrors({ ...errors, nationalNumber: null });
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.nationalNumber ? "border-red-500" : "border-gray-700"}`}
placeholder={t("register.nationalNumberPlaceholder")}
/>
</div>
{errors.nationalNumber && <p className="text-red-500 text-sm mt-1">{errors.nationalNumber}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
{type === "owner" ? (
<>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.ownerType")} <span className="text-red-500">*</span>
</label>
<select
value={formData.ownerType.toString()}
onChange={(e) => {
const selectedType = parseInt(e.target.value, 10);
setFormData((prev) => ({ ...prev, ownerType: selectedType }));
}}
className="w-full py-3 px-4 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent appearance-none text-gray-900 cursor-pointer"
>
{Object.entries(OwnerTypeLabels).map(([value, label]) => (
<option key={value} value={value} className="bg-gray-900 ">
{label}
</option>
))}
</select>
</>
) : (
<>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.customerType")} <span className="text-red-500">*</span>
</label>
<select
value={formData.customerType}
onChange={(e) => setFormData({ ...formData, customerType: e.target.value })}
className="w-full py-3 px-4 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white appearance-none cursor-pointer"
>
{Object.entries(CustomerTypeLabels).map(([value, label]) => (
<option key={value} value={value} className="bg-gray-900 text-white">
{label}
</option>
))}
</select>
</motion.div>
</>
)}
</motion.div>
<AnimatePresence>
{isCompany && <UploadImage idImagePreviews={idImagePreviews} type={"license"} errors={errors.license} setIdImagePreviews={setIdImagePreviews} setIdImages={setIdImages} />}
</AnimatePresence>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.password")} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${errors.password ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
</div>
<input
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => {
setFormData({ ...formData, password: e.target.value });
setErrors({ ...errors, password: null });
}}
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.password ? "border-red-500" : "border-gray-700"}`}
placeholder={t("register.passwordPlaceholder")}
/>
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
{showPassword ? <EyeOff className="w-5 h-5 text-gray-400" /> : <Eye className="w-5 h-5 text-gray-400" />}
</button>
</div>
{errors.password && <p className="text-red-500 text-sm mt-1">{errors.password}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.confirmPassword")} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${errors.confirmPassword ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
</div>
<input
type={showConfirmPassword ? "text" : "password"}
value={formData.confirmPassword}
onChange={(e) => {
setFormData({ ...formData, confirmPassword: e.target.value });
setErrors({ ...errors, confirmPassword: null });
}}
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.confirmPassword ? "border-red-500" : "border-gray-700"}`}
placeholder={t("register.confirmPasswordPlaceholder")}
/>
<button type="button" onClick={() => setShowConfirmPassword(!showConfirmPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
{showConfirmPassword ? <EyeOff className="w-5 h-5 text-gray-400" /> : <Eye className="w-5 h-5 text-gray-400" />}
</button>
{formData.confirmPassword && (
<div className="absolute inset-y-0 left-12 flex items-center">
{formData.password === formData.confirmPassword ? <CheckCircle className="w-5 h-5 text-green-500" /> : <XCircle className="w-5 h-5 text-red-500" />}
</div>
)}
</div>
{errors.confirmPassword && <p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>}
</motion.div>
</>
);
}

View File

@ -0,0 +1,12 @@
import Policy from "./PolicyAgree";
import UploadImage from "./UploadImage";
export default function OwnerFormStepTwo({idImagePreviews,errors,setIdImagePreviews,setIdImages,setFormData,formData}) {
return (
<>
<UploadImage idImagePreviews={idImagePreviews} type={"front"} errors={errors.front} setIdImagePreviews={setIdImagePreviews} setIdImages={setIdImages} />
<UploadImage idImagePreviews={idImagePreviews} type={"back"} errors={errors.back} setIdImagePreviews={setIdImagePreviews} setIdImages={setIdImages} />
<Policy setFormData={setFormData} formData={formData} type={"owner"} />
</>
);
}

View File

@ -0,0 +1,31 @@
import { motion } from "framer-motion";
import { fadeInUp } from "../Animation/OwnerPageAnimation";
import Link from "next/link";
import { useTranslation } from "react-i18next";
export default function Policy({ formData, setFormData, type }) {
const { t } = useTranslation();
return (
<>
<motion.div variants={fadeInUp} className="flex items-center gap-2">
<input
type="checkbox"
id="terms"
checked={formData.agreeTerms}
onChange={(e) => setFormData({ ...formData, agreeTerms: e.target.checked })}
className={`w-4 h-4 rounded border-gray-600 bg-white/5 ${type === "owner" ? `text-amber-500 focus:ring-amber-500` : ` text-blue-500 focus:ring-blue-500`} `}
required
/>
<label htmlFor="terms" className="text-sm text-gray-300">
{t("register.agreeTerms")}{" "}
<Link href="/terms" className={`${type === "owner" ? `text-amber-500 focus:ring-amber-500` : ` text-blue-500 focus:ring-blue-500`}`}>
{t("register.termsOfUse")}
</Link>{" "}
{t("register.and")}{" "}
<Link href="/privacy" className={`${type === "owner" ? `text-amber-500 focus:ring-amber-500` : ` text-blue-500 focus:ring-blue-500`}`}>
{t("register.privacyPolicy")}
</Link>
</label>
</motion.div>
</>
);
}

View File

@ -0,0 +1,85 @@
"use client";
import { useTranslation } from "react-i18next";
import { fadeInUp } from "../Animation/OwnerPageAnimation";
import { handleImageUpload } from "@/app/HelperFunction/UploadImage";
import { motion } from "framer-motion";
import Image from "next/image";
import { Camera, X } from "lucide-react";
import { useRef } from "react";
export default function UploadImage({ idImagePreviews, type, errors, setIdImagePreviews, setIdImages }) {
const { t } = useTranslation();
const fileInputRef = useRef(null);
const currentPreview = idImagePreviews?.[type];
const handleRemoveImage = (e) => {
e.stopPropagation();
setIdImages((prev) => ({ ...prev, [type]: null }));
setIdImagePreviews((prev) => ({ ...prev, [type]: "" }));
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const getLabel = () => {
switch (type) {
case "front":
return t("register.owner.frontIdLabel");
case "back":
return t("register.owner.backIdLabel");
case "license":
return t("register.owner.licenseImageLabel");
default:
return "";
}
};
const getClickText = () => {
return type === "license" ? t("register.owner.clickUploadLicense") : t("register.owner.clickUploadImage");
};
const getAltText = () => {
switch (type) {
case "front":
return t("register.uploadFrontAlt");
case "back":
return t("register.uploadBackAlt");
case "license":
return t("register.uploadLicenseAlt");
default:
return "Preview Image";
}
};
return (
<motion.div variants={fadeInUp} key={`${type}-upload-step`}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{getLabel()} {type === "license" ? <span className="text-gray-400">({t("register.optional")})</span> : <span className="text-red-500">*</span>}
</label>
{type === "license" && <div className="mb-2 text-xs text-gray-400">{t("register.owner.licenseHint")}</div>}
<div
onClick={() => fileInputRef.current?.click()}
className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${
currentPreview ? "border-green-500 bg-green-500/10" : errors ? "border-red-500 bg-red-500/10" : "border-gray-700 hover:border-amber-500 hover:bg-white/5"
}`}
>
<input ref={fileInputRef} type="file" accept="image/*" onChange={(e) => handleImageUpload(type, e.target.files?.[0], t, setIdImagePreviews, setIdImages)} className="hidden" />
{currentPreview ? (
<div className="relative">
<Image src={currentPreview} alt={getAltText()} width={200} height={120} className="mx-auto rounded-lg object-cover" />
<button type="button" onClick={handleRemoveImage} className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600 transition-colors">
<X className="w-4 h-4 text-white" />
</button>
</div>
) : (
<>
<Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" />
<p className="text-gray-400">{getClickText()}</p>
<p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG {t("register.upTo5MB")}</p>
</>
)}
</div>
{errors && <p className="text-red-500 text-sm mt-1">{errors}</p>}
</motion.div>
);
}

View File

@ -0,0 +1,100 @@
"use client";
import { useState, useEffect, useRef } from "react";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png",
iconUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png",
shadowUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png",
});
export default function PropertyMapWithMarkers({ properties = [], onPropertyClick }) {
const mapRef = useRef(null);
const mapInstanceRef = useRef(null);
const markersRef = useRef([]);
const [mapLoaded, setMapLoaded] = useState(false);
useEffect(() => {
if (!mapRef.current || mapInstanceRef.current) return;
const map = L.map(mapRef.current).setView([33.5138, 38.9968], 7);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 19,
}).addTo(map);
mapInstanceRef.current = map;
setMapLoaded(true);
return () => {
if (mapInstanceRef.current) {
mapInstanceRef.current.remove();
mapInstanceRef.current = null;
}
};
}, []);
useEffect(() => {
if (!mapInstanceRef.current || !mapLoaded) return;
markersRef.current.forEach((marker) => marker.remove());
markersRef.current = [];
properties.forEach((property) => {
if (property.lat && property.lng) {
const marker = L.marker([property.lat, property.lng]).addTo(mapInstanceRef.current);
const popupContent = `
<div dir="rtl" style="text-align: right; padding: 12px; max-width: 250px;">
<h3 style="font-weight: bold; font-size: 16px; margin-bottom: 8px; color: #111;">${property.title || "عقار"}</h3>
<p style="font-size: 14px; color: #666; margin-bottom: 8px;">${property.address || property.location?.address || ""}</p>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
<span style="font-weight: bold; font-size: 18px; color: #d97706;">${formatPrice(property)}</span>
</div>
${property.images && property.images.length > 0 ? `<img src="${property.images[0] ?? "https://tse4.mm.bing.net/th/id/OIP.-VT-J9Brp8KDSlb0Z_990wAAAA?r=0&rs=1&pid=ImgDetMain&o=7&rm=3"}" alt="${property.title}" style="width: 100%; height: 96px; object-fit: cover; border-radius: 8px; margin-bottom: 8px;" onerror="this.src='/property-placeholder.jpg'" />` : ""}
<div style="font-size: 12px; color: #888;">
${property.type ? `<p>النوع: ${property.type}</p>` : ""}
${property.bedrooms > 0 ? `<p>غرف نوم: ${property.bedrooms}</p>` : ""}
${property.bathrooms > 0 ? `<p>حمامات: ${property.bathrooms}</p>` : ""}
</div>
</div>
`;
marker.bindPopup(popupContent);
marker.on("click", () => {
if (onPropertyClick) {
onPropertyClick(property);
}
});
markersRef.current.push(marker);
}
});
if (markersRef.current.length > 0) {
const group = L.featureGroup(markersRef.current);
mapInstanceRef.current.fitBounds(group.getBounds(), { padding: [50, 100] });
}
}, [properties, mapLoaded]);
const formatPrice = (property) => {
if (property.priceUnit === "monthly") {
return `${property.price?.toLocaleString() || 0} ل.س/شهر`;
} else if (property.priceUnit === "daily") {
return `${property.price?.toLocaleString() || 0} ل.س/يوم`;
} else {
return `${property.price?.toLocaleString() || 0} ل.س`;
}
};
return (
<div className="relative w-full h-[600px] rounded-xl overflow-hidden border-2 border-gray-200">
<div ref={mapRef} className="w-full h-full z-0" />
</div>
);
}

View File

@ -1,356 +0,0 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import { useProperties } from '@/app/contexts/PropertyContext';
import { CommissionType, City, CitiesList } from '@/app/enums';
import { X, MapPin, Home, DollarSign, Percent } from 'lucide-react';
export default function AddPropertyForm({ onClose, onSuccess }) {
const { addProperty } = useProperties();
const [formData, setFormData] = useState({
title: '',
description: '',
city: '',
district: '',
address: '',
latitude: '',
longitude: '',
type: 'apartment',
bedrooms: 1,
bathrooms: 1,
area: 0,
floor: 1,
dailyPrice: 0,
commissionRate: 5,
commissionType: CommissionType.FROM_OWNER,
securityDeposit: 0,
images: [],
features: [],
status: 'available'
});
const [selectedFeatures, setSelectedFeatures] = useState([]);
const featuresList = [
'مسبح',
'حديقة خاصة',
'موقف سيارات',
'مطبخ مجهز',
'تدفئة مركزية',
'بلكونة',
'نظام أمني',
'حديقة كبيرة',
'صالة استقبال',
'غرفة خادمة',
'كراج',
'إطلالة بحرية',
'تكييف مركزي',
'مخزن'
];
const handleSubmit = async (e) => {
e.preventDefault();
const propertyData = {
...formData,
features: selectedFeatures,
priceDisplay: {
daily: formData.dailyPrice,
monthly: formData.dailyPrice * 30,
withCommission: calculateCommissionPrice(formData)
},
location: {
lat: formData.latitude,
lng: formData.longitude,
address: formData.address
}
};
try {
await addProperty(propertyData);
onSuccess?.();
onClose();
} catch (error) {
console.error('Error adding property:', error);
}
};
const calculateCommissionPrice = (data) => {
const { dailyPrice, commissionRate, commissionType } = data;
const commission = (dailyPrice * commissionRate) / 100;
switch(commissionType) {
case CommissionType.FROM_TENANT:
return dailyPrice + commission;
case CommissionType.FROM_OWNER:
return dailyPrice;
case CommissionType.FROM_BOTH:
return dailyPrice + (commission / 2);
default:
return dailyPrice;
}
};
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50"
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
className="bg-white rounded-xl w-full max-w-4xl max-h-[90vh] overflow-y-auto"
>
<div className="sticky top-0 bg-white border-b p-4 flex justify-between items-center">
<h2 className="text-xl font-bold">إضافة عقار جديد</h2>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-lg">
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-6">
<div className="bg-blue-50 p-4 rounded-lg">
<h3 className="font-semibold mb-3 flex items-center gap-2">
<MapPin className="w-4 h-4" />
موقع العقار (سيظهر على الخريطة)
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">المدينة</label>
<select
value={formData.city}
onChange={(e) => setFormData({...formData, city: e.target.value})}
className="w-full p-2 border rounded-lg"
required
>
<option value="">اختر المدينة</option>
{Object.values(CITIES).map(city => (
<option key={city} value={city}>{city}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">الحي</label>
<input
type="text"
value={formData.district}
onChange={(e) => setFormData({...formData, district: e.target.value})}
className="w-full p-2 border rounded-lg"
required
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium mb-1">العنوان بالتفصيل</label>
<input
type="text"
value={formData.address}
onChange={(e) => setFormData({...formData, address: e.target.value})}
className="w-full p-2 border rounded-lg"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">خط العرض (Latitude)</label>
<input
type="number"
step="any"
value={formData.latitude}
onChange={(e) => setFormData({...formData, latitude: e.target.value})}
className="w-full p-2 border rounded-lg"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">خط الطول (Longitude)</label>
<input
type="number"
step="any"
value={formData.longitude}
onChange={(e) => setFormData({...formData, longitude: e.target.value})}
className="w-full p-2 border rounded-lg"
required
/>
</div>
</div>
</div>
<div className="bg-amber-50 p-4 rounded-lg">
<h3 className="font-semibold mb-3 flex items-center gap-2">
<DollarSign className="w-4 h-4" />
السعر ونسبة الربح
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">
السعر اليومي (ل.س)
</label>
<input
type="number"
value={formData.dailyPrice}
onChange={(e) => setFormData({...formData, dailyPrice: Number(e.target.value)})}
className="w-full p-2 border rounded-lg"
required
min="0"
/>
<p className="text-xs text-gray-500 mt-1">
هذا السعر سيظهر على الخريطة
</p>
</div>
<div>
<label className="block text-sm font-medium mb-1">
نسبة ربح المنصة (%)
</label>
<div className="flex items-center gap-2">
<input
type="number"
value={formData.commissionRate}
onChange={(e) => setFormData({...formData, commissionRate: Number(e.target.value)})}
className="w-full p-2 border rounded-lg"
min="0"
max="100"
step="0.1"
required
/>
<Percent className="w-4 h-4 text-gray-400" />
</div>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium mb-2">
مصدر العمولة (بموافقة الأدمن)
</label>
<div className="grid grid-cols-3 gap-3">
<label className="flex items-center gap-2 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="commissionType"
value={CommissionType.FROM_OWNER}
checked={formData.commissionType === CommissionType.FROM_OWNER}
onChange={(e) => setFormData({...formData, commissionType: e.target.value})}
/>
<span>من المالك</span>
</label>
<label className="flex items-center gap-2 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="commissionType"
value={CommissionType.FROM_TENANT}
checked={formData.commissionType === CommissionType.FROM_TENANT}
onChange={(e) => setFormData({...formData, commissionType: e.target.value})}
/>
<span>من المستأجر</span>
</label>
<label className="flex items-center gap-2 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="commissionType"
value={CommissionType.FROM_BOTH}
checked={formData.commissionType === CommissionType.FROM_BOTH}
onChange={(e) => setFormData({...formData, commissionType: e.target.value})}
/>
<span>من الاثنين</span>
</label>
</div>
</div>
<div className="col-span-2 bg-white p-3 rounded-lg">
<h4 className="font-medium mb-2">تفاصيل السعر بعد العمولة:</h4>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-gray-600">السعر الأصلي:</span>
<span className="block font-bold">{formData.dailyPrice} ل.س</span>
</div>
<div>
<span className="text-gray-600">العمولة:</span>
<span className="block font-bold">
{(formData.dailyPrice * formData.commissionRate / 100)} ل.س
</span>
</div>
<div>
<span className="text-gray-600">السعر النهائي:</span>
<span className="block font-bold text-green-600">
{calculateCommissionPrice(formData)} ل.س
</span>
</div>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">نوع العقار</label>
<select
value={formData.type}
onChange={(e) => setFormData({...formData, type: e.target.value})}
className="w-full p-2 border rounded-lg"
>
<option value="apartment">شقة</option>
<option value="house">بيت</option>
<option value="villa">فيلا</option>
<option value="studio">استوديو</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">مبلغ الضمان (ل.س)</label>
<input
type="number"
value={formData.securityDeposit}
onChange={(e) => setFormData({...formData, securityDeposit: Number(e.target.value)})}
className="w-full p-2 border rounded-lg"
min="0"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-2">المميزات</label>
<div className="grid grid-cols-3 gap-2">
{featuresList.map(feature => (
<label key={feature} className="flex items-center gap-2 p-2 border rounded-lg">
<input
type="checkbox"
checked={selectedFeatures.includes(feature)}
onChange={(e) => {
if (e.target.checked) {
setSelectedFeatures([...selectedFeatures, feature]);
} else {
setSelectedFeatures(selectedFeatures.filter(f => f !== feature));
}
}}
/>
<span className="text-sm">{feature}</span>
</label>
))}
</div>
</div>
<div className="flex gap-3 pt-4 border-t">
<button
type="submit"
className="flex-1 bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
إضافة العقار
</button>
<button
type="button"
onClick={onClose}
className="px-6 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
إلغاء
</button>
</div>
</form>
</motion.div>
</motion.div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,139 +0,0 @@
'use client';
import { motion } from 'framer-motion';
import { Users, Home, Calendar, DollarSign } from 'lucide-react';
import { useEffect, useState } from 'react';
export default function DashboardStats() {
const [stats, setStats] = useState({
totalUsers: 0,
totalProperties: 0,
activeBookings: 0,
totalRevenue: 0,
pendingRequests: 0,
availableProperties: 0
});
useEffect(() => {
setStats({
totalUsers: 156,
totalProperties: 89,
activeBookings: 34,
totalRevenue: 12500000,
pendingRequests: 12,
availableProperties: 45
});
}, []);
const formatNumber = (num) => {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const formatCurrency = (amount) => {
return `${formatNumber(amount)} ل.س`;
};
const cards = [
{
title: 'إجمالي المستخدمين',
value: stats.totalUsers,
icon: Users,
color: 'from-blue-600 to-blue-700',
bgColor: 'bg-blue-100',
iconColor: 'text-blue-600'
},
{
title: 'إجمالي العقارات',
value: stats.totalProperties,
icon: Home,
color: 'from-emerald-600 to-emerald-700',
bgColor: 'bg-emerald-100',
iconColor: 'text-emerald-600'
},
{
title: 'الحجوزات النشطة',
value: stats.activeBookings,
icon: Calendar,
color: 'from-purple-600 to-purple-700',
bgColor: 'bg-purple-100',
iconColor: 'text-purple-600'
},
{
title: 'الإيرادات',
value: formatCurrency(stats.totalRevenue),
icon: DollarSign,
color: 'from-amber-600 to-amber-700',
bgColor: 'bg-amber-100',
iconColor: 'text-amber-600'
}
];
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{cards.map((card, index) => {
const Icon = card.icon;
return (
<motion.div
key={card.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className={`bg-gradient-to-br ${card.color} text-white rounded-xl shadow-lg p-5`}
>
<div className="flex items-center justify-between mb-4">
<div className={`p-3 ${card.bgColor} bg-opacity-20 rounded-lg`}>
<Icon className="w-6 h-6" />
</div>
<div className="text-right">
<div className="text-2xl font-bold">{card.value}</div>
<div className="text-sm opacity-90">{card.title}</div>
</div>
</div>
<div className="text-xs opacity-75">
آخر تحديث: الآن
</div>
</motion.div>
);
})}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="bg-white border rounded-lg p-4"
>
<div className="text-sm text-gray-600 mb-1">طلبات حجز معلقة</div>
<div className="text-2xl font-bold text-yellow-600">{stats.pendingRequests}</div>
<div className="text-xs text-gray-500">بحاجة لموافقة</div>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
className="bg-white border rounded-lg p-4"
>
<div className="text-sm text-gray-600 mb-1">عقارات متاحة</div>
<div className="text-2xl font-bold text-green-600">{stats.availableProperties}</div>
<div className="text-xs text-gray-500">جاهزة للإيجار</div>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
className="bg-white border rounded-lg p-4"
>
<div className="text-sm text-gray-600 mb-1">نسبة الإشغال</div>
<div className="text-2xl font-bold text-blue-600">
{Math.round((stats.activeBookings / stats.totalProperties) * 100)}%
</div>
<div className="text-xs text-gray-500">من إجمالي العقارات</div>
</motion.div>
</div>
</div>
);
}

View File

@ -1,607 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import {
DollarSign,
Calendar,
User,
Home,
Download,
Filter,
Search,
TrendingUp,
TrendingDown,
Wallet,
Shield,
FileText,
Printer,
X,
CheckCircle
} from 'lucide-react';
import { formatCurrency } from '@/app/utils/calculations';
import toast, { Toaster } from 'react-hot-toast';
import * as XLSX from 'xlsx';
export default function LedgerBook({ userType = 'admin' }) {
const [transactions, setTransactions] = useState([]);
const [filteredTransactions, setFilteredTransactions] = useState([]);
const [dateRange, setDateRange] = useState({ start: '', end: '' });
const [searchTerm, setSearchTerm] = useState('');
const [summary, setSummary] = useState({
totalRevenue: 0,
pendingPayments: 0,
securityDeposits: 0,
commissionEarned: 0
});
const [isExporting, setIsExporting] = useState(false);
useEffect(() => {
loadTransactions();
}, []);
useEffect(() => {
filterTransactions();
calculateSummary();
}, [transactions, dateRange, searchTerm]);
const loadTransactions = async () => {
const mockTransactions = [
{
id: 'T001',
date: '2024-02-20',
type: 'rent_payment',
description: 'دفعة إيجار - فيلا في دمشق',
amount: 500000,
commission: 25000,
fromUser: 'أحمد محمد',
toUser: 'مالك العقار',
propertyId: 1,
propertyName: 'luxuryVillaDamascus',
status: 'completed',
paymentMethod: 'cash'
},
{
id: 'T002',
date: '2024-02-19',
type: 'security_deposit',
description: 'سلفة ضمان - شقة في حلب',
amount: 250000,
commission: 0,
fromUser: 'سارة أحمد',
toUser: 'مالك العقار',
propertyId: 2,
propertyName: 'modernApartmentAleppo',
status: 'pending_refund',
paymentMethod: 'cash'
},
{
id: 'T003',
date: '2024-02-18',
type: 'commission',
description: 'عمولة منصة - فيلا في درعا',
amount: 30000,
commission: 30000,
fromUser: 'محمد الحلبي',
toUser: 'المنصة',
propertyId: 5,
propertyName: 'villaDaraa',
status: 'completed',
paymentMethod: 'cash'
}
];
setTransactions(mockTransactions);
};
const filterTransactions = () => {
let filtered = [...transactions];
if (dateRange.start && dateRange.end) {
filtered = filtered.filter(t =>
t.date >= dateRange.start && t.date <= dateRange.end
);
}
if (searchTerm) {
filtered = filtered.filter(t =>
t.description.includes(searchTerm) ||
t.fromUser.includes(searchTerm) ||
t.toUser.includes(searchTerm)
);
}
setFilteredTransactions(filtered);
};
const calculateSummary = () => {
const summary = filteredTransactions.reduce((acc, t) => {
if (t.type === 'rent_payment' || t.type === 'commission') {
acc.totalRevenue += t.amount;
}
if (t.type === 'security_deposit' && t.status === 'pending_refund') {
acc.securityDeposits += t.amount;
}
if (t.commission) {
acc.commissionEarned += t.commission;
}
if (t.status === 'pending') {
acc.pendingPayments += t.amount;
}
return acc;
}, {
totalRevenue: 0,
pendingPayments: 0,
securityDeposits: 0,
commissionEarned: 0
});
setSummary(summary);
};
const getTransactionIcon = (type) => {
switch(type) {
case 'rent_payment':
return <Home className="w-4 h-4 text-blue-600" />;
case 'security_deposit':
return <Shield className="w-4 h-4 text-green-600" />;
case 'commission':
return <TrendingUp className="w-4 h-4 text-amber-600" />;
default:
return <DollarSign className="w-4 h-4" />;
}
};
const exportToExcel = async () => {
if (filteredTransactions.length === 0) {
toast.error('لا توجد معاملات للتصدير');
return;
}
setIsExporting(true);
toast.loading('جاري تصدير البيانات...', { id: 'export' });
try {
const exportData = filteredTransactions.map(t => ({
'رقم العملية': t.id,
'التاريخ': t.date,
'نوع العملية': t.type === 'rent_payment' ? 'دفعة إيجار' :
t.type === 'security_deposit' ? 'سلفة ضمان' :
t.type === 'commission' ? 'عمولة' : 'أخرى',
'الوصف': t.description,
'من': t.fromUser,
'إلى': t.toUser,
'المبلغ (ل.س)': t.amount,
'العمولة (ل.س)': t.commission || 0,
'الحالة': t.status === 'completed' ? 'مكتمل' :
t.status === 'pending' ? 'معلق' :
t.status === 'pending_refund' ? 'بإنتظار الاسترداد' : 'مؤكد',
}));
const summaryRow = {
'رقم العملية': '',
'التاريخ': '',
'نوع العملية': '',
'الوصف': '',
'من': '',
'إلى': '',
'المبلغ (ل.س)': summary.totalRevenue,
'العمولة (ل.س)': summary.commissionEarned,
'الحالة': ''
};
exportData.push(summaryRow);
const worksheet = XLSX.utils.json_to_sheet(exportData);
const columnWidths = [
{ wch: 12 }, // رقم العملية
{ wch: 12 }, // التاريخ
{ wch: 12 }, // نوع العملية
{ wch: 30 }, // الوصف
{ wch: 20 }, // من
{ wch: 20 }, // إلى
{ wch: 15 }, // المبلغ
{ wch: 15 }, // العمولة
{ wch: 12 }, // الحالة
];
worksheet['!cols'] = columnWidths;
const range = XLSX.utils.decode_range(worksheet['!ref']);
for (let C = range.s.c; C <= range.e.c; ++C) {
const address = XLSX.utils.encode_col(C) + '1';
if (!worksheet[address]) continue;
worksheet[address].s = {
font: { bold: true, sz: 12 },
fill: { fgColor: { rgb: "F59E0B" } },
alignment: { horizontal: "center", vertical: "center" }
};
}
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'دفتر الحسابات');
const fileName = `دفتر_الحسابات_${new Date().toISOString().split('T')[0]}.xlsx`;
XLSX.writeFile(workbook, fileName);
toast.success(`تم تصدير ${filteredTransactions.length} معاملة بنجاح!`, { id: 'export' });
} catch (error) {
console.error('Error exporting to Excel:', error);
toast.error('حدث خطأ أثناء تصدير البيانات', { id: 'export' });
} finally {
setIsExporting(false);
}
};
const printReport = () => {
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<!DOCTYPE html>
<html dir="rtl">
<head>
<meta charset="UTF-8">
<title>تقرير دفتر الحسابات</title>
<style>
body {
font-family: 'Cairo', Arial, sans-serif;
padding: 20px;
direction: rtl;
}
.header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #f59e0b;
}
.title {
font-size: 24px;
font-weight: bold;
color: #1f2937;
}
.subtitle {
color: #6b7280;
margin-top: 5px;
}
.summary {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
margin-bottom: 30px;
}
.summary-card {
background: #f9fafb;
padding: 15px;
border-radius: 12px;
text-align: center;
}
.summary-value {
font-size: 20px;
font-weight: bold;
color: #f59e0b;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
border: 1px solid #e5e7eb;
padding: 10px;
text-align: right;
}
th {
background: #f59e0b;
color: white;
font-weight: bold;
}
.footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
color: #9ca3af;
font-size: 12px;
}
@media print {
.no-print {
display: none;
}
}
</style>
</head>
<body>
<div class="header">
<div class="title">تقرير دفتر الحسابات</div>
<div class="subtitle">الفترة: ${dateRange.start || 'بداية السجلات'} - ${dateRange.end || 'حتى الآن'}</div>
<div class="subtitle">تاريخ التقرير: ${new Date().toLocaleDateString('ar-SA')}</div>
</div>
<div class="summary">
<div class="summary-card">
<div>إجمالي الإيرادات</div>
<div class="summary-value">${formatCurrency(summary.totalRevenue)}</div>
</div>
<div class="summary-card">
<div>أرباح المنصة</div>
<div class="summary-value">${formatCurrency(summary.commissionEarned)}</div>
</div>
<div class="summary-card">
<div>سلف الضمان</div>
<div class="summary-value">${formatCurrency(summary.securityDeposits)}</div>
</div>
<div class="summary-card">
<div>المدفوعات المعلقة</div>
<div class="summary-value">${formatCurrency(summary.pendingPayments)}</div>
</div>
</div>
<table>
<thead>
<tr>
<th>التاريخ</th>
<th>الوصف</th>
<th>من</th>
<th>إلى</th>
<th>المبلغ</th>
<th>العمولة</th>
<th>الحالة</th>
</tr>
</thead>
<tbody>
${filteredTransactions.map(t => `
<tr>
<td>${t.date}</td>
<td>${t.description}</td>
<td>${t.fromUser}</td>
<td>${t.toUser}</td>
<td>${formatCurrency(t.amount)}</td>
<td>${t.commission ? formatCurrency(t.commission) : '-'}</td>
<td>${t.status === 'completed' ? 'مكتمل' : t.status === 'pending' ? 'معلق' : 'بإنتظار الرد'}</td>
</tr>
`).join('')}
</tbody>
</table>
<div class="footer">
<p>تقرير صادر عن نظام SweetHome لإدارة العقارات</p>
<p>جميع الحقوق محفوظة © ${new Date().getFullYear()}</p>
</div>
<div class="no-print" style="text-align: center; margin-top: 20px;">
<button onclick="window.print()" style="padding: 10px 20px; background: #f59e0b; color: white; border: none; border-radius: 8px; cursor: pointer;">
طباعة التقرير
</button>
</div>
</body>
</html>
`);
printWindow.document.close();
};
return (
<div className="space-y-6">
<Toaster position="top-center" reverseOrder={false} />
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-gradient-to-br from-blue-600 to-blue-700 text-white rounded-xl p-5"
>
<div className="flex items-center justify-between mb-3">
<Wallet className="w-8 h-8 opacity-80" />
<span className="text-sm opacity-90">إجمالي الإيرادات</span>
</div>
<div className="text-2xl font-bold">{formatCurrency(summary.totalRevenue)}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="bg-gradient-to-br from-amber-600 to-amber-700 text-white rounded-xl p-5"
>
<div className="flex items-center justify-between mb-3">
<TrendingUp className="w-8 h-8 opacity-80" />
<span className="text-sm opacity-90">أرباح المنصة</span>
</div>
<div className="text-2xl font-bold">{formatCurrency(summary.commissionEarned)}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="bg-gradient-to-br from-green-600 to-green-700 text-white rounded-xl p-5"
>
<div className="flex items-center justify-between mb-3">
<Shield className="w-8 h-8 opacity-80" />
<span className="text-sm opacity-90">سلف الضمان</span>
</div>
<div className="text-2xl font-bold">{formatCurrency(summary.securityDeposits)}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="bg-gradient-to-br from-red-600 to-red-700 text-white rounded-xl p-5"
>
<div className="flex items-center justify-between mb-3">
<TrendingDown className="w-8 h-8 opacity-80" />
<span className="text-sm opacity-90">المدفوعات المعلقة</span>
</div>
<div className="text-2xl font-bold">{formatCurrency(summary.pendingPayments)}</div>
</motion.div>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border">
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
placeholder="بحث في المعاملات..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-12 pr-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex gap-2">
<input
type="date"
value={dateRange.start}
onChange={(e) => setDateRange({...dateRange, start: e.target.value})}
className="px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<span className="text-gray-500 self-center">إلى</span>
<input
type="date"
value={dateRange.end}
onChange={(e) => setDateRange({...dateRange, end: e.target.value})}
className="px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex gap-2">
<button
onClick={exportToExcel}
disabled={isExporting || filteredTransactions.length === 0}
className="px-5 py-3 bg-green-600 text-white rounded-xl flex items-center gap-2 hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isExporting ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
جاري التصدير...
</>
) : (
<>
<Download className="w-5 h-5" />
تصدير Excel
</>
)}
</button>
<button
onClick={printReport}
disabled={filteredTransactions.length === 0}
className="px-5 py-3 bg-blue-600 text-white rounded-xl flex items-center gap-2 hover:bg-blue-700 transition-colors disabled:opacity-50"
>
<Printer className="w-5 h-5" />
طباعة
</button>
</div>
</div>
{(dateRange.start || dateRange.end || searchTerm) && (
<div className="mt-4 pt-4 border-t flex justify-between items-center">
<div className="text-sm text-gray-500">
<span className="font-medium">{filteredTransactions.length}</span> معاملة من إجمالي <span className="font-medium">{transactions.length}</span>
</div>
<button
onClick={() => {
setDateRange({ start: '', end: '' });
setSearchTerm('');
}}
className="text-sm text-red-500 hover:text-red-600 flex items-center gap-1"
>
<X className="w-4 h-4" />
إلغاء الفلترة
</button>
</div>
)}
</div>
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b">
<tr>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">التاريخ</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">الوصف</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">من</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">إلى</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">المبلغ</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">العمولة</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">الحالة</th>
</tr>
</thead>
<tbody className="divide-y">
{filteredTransactions.map((transaction, index) => (
<motion.tr
key={transaction.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
className="hover:bg-gray-50"
>
<td className="px-6 py-4 text-sm">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-gray-400" />
{transaction.date}
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
{getTransactionIcon(transaction.type)}
<span className="text-sm font-medium">{transaction.description}</span>
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-gray-400" />
<span className="text-sm">{transaction.fromUser}</span>
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-gray-400" />
<span className="text-sm">{transaction.toUser}</span>
</div>
</td>
<td className="px-6 py-4 text-sm font-bold text-green-600">
{formatCurrency(transaction.amount)}
</td>
<td className="px-6 py-4 text-sm text-amber-600">
{transaction.commission ? formatCurrency(transaction.commission) : '-'}
</td>
<td className="px-6 py-4">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
transaction.status === 'completed' ? 'bg-green-100 text-green-800' :
transaction.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
'bg-blue-100 text-blue-800'
}`}>
{transaction.status === 'completed' ? 'مكتمل' :
transaction.status === 'pending' ? 'معلق' : 'بإنتظار الرد'}
</span>
</td>
</motion.tr>
))}
</tbody>
</table>
</div>
{filteredTransactions.length === 0 && (
<div className="text-center py-12">
<Wallet className="w-12 h-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500">لا توجد معاملات في هذه الفترة</p>
</div>
)}
</div>
{userType === 'owner' && (
<div className="bg-blue-50 rounded-xl p-5">
<h3 className="font-bold mb-4 flex items-center gap-2">
<User className="w-5 h-5" />
أرصدة المستأجرين
</h3>
<div className="space-y-3">
<p className="text-gray-500 text-sm">لا توجد أرصدة حالياً</p>
</div>
</div>
)}
</div>
);
}

View File

@ -1,636 +0,0 @@
'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Edit,
Trash2,
Eye,
MapPin,
Bed,
Bath,
Square,
DollarSign,
Percent,
MoreVertical,
X,
CheckCircle,
AlertCircle,
Calendar,
User,
Home,
Building,
Clock
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
const DeleteConfirmationModal = ({ isOpen, onClose, onConfirm, propertyTitle }) => {
if (!isOpen) return null;
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white rounded-2xl w-full max-w-md p-6 shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="text-center mb-4">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-3">
<AlertCircle className="w-8 h-8 text-red-600" />
</div>
<h3 className="text-xl font-bold text-gray-900">تأكيد الحذف</h3>
<p className="text-sm text-gray-500 mt-2">
هل أنت متأكد من حذف العقار: <span className="font-bold text-gray-700">"{propertyTitle}"</span>؟
</p>
<p className="text-xs text-red-500 mt-1">هذا الإجراء لا يمكن التراجع عنه</p>
</div>
<div className="flex gap-3 pt-3">
<button
onClick={onClose}
className="flex-1 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors"
>
إلغاء
</button>
<button
onClick={onConfirm}
className="flex-1 bg-red-600 text-white py-3 rounded-xl font-medium hover:bg-red-700 transition-colors"
>
نعم، احذف
</button>
</div>
</motion.div>
</motion.div>
);
};
const PropertyViewModal = ({ property, isOpen, onClose }) => {
if (!isOpen || !property) return null;
const formatCurrency = (amount) => {
return amount?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + ' ل.س';
};
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white">
<div className="flex justify-between items-center">
<div>
<h2 className="text-xl font-bold">{property.title}</h2>
<p className="text-amber-100 text-sm mt-1">{property.location}</p>
</div>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full">
<X className="w-6 h-6" />
</button>
</div>
</div>
<div className="p-6 space-y-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-gray-50 p-3 rounded-xl text-center">
<Home className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-sm font-bold">{property.type === 'villa' ? 'فيلا' : property.type === 'apartment' ? 'شقة' : 'بيت'}</div>
<div className="text-xs text-gray-500">نوع العقار</div>
</div>
<div className="bg-gray-50 p-3 rounded-xl text-center">
<DollarSign className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-sm font-bold">{formatCurrency(property.price)}</div>
<div className="text-xs text-gray-500">السعر اليومي</div>
</div>
<div className="bg-gray-50 p-3 rounded-xl text-center">
<Percent className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-sm font-bold">{property.commission}%</div>
<div className="text-xs text-gray-500">نسبة العمولة</div>
</div>
<div className="bg-gray-50 p-3 rounded-xl text-center">
<Calendar className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-sm font-bold">{property.bookings || 0}</div>
<div className="text-xs text-gray-500">عدد الحجوزات</div>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
<MapPin className="w-5 h-5 text-amber-500" />
الموقع
</h3>
<p className="text-gray-700">{property.location}</p>
</div>
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3">المواصفات</h3>
<div className="grid grid-cols-3 gap-4">
<div className="text-center">
<Bed className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-lg font-bold">{property.bedrooms}</div>
<div className="text-xs text-gray-500">غرف نوم</div>
</div>
<div className="text-center">
<Bath className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-lg font-bold">{property.bathrooms}</div>
<div className="text-xs text-gray-500">حمامات</div>
</div>
<div className="text-center">
<Square className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-lg font-bold">{property.area}</div>
<div className="text-xs text-gray-500">م²</div>
</div>
</div>
</div>
<div className="bg-amber-50 p-4 rounded-xl">
<h3 className="font-bold text-amber-700 mb-3 flex items-center gap-2">
<Percent className="w-5 h-5" />
معلومات العمولة
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs text-gray-500">نسبة العمولة</label>
<div className="font-bold text-amber-600">{property.commission}%</div>
</div>
<div>
<label className="text-xs text-gray-500">مصدر العمولة</label>
<div className="font-bold text-amber-600">{property.commissionType}</div>
</div>
<div>
<label className="text-xs text-gray-500">قيمة العمولة</label>
<div className="font-bold text-amber-600">
{formatCurrency((property.price * property.commission) / 100)}
</div>
</div>
<div>
<label className="text-xs text-gray-500">حالة العقار</label>
<div className={`inline-block px-2 py-1 rounded-lg text-xs font-medium ${
property.status === 'available'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{property.status === 'available' ? 'متاح' : 'محجوز'}
</div>
</div>
</div>
</div>
</div>
</motion.div>
</motion.div>
);
};
const PropertyEditModal = ({ property, isOpen, onClose, onSave }) => {
const [formData, setFormData] = useState({ ...property });
const [isSaving, setIsSaving] = useState(false);
const formatCurrency = (amount) => {
return amount?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const handleSave = () => {
setIsSaving(true);
setTimeout(() => {
onSave(formData);
setIsSaving(false);
onClose();
toast.success('تم تحديث العقار بنجاح');
}, 1000);
};
if (!isOpen || !property) return null;
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white">
<div className="flex justify-between items-center">
<h2 className="text-xl font-bold">تعديل العقار</h2>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full">
<X className="w-6 h-6" />
</button>
</div>
<p className="text-amber-100 text-sm mt-1">يمكنك تعديل معلومات العقار</p>
</div>
<div className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
اسم العقار
</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({...formData, title: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
نوع العقار
</label>
<select
value={formData.type}
onChange={(e) => setFormData({...formData, type: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
>
<option value="villa">فيلا</option>
<option value="apartment">شقة</option>
<option value="house">بيت</option>
<option value="studio">استوديو</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
الموقع
</label>
<input
type="text"
value={formData.location}
onChange={(e) => setFormData({...formData, location: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
السعر اليومي (ل.س)
</label>
<input
type="number"
value={formData.price}
onChange={(e) => setFormData({...formData, price: parseInt(e.target.value)})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
نسبة العمولة (%)
</label>
<input
type="number"
step="0.1"
value={formData.commission}
onChange={(e) => setFormData({...formData, commission: parseFloat(e.target.value)})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
مصدر العمولة
</label>
<select
value={formData.commissionType}
onChange={(e) => setFormData({...formData, commissionType: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
>
<option value="من المالك">من المالك</option>
<option value="من المستأجر">من المستأجر</option>
<option value="من الاثنين">من الاثنين</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
عدد الغرف
</label>
<input
type="number"
value={formData.bedrooms}
onChange={(e) => setFormData({...formData, bedrooms: parseInt(e.target.value)})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
عدد الحمامات
</label>
<input
type="number"
value={formData.bathrooms}
onChange={(e) => setFormData({...formData, bathrooms: parseInt(e.target.value)})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
المساحة (م²)
</label>
<input
type="number"
value={formData.area}
onChange={(e) => setFormData({...formData, area: parseInt(e.target.value)})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
حالة العقار
</label>
<select
value={formData.status}
onChange={(e) => setFormData({...formData, status: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
>
<option value="available">متاح</option>
<option value="booked">محجوز</option>
<option value="maintenance">صيانة</option>
</select>
</div>
</div>
</div>
<div className="sticky bottom-0 bg-gray-50 border-t p-4 flex gap-3">
<button
onClick={onClose}
className="flex-1 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors"
>
إلغاء
</button>
<button
onClick={handleSave}
disabled={isSaving}
className="flex-1 bg-amber-500 text-white py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors disabled:opacity-50"
>
{isSaving ? 'جاري الحفظ...' : 'حفظ التغييرات'}
</button>
</div>
</motion.div>
</motion.div>
);
};
const MoreActionsMenu = ({ property, isOpen, onClose, onViewBookings, onViewReports }) => {
if (!isOpen) return null;
return (
<>
<div className="fixed inset-0 z-40" onClick={onClose} />
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="absolute left-0 mt-2 w-56 bg-white rounded-xl shadow-xl border border-gray-200 overflow-hidden z-50"
>
</motion.div>
</>
);
};
export default function PropertiesTable() {
const [properties, setProperties] = useState([
{
id: 1,
title: 'فيلا فاخرة في المزة',
type: 'villa',
location: 'دمشق, المزة',
price: 500000,
commission: 5,
commissionType: 'من المالك',
bedrooms: 5,
bathrooms: 4,
area: 450,
status: 'available',
bookings: 3
},
{
id: 2,
title: 'شقة حديثة في الشهباء',
type: 'apartment',
location: 'حلب, الشهباء',
price: 250000,
commission: 7,
commissionType: 'من المستأجر',
bedrooms: 3,
bathrooms: 2,
area: 180,
status: 'booked',
bookings: 1
}
]);
const [viewModal, setViewModal] = useState({ isOpen: false, property: null });
const [editModal, setEditModal] = useState({ isOpen: false, property: null });
const [deleteModal, setDeleteModal] = useState({ isOpen: false, property: null });
const [moreMenu, setMoreMenu] = useState({ isOpen: false, property: null, anchorEl: null });
const formatCurrency = (amount) => {
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + ' ل.س';
};
const getStatusBadge = (status) => {
const styles = {
available: 'bg-green-100 text-green-800',
booked: 'bg-red-100 text-red-800',
maintenance: 'bg-yellow-100 text-yellow-800'
};
const labels = {
available: 'متاح',
booked: 'محجوز',
maintenance: 'صيانة'
};
return (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${styles[status]}`}>
{labels[status]}
</span>
);
};
const handleView = (property) => {
setViewModal({ isOpen: true, property });
};
const handleEdit = (property) => {
setEditModal({ isOpen: true, property });
};
const handleDelete = (property) => {
setDeleteModal({ isOpen: true, property });
};
const confirmDelete = () => {
if (deleteModal.property) {
setProperties(prev => prev.filter(p => p.id !== deleteModal.property.id));
setDeleteModal({ isOpen: false, property: null });
toast.success('تم حذف العقار بنجاح');
}
};
const handleSaveEdit = (updatedProperty) => {
setProperties(prev => prev.map(p =>
p.id === updatedProperty.id ? updatedProperty : p
));
toast.success('تم تحديث العقار بنجاح');
};
const handleMoreClick = (event, property) => {
event.stopPropagation();
setMoreMenu({ isOpen: true, property, anchorEl: event.currentTarget });
};
const handleViewBookings = (property) => {
toast.success(`جاري عرض حجوزات ${property.title}`);
};
const handleViewReports = (property) => {
toast.success(`جاري عرض تقرير أرباح ${property.title}`);
};
return (
<div className="overflow-x-auto">
<Toaster position="top-center" reverseOrder={false} />
<table className="w-full">
<thead className="bg-gray-50 border-b">
<tr>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-900">العقار</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-900">الموقع</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-900">السعر/يوم</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-900">العمولة</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-900">المصدر</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-900">التفاصيل</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-900">الحالة</th>
<th className="px-4 py-3 text-center text-sm font-semibold text-gray-900">الإجراءات</th>
</tr>
</thead>
<tbody className="divide-y">
{properties.map((property, index) => (
<motion.tr
key={property.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
className="hover:bg-gray-50"
>
<td className="px-4 py-3">
<div className="font-medium">{property.title}</div>
<div className="text-xs text-gray-500">
{property.type === 'villa' ? 'فيلا' :
property.type === 'apartment' ? 'شقة' :
property.type === 'house' ? 'بيت' : 'استوديو'}
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1 text-sm">
<MapPin className="w-3 h-3 text-gray-400" />
{property.location}
</div>
</td>
<td className="px-4 py-3 font-bold text-blue-600">
{formatCurrency(property.price)}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1">
<Percent className="w-3 h-3 text-amber-500" />
{property.commission}%
</div>
</td>
<td className="px-4 py-3 text-sm">{property.commissionType}</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2 text-xs">
<Bed className="w-3 h-3" /> {property.bedrooms}
<Bath className="w-3 h-3 mr-2" /> {property.bathrooms}
<Square className="w-3 h-3 mr-2" /> {property.area}
</div>
</td>
<td className="px-4 py-3">
{getStatusBadge(property.status)}
</td>
<td className="px-4 py-3 relative">
<div className="flex items-center justify-center gap-2">
<button
onClick={() => handleView(property)}
className="p-1 hover:bg-blue-100 rounded text-blue-600 transition-colors"
title="عرض التفاصيل"
>
<Eye className="w-4 h-4" />
</button>
<button
onClick={() => handleEdit(property)}
className="p-1 hover:bg-amber-100 rounded text-amber-600 transition-colors"
title="تعديل العقار"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(property)}
className="p-1 hover:bg-red-100 rounded text-red-600 transition-colors"
title="حذف العقار"
>
<Trash2 className="w-4 h-4" />
</button>
{moreMenu.isOpen && moreMenu.property?.id === property.id && (
<MoreActionsMenu
property={property}
isOpen={moreMenu.isOpen}
onClose={() => setMoreMenu({ isOpen: false, property: null, anchorEl: null })}
onViewBookings={handleViewBookings}
onViewReports={handleViewReports}
/>
)}
</div>
</td>
</motion.tr>
))}
</tbody>
</table>
{properties.length === 0 && (
<div className="text-center py-12">
<Home className="w-12 h-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500">لا توجد عقارات مضافة بعد</p>
</div>
)}
<PropertyViewModal
property={viewModal.property}
isOpen={viewModal.isOpen}
onClose={() => setViewModal({ isOpen: false, property: null })}
/>
<PropertyEditModal
property={editModal.property}
isOpen={editModal.isOpen}
onClose={() => setEditModal({ isOpen: false, property: null })}
onSave={handleSaveEdit}
/>
<DeleteConfirmationModal
isOpen={deleteModal.isOpen}
onClose={() => setDeleteModal({ isOpen: false, property: null })}
onConfirm={confirmDelete}
propertyTitle={deleteModal.property?.title}
/>
</div>
);
}

View File

@ -1,773 +0,0 @@
'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
User,
Mail,
Phone,
Calendar,
Home,
DollarSign,
Search,
Filter,
Eye,
X,
CheckCircle,
XCircle,
ChevronDown,
Users,
Award,
Clock,
TrendingUp,
CalendarDays,
Shield
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
const FilterDialog = ({ isOpen, onClose, filters, onApplyFilters, onResetFilters }) => {
const [localFilters, setLocalFilters] = useState({ ...filters });
const identityTypes = [
{ id: 'all', label: 'الكل' },
{ id: 'syrian', label: 'هوية سورية' },
{ id: 'passport', label: 'جواز سفر' }
];
const bookingRanges = [
{ id: 'all', label: 'الكل' },
{ id: '0-5', label: '0 - 5 حجوزات' },
{ id: '5-10', label: '5 - 10 حجوزات' },
{ id: '10-20', label: '10 - 20 حجوزات' },
{ id: '20+', label: 'أكثر من 20 حجز' }
];
const spendingRanges = [
{ id: 'all', label: 'الكل' },
{ id: '0-500000', label: 'أقل من 500,000 ل.س' },
{ id: '500000-1000000', label: '500,000 - 1,000,000 ل.س' },
{ id: '1000000-5000000', label: '1,000,000 - 5,000,000 ل.س' },
{ id: '5000000+', label: 'أكثر من 5,000,000 ل.س' }
];
const dateRanges = [
{ id: 'all', label: 'الكل' },
{ id: 'today', label: 'اليوم' },
{ id: 'week', label: 'آخر 7 أيام' },
{ id: 'month', label: 'آخر 30 يوم' },
{ id: 'year', label: 'آخر 12 شهر' }
];
const applyFilters = () => {
onApplyFilters(localFilters);
onClose();
toast.success('تم تطبيق الفلاتر بنجاح');
};
const resetFilters = () => {
const resetData = {
identityType: 'all',
minBookings: '',
maxBookings: '',
minSpending: '',
maxSpending: '',
dateRange: 'all',
activeOnly: false,
inactiveOnly: false
};
setLocalFilters(resetData);
onResetFilters();
onClose();
toast.success('تم إعادة تعيين الفلاتر');
};
if (!isOpen) return null;
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 bg-gradient-to-r from-blue-600 to-blue-700 p-6 text-white">
<div className="flex justify-between items-center">
<div>
<h2 className="text-xl font-bold flex items-center gap-2">
<Filter className="w-5 h-5" />
تصفية متقدمة
</h2>
<p className="text-blue-100 text-sm mt-1">حدد معايير التصفية المطلوبة</p>
</div>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full">
<X className="w-6 h-6" />
</button>
</div>
</div>
<div className="p-6 space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
نوع الهوية
</label>
<div className="grid grid-cols-3 gap-2">
{identityTypes.map((type) => (
<button
key={type.id}
onClick={() => setLocalFilters({...localFilters, identityType: type.id})}
className={`px-4 py-2 rounded-xl text-sm font-medium transition-all ${
localFilters.identityType === type.id
? 'bg-blue-600 text-white shadow-md'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{type.label}
</button>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
عدد الحجوزات
</label>
<div className="grid grid-cols-2 gap-2 mb-3">
<input
type="number"
placeholder="من"
value={localFilters.minBookings}
onChange={(e) => setLocalFilters({...localFilters, minBookings: e.target.value})}
className="px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500"
/>
<input
type="number"
placeholder="إلى"
value={localFilters.maxBookings}
onChange={(e) => setLocalFilters({...localFilters, maxBookings: e.target.value})}
className="px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex flex-wrap gap-2">
{bookingRanges.slice(1).map((range) => (
<button
key={range.id}
onClick={() => {
const [min, max] = range.id.split('-');
setLocalFilters({
...localFilters,
minBookings: min,
maxBookings: max === '5' ? '5' : max === '10' ? '10' : max === '20' ? '20' : '1000'
});
}}
className="px-3 py-1 text-xs bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200"
>
{range.label}
</button>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
إجمالي الإنفاق (ل.س)
</label>
<div className="grid grid-cols-2 gap-2 mb-3">
<input
type="number"
placeholder="من"
value={localFilters.minSpending}
onChange={(e) => setLocalFilters({...localFilters, minSpending: e.target.value})}
className="px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500"
/>
<input
type="number"
placeholder="إلى"
value={localFilters.maxSpending}
onChange={(e) => setLocalFilters({...localFilters, maxSpending: e.target.value})}
className="px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex flex-wrap gap-2">
{spendingRanges.slice(1).map((range) => (
<button
key={range.id}
onClick={() => {
const [min, max] = range.id.split('-');
setLocalFilters({
...localFilters,
minSpending: min,
maxSpending: max === '500000' ? '500000' : max === '1000000' ? '1000000' : max === '5000000' ? '5000000' : '999999999'
});
}}
className="px-3 py-1 text-xs bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200"
>
{range.label}
</button>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
فترة التسجيل
</label>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{dateRanges.map((range) => (
<button
key={range.id}
onClick={() => setLocalFilters({...localFilters, dateRange: range.id})}
className={`px-3 py-2 rounded-xl text-sm font-medium transition-all ${
localFilters.dateRange === range.id
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{range.label}
</button>
))}
</div>
</div>
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={localFilters.activeOnly}
onChange={(e) => setLocalFilters({...localFilters, activeOnly: e.target.checked, inactiveOnly: false})}
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">مستخدمون لديهم حجوزات نشطة فقط</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={localFilters.inactiveOnly}
onChange={(e) => setLocalFilters({...localFilters, inactiveOnly: e.target.checked, activeOnly: false})}
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">مستخدمون بدون حجوزات نشطة</span>
</label>
</div>
</div>
<div className="sticky bottom-0 bg-gray-50 border-t p-4 flex gap-3">
<button
onClick={resetFilters}
className="flex-1 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors"
>
إعادة تعيين
</button>
<button
onClick={applyFilters}
className="flex-1 bg-blue-600 text-white py-3 rounded-xl font-medium hover:bg-blue-700 transition-colors"
>
تطبيق الفلاتر
</button>
</div>
</motion.div>
</motion.div>
);
};
const UserDetailsModal = ({ user, isOpen, onClose }) => {
if (!isOpen || !user) return null;
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ل.س';
};
const userBookings = [
{
id: 'BK001',
property: 'فيلا فاخرة في المزة',
startDate: '2024-03-10',
endDate: '2024-03-15',
amount: 2500000,
status: 'completed'
},
{
id: 'BK002',
property: 'شقة حديثة في الشهباء',
startDate: '2024-02-20',
endDate: '2024-02-25',
amount: 1250000,
status: 'completed'
},
{
id: 'BK003',
property: 'بيت عائلي في بابا عمرو',
startDate: '2024-04-01',
endDate: '2024-04-10',
amount: 3500000,
status: 'confirmed'
}
];
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white rounded-2xl w-full max-w-3xl max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 bg-gradient-to-r from-blue-600 to-blue-700 p-6 text-white">
<div className="flex justify-between items-center">
<div>
<h2 className="text-xl font-bold flex items-center gap-2">
<User className="w-5 h-5" />
تفاصيل المستخدم
</h2>
<p className="text-blue-100 text-sm mt-1">{user.name}</p>
</div>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full">
<X className="w-6 h-6" />
</button>
</div>
</div>
<div className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
<User className="w-4 h-4 text-blue-500" />
معلومات شخصية
</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-500">الاسم الكامل:</span>
<span className="font-medium">{user.name}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">البريد الإلكتروني:</span>
<span className="font-medium">{user.email}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">رقم الهاتف:</span>
<span className="font-medium">{user.phone}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">تاريخ التسجيل:</span>
<span className="font-medium">{user.joinDate}</span>
</div>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
<Shield className="w-4 h-4 text-blue-500" />
معلومات الهوية
</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-500">نوع الهوية:</span>
<span className="font-medium">
{user.identityType === 'syrian' ? 'هوية سورية' : 'جواز سفر'}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">رقم الهوية:</span>
<span className="font-medium">{user.identityNumber}</span>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-blue-50 p-4 rounded-xl text-center">
<div className="text-2xl font-bold text-blue-600">{user.totalBookings}</div>
<div className="text-sm text-gray-600">إجمالي الحجوزات</div>
</div>
<div className="bg-green-50 p-4 rounded-xl text-center">
<div className="text-2xl font-bold text-green-600">{user.activeBookings}</div>
<div className="text-sm text-gray-600">حجوزات نشطة</div>
</div>
<div className="bg-amber-50 p-4 rounded-xl text-center">
<div className="text-2xl font-bold text-amber-600">{formatCurrency(user.totalSpent)}</div>
<div className="text-sm text-gray-600">إجمالي المنصرف</div>
</div>
</div>
<div>
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
<Calendar className="w-4 h-4 text-blue-500" />
سجل الحجوزات
</h3>
<div className="space-y-3">
{userBookings.map((booking) => (
<div key={booking.id} className="bg-gray-50 p-4 rounded-xl flex flex-col md:flex-row justify-between items-start md:items-center gap-3">
<div>
<p className="font-medium text-gray-900">{booking.property}</p>
<div className="flex items-center gap-2 text-sm text-gray-500 mt-1">
<CalendarDays className="w-3 h-3" />
{booking.startDate} - {booking.endDate}
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<div className="text-lg font-bold text-amber-600">{formatCurrency(booking.amount)}</div>
<div className="text-xs text-gray-500">المبلغ الإجمالي</div>
</div>
<span className={`px-2 py-1 rounded-lg text-xs font-medium ${
booking.status === 'completed'
? 'bg-green-100 text-green-800'
: 'bg-blue-100 text-blue-800'
}`}>
{booking.status === 'completed' ? 'مكتمل' : 'مؤكد'}
</span>
</div>
</div>
))}
{userBookings.length === 0 && (
<div className="text-center py-8 text-gray-500">
<Calendar className="w-12 h-12 text-gray-300 mx-auto mb-2" />
<p>لا توجد حجوزات سابقة</p>
</div>
)}
</div>
</div>
</div>
<div className="sticky bottom-0 bg-gray-50 border-t p-4 flex gap-3">
</div>
</motion.div>
</motion.div>
);
};
export default function UsersList() {
const [users, setUsers] = useState([
{
id: 1,
name: 'أحمد محمد',
email: 'ahmed@example.com',
phone: '0938123456',
identityType: 'syrian',
identityNumber: '123456789',
joinDate: '2024-01-15',
totalBookings: 3,
activeBookings: 1,
totalSpent: 1500000
},
{
id: 2,
name: 'سارة أحمد',
email: 'sara@example.com',
phone: '0945123789',
identityType: 'passport',
identityNumber: 'AB123456',
joinDate: '2024-02-10',
totalBookings: 2,
activeBookings: 0,
totalSpent: 500000
}
]);
const [searchTerm, setSearchTerm] = useState('');
const [selectedUser, setSelectedUser] = useState(null);
const [showFilterDialog, setShowFilterDialog] = useState(false);
const [filters, setFilters] = useState({
identityType: 'all',
minBookings: '',
maxBookings: '',
minSpending: '',
maxSpending: '',
dateRange: 'all',
activeOnly: false,
inactiveOnly: false
});
const applyFilters = (newFilters) => {
setFilters(newFilters);
};
const resetFilters = () => {
setFilters({
identityType: 'all',
minBookings: '',
maxBookings: '',
minSpending: '',
maxSpending: '',
dateRange: 'all',
activeOnly: false,
inactiveOnly: false
});
setSearchTerm('');
};
const filteredUsers = users.filter(user => {
if (searchTerm && !user.name.includes(searchTerm) && !user.email.includes(searchTerm) && !user.phone.includes(searchTerm)) {
return false;
}
if (filters.identityType !== 'all' && user.identityType !== filters.identityType) {
return false;
}
if (filters.minBookings && user.totalBookings < parseInt(filters.minBookings)) {
return false;
}
if (filters.maxBookings && user.totalBookings > parseInt(filters.maxBookings)) {
return false;
}
if (filters.minSpending && user.totalSpent < parseInt(filters.minSpending)) {
return false;
}
if (filters.maxSpending && user.totalSpent > parseInt(filters.maxSpending)) {
return false;
}
if (filters.activeOnly && user.activeBookings === 0) {
return false;
}
if (filters.inactiveOnly && user.activeBookings > 0) {
return false;
}
if (filters.dateRange !== 'all') {
const joinDate = new Date(user.joinDate);
const today = new Date();
const diffDays = Math.floor((today - joinDate) / (1000 * 60 * 60 * 24));
switch(filters.dateRange) {
case 'today':
if (joinDate.toDateString() !== today.toDateString()) return false;
break;
case 'week':
if (diffDays > 7) return false;
break;
case 'month':
if (diffDays > 30) return false;
break;
case 'year':
if (diffDays > 365) return false;
break;
}
}
return true;
});
const filterStats = {
total: filteredUsers.length,
filtered: filteredUsers.length !== users.length
};
const getActiveFiltersCount = () => {
let count = 0;
if (filters.identityType !== 'all') count++;
if (filters.minBookings || filters.maxBookings) count++;
if (filters.minSpending || filters.maxSpending) count++;
if (filters.dateRange !== 'all') count++;
if (filters.activeOnly || filters.inactiveOnly) count++;
return count;
};
return (
<div className="space-y-4">
<Toaster position="top-center" reverseOrder={false} />
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1 relative">
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
placeholder="بحث عن مستخدم بالاسم أو البريد أو الهاتف..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pr-12 px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex gap-2">
<button
onClick={() => setShowFilterDialog(true)}
className={`px-5 py-3 rounded-xl font-medium flex items-center gap-2 transition-all ${
getActiveFiltersCount() > 0
? 'bg-blue-600 text-white hover:bg-blue-700'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
<Filter className="w-5 h-5" />
تصفية متقدمة
{getActiveFiltersCount() > 0 && (
<span className="ml-1 bg-white text-blue-600 rounded-full w-5 h-5 text-xs flex items-center justify-center">
{getActiveFiltersCount()}
</span>
)}
</button>
{filterStats.filtered && (
<button
onClick={resetFilters}
className="px-5 py-3 bg-gray-100 text-gray-700 rounded-xl font-medium hover:bg-gray-200 transition-colors flex items-center gap-2"
>
<X className="w-4 h-4" />
إعادة تعيين
</button>
)}
</div>
</div>
{getActiveFiltersCount() > 0 && (
<div className="flex flex-wrap gap-2 p-3 bg-blue-50 rounded-xl">
<span className="text-sm text-blue-800 font-medium">الفلاتر النشطة:</span>
{filters.identityType !== 'all' && (
<span className="px-2 py-1 bg-blue-100 text-blue-700 rounded-lg text-xs">
{filters.identityType === 'syrian' ? 'هوية سورية' : 'جواز سفر'}
</span>
)}
{(filters.minBookings || filters.maxBookings) && (
<span className="px-2 py-1 bg-blue-100 text-blue-700 rounded-lg text-xs">
الحجوزات: {filters.minBookings || '0'} - {filters.maxBookings || '∞'}
</span>
)}
{(filters.minSpending || filters.maxSpending) && (
<span className="px-2 py-1 bg-blue-100 text-blue-700 rounded-lg text-xs">
الإنفاق: {parseInt(filters.minSpending || 0).toLocaleString()} - {parseInt(filters.maxSpending || '∞').toLocaleString()} ل.س
</span>
)}
{filters.dateRange !== 'all' && (
<span className="px-2 py-1 bg-blue-100 text-blue-700 rounded-lg text-xs">
{filters.dateRange === 'today' ? 'اليوم' :
filters.dateRange === 'week' ? 'آخر 7 أيام' :
filters.dateRange === 'month' ? 'آخر 30 يوم' : 'آخر 12 شهر'}
</span>
)}
{filters.activeOnly && (
<span className="px-2 py-1 bg-green-100 text-green-700 rounded-lg text-xs">
لديهم حجوزات نشطة
</span>
)}
{filters.inactiveOnly && (
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs">
بدون حجوزات نشطة
</span>
)}
</div>
)}
<div className="flex justify-between items-center">
<div className="text-sm text-gray-600">
عرض <span className="font-bold text-gray-900">{filteredUsers.length}</span> مستخدم
{filterStats.filtered && (
<span className="text-gray-500 mr-1">(من {users.length})</span>
)}
</div>
</div>
<div className="space-y-3">
{filteredUsers.map((user, index) => (
<motion.div
key={user.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
className="bg-white border border-gray-200 rounded-xl p-5 hover:shadow-md transition-all"
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-gradient-to-br from-blue-500 to-blue-600 rounded-full flex items-center justify-center text-white text-xl font-bold shadow-lg">
{user.name.charAt(0).toUpperCase()}
</div>
<div>
<h3 className="font-bold text-gray-900 text-lg">{user.name}</h3>
<div className="flex flex-wrap gap-3 mt-1 text-sm text-gray-500">
<div className="flex items-center gap-1">
<Mail className="w-4 h-4" />
{user.email}
</div>
<div className="flex items-center gap-1">
<Phone className="w-4 h-4" />
{user.phone}
</div>
<div className="flex items-center gap-1">
<Shield className="w-4 h-4" />
{user.identityType === 'syrian' ? 'هوية سورية' : 'جواز سفر'}
</div>
</div>
</div>
</div>
<div className="flex gap-6">
<div className="text-center min-w-[80px]">
<div className="text-xl font-bold text-blue-600">{user.totalBookings}</div>
<div className="text-xs text-gray-500">إجمالي الحجوزات</div>
</div>
<div className="text-center min-w-[80px]">
<div className={`text-xl font-bold ${user.activeBookings > 0 ? 'text-green-600' : 'text-gray-400'}`}>
{user.activeBookings}
</div>
<div className="text-xs text-gray-500">حجوزات نشطة</div>
</div>
<div className="text-center min-w-[100px]">
<div className="text-xl font-bold text-amber-600">
{user.totalSpent.toLocaleString()}
</div>
<div className="text-xs text-gray-500">إجمالي المنصرف</div>
</div>
</div>
<button
onClick={() => setSelectedUser(user)}
className="px-4 py-2 bg-blue-600 text-white rounded-xl text-sm font-medium hover:bg-blue-700 transition-colors flex items-center gap-2"
>
<Eye className="w-4 h-4" />
عرض التفاصيل
</button>
</div>
</motion.div>
))}
</div>
{filteredUsers.length === 0 && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="text-center py-16 bg-white rounded-2xl border-2 border-dashed border-gray-300"
>
<Users className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد نتائج</h3>
<p className="text-gray-500">لا يوجد مستخدمون يطابقون معايير البحث</p>
{(searchTerm || getActiveFiltersCount() > 0) && (
<button
onClick={resetFilters}
className="mt-4 px-6 py-2 bg-blue-600 text-white rounded-xl text-sm font-medium hover:bg-blue-700"
>
إعادة تعيين الفلاتر
</button>
)}
</motion.div>
)}
<FilterDialog
isOpen={showFilterDialog}
onClose={() => setShowFilterDialog(false)}
filters={filters}
onApplyFilters={applyFilters}
onResetFilters={resetFilters}
/>
<UserDetailsModal
user={selectedUser}
isOpen={!!selectedUser}
onClose={() => setSelectedUser(null)}
/>
</div>
);
}

View File

@ -0,0 +1,28 @@
import { motion } from "framer-motion";
export default function ArrowForScroll() {
return (
<>
<motion.div
className="absolute bottom-8 left-1/2 transform -translate-x-1/2 cursor-pointer"
animate={{
y: [0, 10, 0],
}}
transition={{
duration: 1.5,
repeat: Infinity,
ease: "easeInOut",
}}
onClick={() =>
window.scrollTo({
top: window.innerHeight,
behavior: "smooth",
})
}
>
<svg className="w-6 h-6 text-amber-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>
</motion.div>
</>
);
}

View File

@ -0,0 +1,21 @@
import { Lock, ShieldCheck, Zap } from "lucide-react";
import FeaturesOurCompany from "./FeaturesOurCompany";
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
export default function FeatureSection() {
const { t } = useTranslation();
return (
<>
<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: false }} transition={{ duration: 0.6 }} className="text-center mt-5">
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4 tracking-tight">{t("whyChooseUsTitle")}</h2>
<p className="text-gray-600 max-w-2xl mx-auto text-lg">{t("whyChooseUsSubtitle")}</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-5 p-5">
<FeaturesOurCompany icon={<ShieldCheck className="w-6 h-6 text-amber-600" />} title={t("feature1Title")} discripction={t("feature1Description")} />
<FeaturesOurCompany icon={<Lock className="w-6 h-6 text-blue-600" />} title={t("feature2Title")} discripction={t("feature2Description")} />
<FeaturesOurCompany icon={<Zap className="w-6 h-6 text-green-600" />} title={t("feature3Title")} discripction={t("feature3Description")} />
</div>
</>
);
}

View File

@ -0,0 +1,21 @@
import { motion } from "framer-motion";
export default function FeaturesOurCompany({ icon, title, discripction }) {
return (
<>
<motion.div
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: false }}
transition={{ duration: 0.5, delay: 0.1 }}
whileHover={{ y: -4 }}
>
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center group-hover:bg-amber-200 transition-colors duration-300">{icon}</div>
<h3 className="text-lg font-bold text-gray-900">{title}</h3>
</div>
<p className="text-gray-600 text-sm leading-relaxed">{discripction}</p>
</motion.div>
</>
);
}

View File

@ -0,0 +1,32 @@
export default function FilterSelect({ onChange, icon, value, option, label }) {
return (
<>
<div>
<label className="block text-sm font-medium text-amber-50 mb-2">
<div className="flex items-center gap-1">
{icon??""}
{label}
</div>
</label>
<select
value={value}
onChange={onChange}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: "no-repeat",
backgroundPosition: "left 1rem center",
backgroundSize: "1rem",
paddingLeft: "2.5rem",
}}
>
{option.map((opt) => (
<option key={opt.id} value={opt.id}>
{opt.label}
</option>
))}
</select>
</div>
</>
);
}

View File

@ -1,192 +1,117 @@
'use client'; "use client";
import { useState } from "react";
import { useState } from 'react'; import { motion } from "framer-motion";
import { motion } from 'framer-motion'; import { useTranslation } from "react-i18next";
import { useTranslation } from 'react-i18next'; import { Search, MapPin, Home, DollarSign } from "lucide-react";
import { Search, MapPin, Home, DollarSign } from 'lucide-react'; import { useFilterOptions } from "@/app/hooks/UseFilterOfHeroSearch";
import ShowLoginDialog from "./showLoginDialog";
import FilterSelect from "./FilterSelect";
export default function HeroSearch({ onSearch }) { export default function HeroSearch({ onSearch }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('rent'); const { cities, identityTypes, ownerSources, priceRanges, propertyTypes, rentPeriods } = useFilterOptions();
const [activeTab, setActiveTab] = useState("buy");
const [filters, setFilters] = useState({ const [filters, setFilters] = useState({
city: '', city: "all",
propertyType: '', propertyType: "all",
priceRange: '', priceRange: "all",
identityType: 'syrian' identityType: "syrian",
ownerSource: "all",
rentPeriod: "all",
availableToday: false,
}); });
const [showLoginDialog, setShowLoginDialog] = useState(false);
const cities = [ const handleTabClick = (tab) => {
{ id: 'all', label: 'جميع المدن' }, setActiveTab(tab);
{ id: 'دمشق', label: 'دمشق' }, };
{ id: 'حلب', label: 'حلب' },
{ id: 'حمص', label: 'حمص' },
{ id: 'اللاذقية', label: 'اللاذقية' },
{ id: 'درعا', label: 'درعا' }
];
const propertyTypes = [
{ id: 'all', label: 'الكل' },
{ id: 'apartment', label: 'شقة' },
{ id: 'villa', label: 'فيلا' },
{ id: 'house', label: 'بيت' },
{ id: 'studio', label: 'استوديو' }
];
const priceRanges = [
{ id: 'all', label: 'جميع الأسعار' },
{ id: '0-500', label: 'أقل من 50$' },
{ id: '500-1000', label: '50$ - 100$' },
{ id: '1000-2000', label: '100$ - 200$' },
{ id: '2000-3000', label: '200$ - 300$' },
{ id: '3000+', label: 'أكثر من 300$' }
];
const identityTypes = [
{ id: 'syrian', label: 'هوية سورية' },
{ id: 'passport', label: 'جواز سفر' }
];
const handleSearch = () => { const handleSearch = () => {
onSearch({ onSearch({
...filters, ...filters,
propertyType: filters.propertyType || 'all', mode: activeTab,
city: filters.city || 'all', city: filters.city || "all",
priceRange: filters.priceRange || 'all' propertyType: filters.propertyType || "all",
priceRange: filters.priceRange || "all",
ownerSource: filters.ownerSource || "all",
rentPeriod: filters.rentPeriod || "all",
}); });
}; };
return ( return (
<motion.div <>
className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 sm:p-8 border border-white/20 shadow-2xl" <motion.div
initial={{ opacity: 0, y: 30 }} className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 sm:p-8 border border-white/20 shadow-2xl"
animate={{ opacity: 1, y: 0 }} initial={{ opacity: 0, y: 30 }}
transition={{ duration: 0.8, delay: 0.5 }} animate={{ opacity: 1, y: 0 }}
> transition={{ duration: 0.8, delay: 0.5 }}
<div className="flex flex-wrap gap-2 mb-8"> >
{['rent', 'buy', 'sell'].map((tab) => ( <div className="flex flex-wrap gap-2 mb-8">
<motion.button {["rent", "buy", "sell"].map((tab) => {
key={tab} const isActive = activeTab === tab;
onClick={() => setActiveTab(tab)}
className={`px-4 py-2 rounded-lg font-medium text-sm transition-all ${ return (
activeTab === tab <button
? 'bg-amber-500 text-white' key={tab}
: 'bg-white/20 text-white hover:bg-white/30' onClick={() => handleTabClick(tab)}
}`} className={`relative px-5 py-2.5 rounded-lg font-medium text-sm transition-colors duration-200 outline-none select-none ${isActive ? "text-white" : "text-amber-50 hover:text-white"}`}
whileHover={{ scale: 1.05 }} >
whileTap={{ scale: 0.95 }} {isActive && <motion.div layoutId="activeTabBackground" className="absolute inset-0 bg-amber-500 rounded-lg shadow-md" transition={{ type: "spring", stiffness: 500, damping: 35 }} />}
> <span className="relative z-10">{t(`${tab}Tab`)}</span>
{t(`${tab}Tab`)} </button>
</motion.button> );
))} })}
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-white mb-2">
<div className="flex items-center gap-1">
<MapPin className="w-4 h-4" />
{t("cityStreetLabel")}
</div>
</label>
<select
value={filters.city}
onChange={(e) => setFilters({...filters, city: e.target.value})}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'left 1rem center',
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{cities.map(city => (
<option key={city.id} value={city.id}>{city.label}</option>
))}
</select>
</div> </div>
<div> {/* city */}
<label className="block text-sm font-medium text-white mb-2"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="flex items-center gap-1"> <FilterSelect icon={<MapPin className="w-4 h-4" />} value={filters.city} onChange={(e) => setFilters({ ...filters, city: e.target.value })} option={cities} label={t(t("cityStreetLabel"))} />
<Home className="w-4 h-4" /> {activeTab === "rent" && (
{t("rentTypeLabel")} <FilterSelect
</div> icon={<Home className="w-4 h-4" />}
</label> value={filters.propertyType}
<select onChange={(e) => setFilters({ ...filters, city: e.target.value })}
value={filters.propertyType} option={propertyTypes}
onChange={(e) => setFilters({...filters, propertyType: e.target.value})} label={t("rentTypeLabel")}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer" />
style={{ )}
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`, {/* price */}
backgroundRepeat: 'no-repeat', <FilterSelect
backgroundPosition: 'left 1rem center', icon={<DollarSign className="w-4 h-4" />}
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{propertyTypes.map(type => (
<option key={type.id} value={type.id}>{type.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
<div className="flex items-center gap-1">
<DollarSign className="w-4 h-4" />
{t("priceLabel")}
</div>
</label>
<select
value={filters.priceRange} value={filters.priceRange}
onChange={(e) => setFilters({...filters, priceRange: e.target.value})} onChange={(e) => setFilters({ ...filters, priceRange: e.target.value })}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer" option={priceRanges}
style={{ label={t("priceLabel")}
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`, />
backgroundRepeat: 'no-repeat', {/* type */}
backgroundPosition: 'left 1rem center', <FilterSelect value={filters.identityType} onChange={(e) => setFilters({ ...filters, identityType: e.target.value })} option={identityTypes} label={t("identityTypeLabel")} />
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{priceRanges.map(range => (
<option key={range.id} value={range.id}>{range.label}</option>
))}
</select>
</div> </div>
<div> <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4">
<label className="block text-sm font-medium text-white mb-2"> {/* owener */}
<div className="flex items-center gap-1"> <FilterSelect value={filters.ownerSource} onChange={(e) => setFilters({ ...filters, ownerSource: e.target.value })} option={ownerSources} label={t("ownerSourceLabel")} />
نوع الوثيقة {activeTab === "rent" && <FilterSelect value={filters.rentPeriod} onChange={(e) => setFilters({ ...filters, rentPeriod: e.target.value })} option={rentPeriods} label={t("rentTypeLabel")} />}
</div> <div className={`${activeTab === "rent" ? "md:col-span-2" : "md:col-span-3"} flex flex-col justify-between p-4 rounded-2xl border border-dashed border-white/30 bg-white/5`}>
</label> {/* checkbox */}
<select <label className="mt-4 flex items-center gap-3 text-amber-50 text-sm">
value={filters.identityType} <input
onChange={(e) => setFilters({...filters, identityType: e.target.value})} type="checkbox"
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer" checked={filters.availableToday}
style={{ onChange={(e) => setFilters({ ...filters, availableToday: e.target.checked })}
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`, className="w-5 h-5 text-amber-500 rounded border-gray-300 bg-white"
backgroundRepeat: 'no-repeat', />
backgroundPosition: 'left 1rem center', <span className="font-medium">{t("availableToday")}</span>
backgroundSize: '1rem', </label>
paddingLeft: '2.5rem' </div>
}}
>
{identityTypes.map(type => (
<option key={type.id} value={type.id}>{type.label}</option>
))}
</select>
</div> </div>
</div> <div className="mt-6">
<div className="mt-6"> <motion.button
<motion.button onClick={handleSearch}
onClick={handleSearch} className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-4 px-6 rounded-xl transition-all duration-300 flex items-center justify-center text-base gap-3 shadow-lg hover:shadow-xl"
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-4 px-6 rounded-xl transition-all duration-300 flex items-center justify-center text-base gap-3 shadow-lg hover:shadow-xl" whileHover={{ scale: 1.02 }}
whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}
whileTap={{ scale: 0.98 }} >
> <Search className="w-5 h-5" />
<Search className="w-5 h-5" /> {t("searchButton")}
{t("searchButton")} </motion.button>
</motion.button> </div>
</div> </motion.div>
</motion.div> {showLoginDialog && !isAuthenticated && <ShowLoginDialog setShowLoginDialog={setShowLoginDialog} />}
</>
); );
} }

View File

@ -0,0 +1,54 @@
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
export default function HeroSection() {
const {t} =useTranslation()
return (
<>
<motion.div
className="text-center mb-12"
initial="hidden"
animate="visible"
variants={{
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.2 },
},
}}
>
<motion.h1
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-amber-50 mb-6 leading-tight tracking-tight"
variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
}}
>
{t("heroTitleLine1")}
<br />
<motion.span
className="text-amber-300"
animate={{
y: [0, -10, 0],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
>
{t("heroTitleLine2")}
</motion.span>
</motion.h1>
<motion.p
className="text-base sm:text-lg text-amber-50 max-w-2xl mx-auto leading-relaxed"
variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
}}
>
{t("heroSubtitle")}
</motion.p>
</motion.div>
</>
);
}

View File

@ -0,0 +1,71 @@
"use client";
import { useState, useRef } from "react";
import { AnimatePresence } from "framer-motion";
import HeroSearch from "./HeroSearch";
import ShowIfOwner from "./ShowIfOwner";
import MapSection from "./MapSection";
import useApplyFilters from "../../hooks/useApplyFilters";
import HeroSection from "./HeroSection";
import FeatureSection from "./FeatureSection";
import ArrowForScroll from "./ArrowForScroll";
import useAuth from "@/app/hooks/useAuth";
export default function HomeClient() {
const mapSectionRef = useRef(null);
const [showMap, setShowMap] = useState(false);
const [isScrolling, setIsScrolling] = useState(false);
const { filteredProperties, applyFilters, resetFilters } = useApplyFilters();
const { name, isOwner, isAuthenticated, role } = useAuth();
const handleSearch = (filters) => {
applyFilters(filters);
if (!showMap) {
setShowMap(true);
setTimeout(() => {
scrollToMap();
}, 300);
} else {
scrollToMap();
}
};
const scrollToMap = () => {
if (mapSectionRef.current) {
setIsScrolling(true);
mapSectionRef.current.scrollIntoView({
behavior: "smooth",
block: "center",
});
setTimeout(() => setIsScrolling(false), 1000);
}
};
const resetSearch = () => {
setShowMap(false);
resetFilters();
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
return (
<div className="min-h-screen">
<section className="relative min-h-screen flex items-center justify-center overflow-hidden">
<div className="relative z-10 container mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-6xl mx-auto">
<HeroSection />
{!isOwner && <HeroSearch onSearch={handleSearch} />}
{isOwner && <ShowIfOwner name={name} />}
</div>
</div>
{!showMap && !isOwner && <ArrowForScroll />}
</section>
{!isOwner && (
<>
<AnimatePresence mode="wait">
{showMap && <MapSection mapSectionRef={mapSectionRef} isScrolling={isScrolling} resetSearch={resetSearch} filteredProperties={filteredProperties} />}
</AnimatePresence>
</>
)}
<FeatureSection />
</div>
);
}

View File

@ -0,0 +1,80 @@
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import PropertyMapWithMarkers from "../PropertyMapWithMarkers";
export default function MapSection({ mapSectionRef, isScrolling, resetSearch, filteredProperties }) {
const { t } = useTranslation();
return (
<>
<motion.section
ref={mapSectionRef}
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -50 }}
transition={{
type: "spring",
damping: 20,
stiffness: 100,
duration: 0.6,
}}
className="py-12 bg-gray-50 relative"
>
{isScrolling && <motion.div className="absolute top-0 left-0 right-0 h-1 bg-amber-500 z-10" initial={{ scaleX: 0 }} animate={{ scaleX: 1 }} transition={{ duration: 1, ease: "easeInOut" }} />}
<div className="container mx-auto px-4">
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }} className="text-center mb-8">
<div className="flex items-center justify-center gap-4 mb-2">
<h2 className="text-3xl font-bold text-gray-900">{filteredProperties.length > 0 ? t("searchResults") : t("no-properties")}</h2>
<motion.button
onClick={resetSearch}
className="px-4 py-2 bg-white border border-gray-300 rounded-full text-sm font-medium text-gray-700 hover:bg-gray-50 shadow-sm flex items-center gap-2"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
{t("newSearch")}
</motion.button>
</div>
{filteredProperties.length > 0 ? (
<p className="text-gray-600">{t("foundPropertiesCount", { count: filteredProperties.length })}</p>
) : (
<p className="text-gray-600">{t("noPropertiesMatchFilters")}</p>
)}
</motion.div>
<motion.div
className="bg-white rounded-2xl shadow-xl overflow-hidden border border-gray-200"
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.3, type: "spring" }}
>
{filteredProperties.length > 0 ? (
<PropertyMapWithMarkers
properties={filteredProperties.map((p) => ({
...p,
lat: p.location.lat,
lng: p.location.lng,
address: p.location.address,
}))}
onPropertyClick={() => {}}
/>
) : (
<div className="h-[400px] flex flex-col items-center justify-center bg-gray-50">
<div className="w-24 h-24 bg-gray-200 rounded-full flex items-center justify-center mb-4">
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("no-properties")}</h3>
<p className="text-gray-500">{t("try-changing-search-criteria")}</p>
</div>
)}
</motion.div>
</div>
</motion.section>
</>
);
}

View File

@ -0,0 +1,26 @@
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { Building } from "lucide-react";
export default function ShowIfOwner({ name }) {
const { t } = useTranslation();
return (
<>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 text-center border border-white/20"
>
<h2 className="text-2xl font-bold text-amber-50 mb-2">
{t("ownerGreeting")} {name ?? " notFound "}!
</h2>
<p className="text-amber-50/80 mb-4">{t("ownerDescription")}</p>
<Link href="/owner/properties" className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors">
<Building className="w-5 h-5" />
{t("manageMyProperties")}
</Link>
</motion.div>
</>
);
}

View File

@ -0,0 +1,38 @@
import { ShieldCheck } from "lucide-react";
import Link from "next/link";
import { useTranslation } from "react-i18next";
export default function ShowLoginDialog({setShowLoginDialog}) {
const {t}=useTranslation()
return (
<>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 px-4 py-8">
<div className="w-full max-w-md rounded-3xl bg-white p-6 shadow-2xl border border-gray-200">
<div className="flex items-center gap-3 mb-5">
<ShieldCheck className="w-7 h-7 text-amber-500" />
<div>
<h3 className="text-lg font-semibold text-gray-900">{t("loginRequired")}</h3>
<p className="text-sm text-gray-600">{t("loginRequiredDesc")}</p>
</div>
</div>
<div className="space-y-4">
<div className="rounded-2xl bg-gray-50 p-4">
<p className="text-sm text-gray-700">{t("loginPrompt")}</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
<button
type="button"
onClick={() => setShowLoginDialog(false)}
className="w-full sm:w-auto px-5 py-3 rounded-xl border border-gray-300 text-gray-700 hover:bg-gray-100 transition-colors"
>
{t("close")}
</button>
<Link href="/login" className="w-full sm:w-auto px-5 py-3 rounded-xl bg-amber-500 text-white font-semibold text-center hover:bg-amber-600 transition-colors">
{t("login")}
</Link>
</div>
</div>
</div>
</div>
</>
);
}

View File

@ -0,0 +1,85 @@
'use client';
import { useEffect, useRef } from 'react';
export default function AddPropertyMap({ mapCenter, mapZoom, selectedLocation, onMapClick, onMarkerDragEnd }) {
const containerRef = useRef(null);
const mapRef = useRef(null);
const markerRef = useRef(null);
const LRef = useRef(null);
useEffect(() => {
async function init() {
const L = await import('leaflet');
LRef.current = L;
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
});
if (!containerRef.current || mapRef.current) return;
const map = L.map(containerRef.current, {
center: mapCenter,
zoom: mapZoom,
doubleClickZoom: false,
});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors',
}).addTo(map);
map.on('click', (e) => {
const pos = [e.latlng.lat, e.latlng.lng];
placeMarker(pos);
map.setView(pos, mapZoom);
onMapClick(pos);
});
mapRef.current = map;
setTimeout(() => map.invalidateSize(), 200);
}
init();
return () => {
if (mapRef.current) {
mapRef.current.remove();
mapRef.current = null;
}
};
}, []);
function placeMarker(pos) {
const L = LRef.current;
if (!L || !mapRef.current) return;
if (markerRef.current) {
markerRef.current.setLatLng(pos);
} else {
markerRef.current = L.marker(pos, { draggable: true }).addTo(mapRef.current);
markerRef.current.on('dragend', (ev) => {
const p = ev.target.getLatLng();
onMarkerDragEnd(p.lat, p.lng);
});
}
}
useEffect(() => {
const map = mapRef.current;
if (!map) return;
map.setView(mapCenter, mapZoom);
}, [mapCenter, mapZoom]);
useEffect(() => {
if (!selectedLocation || !mapRef.current) return;
const pos = [selectedLocation.lat, selectedLocation.lng];
mapRef.current.setView(pos, mapZoom);
placeMarker(pos);
}, [selectedLocation]);
return <div ref={containerRef} className="w-full h-full z-0" />;
}

View File

@ -0,0 +1,246 @@
"use client";
import { useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { ChevronRight, ChevronLeft } from "lucide-react";
const MONTH_KEYS = [
"month.january",
"month.february",
"month.march",
"month.april",
"month.may",
"month.june",
"month.july",
"month.august",
"month.september",
"month.october",
"month.november",
"month.december",
];
const DAY_KEYS = [
"dayAbbr.sun",
"dayAbbr.mon",
"dayAbbr.tue",
"dayAbbr.wed",
"dayAbbr.thu",
"dayAbbr.fri",
"dayAbbr.sat",
];
export default function DateSelectionCalendar({
mode,
availableDates,
selectedStart,
selectedEnd,
onSelectStart,
onSelectEnd,
}) {
const { t } = useTranslation();
const [calendarMonth, setCalendarMonth] = useState(() => new Date().getMonth());
const [calendarYear, setCalendarYear] = useState(() => new Date().getFullYear());
const [rangeError, setRangeError] = useState(null);
const isPastDate = useCallback((dateStr) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
return new Date(dateStr) < today;
}, []);
const isDateAvailable = useCallback(
(dateStr) => (availableDates ? availableDates.has(dateStr) : true),
[availableDates]
);
const isMonthFullyAvailable = useCallback(
(year, monthIdx) => {
const daysInMonth = new Date(year, monthIdx + 1, 0).getDate();
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = `${year}-${String(monthIdx + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
if (isPastDate(dateStr) || !isDateAvailable(dateStr)) return false;
}
return true;
},
[isPastDate, isDateAvailable]
);
const isPastMonth = useCallback((year, monthIdx) => {
const now = new Date();
return (
year < now.getFullYear() ||
(year === now.getFullYear() && monthIdx < now.getMonth())
);
}, []);
const handleDayClick = (dateStr) => {
setRangeError(null);
if (!selectedStart || selectedEnd) {
onSelectStart(dateStr);
onSelectEnd(null);
return;
}
if (new Date(dateStr) <= new Date(selectedStart)) {
onSelectStart(dateStr);
onSelectEnd(null);
return;
}
const start = new Date(selectedStart);
const end = new Date(dateStr);
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const ds = d.toISOString().split("T")[0];
if (!isDateAvailable(ds)) {
setRangeError(t("unavailableInRange", { defaultValue: "توجد أيام غير متاحة ضمن الفترة المحددة" }));
return;
}
}
onSelectEnd(dateStr);
};
const handleMonthClick = (year, monthIdx) => {
setRangeError(null);
const monthStr = `${year}-${String(monthIdx + 1).padStart(2, "0")}`;
const firstDay = `${monthStr}-01`;
if (!selectedStart || selectedEnd || monthStr <= (selectedStart || "").substring(0, 7)) {
onSelectStart(firstDay);
onSelectEnd(null);
} else {
const lastDay = new Date(year, monthIdx + 1, 0).getDate();
onSelectEnd(`${monthStr}-${String(lastDay).padStart(2, "0")}`);
}
};
const navigateMonth = (delta) => {
let month = calendarMonth + delta;
let year = calendarYear;
if (month < 0) {
month = 11;
year--;
}
if (month > 11) {
month = 0;
year++;
}
setCalendarMonth(month);
setCalendarYear(year);
};
const isEntryStep = !selectedStart || !!selectedEnd;
return (
<div className="mb-3">
{/* Step Indicator */}
<div className="flex items-center justify-center gap-2 mb-3">
<div
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${isEntryStep ? "bg-amber-100 text-amber-800" : "bg-gray-100 text-gray-500"}`}
>
<div className={`w-1.5 h-1.5 rounded-full ${isEntryStep ? "bg-amber-500" : "bg-gray-400"}`} />
{t("selectStartDate")}
</div>
<div className="text-gray-300 text-xs"></div>
<div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${!isEntryStep ? "bg-amber-100 text-amber-800" : "bg-gray-100 text-gray-500"}`}>
<div className={`w-1.5 h-1.5 rounded-full ${!isEntryStep ? "bg-amber-500" : "bg-gray-400"}`} />
{t("selectEndDate")}
</div>
</div>
{mode === "daily" ? (
<div>
<div className="flex items-center justify-between mb-2">
<button onClick={() => navigateMonth(-1)} className="p-1 rounded-lg hover:bg-gray-100 transition-colors">
<ChevronRight className="w-4 h-4 text-gray-600" />
</button>
<span className="text-sm font-bold text-gray-900">
{t(MONTH_KEYS[calendarMonth])} {calendarYear}
</span>
<button onClick={() => navigateMonth(1)} className="p-1 rounded-lg hover:bg-gray-100 transition-colors">
<ChevronLeft className="w-4 h-4 text-gray-600" />
</button>
</div>
<div className="grid grid-cols-7 mb-1">
{DAY_KEYS.map((dk, i) => (
<div key={i} className="text-center text-[10px] text-gray-400 font-medium py-1">
{t(dk)}
</div>
))}
</div>
{(() => {
const firstDay = new Date(calendarYear, calendarMonth, 1).getDay();
const daysInMonth = new Date(calendarYear, calendarMonth + 1, 0).getDate();
const cells = [];
for (let i = 0; i < firstDay; i++) {
cells.push(<div key={`e-${i}`} />);
}
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = `${calendarYear}-${String(calendarMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
const past = isPastDate(dateStr);
const available = isDateAvailable(dateStr);
const isSelStart = dateStr === selectedStart;
const isSelEnd = dateStr === selectedEnd;
const inRange =
selectedStart &&
selectedEnd &&
new Date(dateStr) > new Date(selectedStart) &&
new Date(dateStr) < new Date(selectedEnd);
const disabled = past || !available;
cells.push(
<button
key={dateStr}
onClick={() => !disabled && handleDayClick(dateStr)}
disabled={disabled}
className={`text-center py-1.5 text-xs rounded-lg transition-all ${disabled ? "text-gray-300 cursor-not-allowed" : ""} ${isSelStart || isSelEnd ? "bg-amber-500 text-white font-bold shadow-sm" : ""} ${inRange ? "bg-amber-100 text-amber-800" : ""} ${!disabled && !isSelStart && !isSelEnd && !inRange && available ? "hover:bg-amber-50 text-gray-700" : ""} ${!disabled && !isSelStart && !isSelEnd && !inRange && !available ? "text-red-300" : ""}`}
>
{day}
</button>
);
}
return <div className="grid grid-cols-7 gap-0.5">{cells}</div>;
})()}
</div>
) : (
<div>
<div className="flex items-center justify-between mb-2">
<button onClick={() => setCalendarYear((prev) => prev - 1)} className="p-1 rounded-lg hover:bg-gray-100 transition-colors">
<ChevronRight className="w-4 h-4 text-gray-600" />
</button>
<span className="text-sm font-bold text-gray-900">{calendarYear}</span>
<button onClick={() => setCalendarYear((prev) => prev + 1)} className="p-1 rounded-lg hover:bg-gray-100 transition-colors">
<ChevronLeft className="w-4 h-4 text-gray-600" />
</button>
</div>
<div className="grid grid-cols-3 gap-2">
{MONTH_KEYS.map((mk, idx) => {
const monthStr = `${calendarYear}-${String(idx + 1).padStart(2, "0")}`;
const isSelStart = selectedStart && selectedStart.startsWith(monthStr);
const isSelEnd = selectedEnd && selectedEnd.startsWith(monthStr);
const inRange =
selectedStart &&
selectedEnd &&
monthStr > selectedStart.substring(0, 7) &&
monthStr < selectedEnd.substring(0, 7);
const disabled = isPastMonth(calendarYear, idx) || !isMonthFullyAvailable(calendarYear, idx);
return (
<button
key={idx}
onClick={() => !disabled && handleMonthClick(calendarYear, idx)}
disabled={disabled}
className={`p-2.5 rounded-xl text-center text-xs font-medium transition-all border ${disabled ? "text-gray-300 border-gray-100 bg-gray-50 cursor-not-allowed" : ""} ${isSelStart || isSelEnd ? "bg-amber-500 text-white border-amber-500 shadow-sm" : ""} ${!disabled && inRange ? "bg-amber-100 text-amber-800 border-amber-200" : ""} ${!disabled && !isSelStart && !isSelEnd && !inRange ? "bg-white text-gray-700 border-gray-200 hover:border-amber-300" : ""}`}
>
{t(mk)}
</button>
);
})}
</div>
</div>
)}
{rangeError && (
<div className="bg-red-50 text-red-600 p-2.5 rounded-xl text-xs mt-3">
{rangeError}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,94 @@
'use client';
import { useState } from 'react';
import { X, Loader2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import toast from 'react-hot-toast';
import StarRating from './StarRating';
import { addCustomerRating } from '../../utils/ratings';
const RatingField = ({ label, value, onChange }) => (
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">{label} <span className="text-red-500">*</span></label>
<StarRating rating={value} onRatingChange={onChange} size={28} />
{value === 0 && <p className="text-xs text-red-500">{/* required text handled in parent */}</p>}
</div>
);
export default function CustomerRatingForm({ reservationId, onSuccess, onCancel }) {
const { t } = useTranslation();
const [furnitureIntegrityRating, setFurnitureIntegrityRating] = useState(0);
const [termsComplianceRating, setTermsComplianceRating] = useState(0);
const [renterBehaviorRating, setRenterBehaviorRating] = useState(0);
const [comment, setComment] = useState('');
const [loading, setLoading] = useState(false);
const validate = () => {
if (furnitureIntegrityRating === 0) return t('ratings.furnitureIntegrity');
if (termsComplianceRating === 0) return t('ratings.termsCompliance');
if (renterBehaviorRating === 0) return t('ratings.renterBehavior');
return null;
};
const handleSubmit = async (e) => {
e.preventDefault();
const missing = validate();
if (missing) {
toast.error(t('ratings.pleaseRate') + ': ' + missing);
return;
}
setLoading(true);
try {
await addCustomerRating({
reservationId,
furnitureIntegrityRating,
termsComplianceRating,
renterBehaviorRating,
comment: comment.trim() || null,
});
toast.success(t('ratings.customerRatingSubmitted'));
onSuccess?.();
} catch (err) {
toast.error(t('ratings.submitError'));
} finally {
setLoading(false);
}
};
return (
<div className="bg-white rounded-2xl shadow-lg border border-gray-200 p-6 max-w-lg mx-auto">
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-bold text-gray-900">{t('ratings.customerRating')}</h3>
{onCancel && (
<button onClick={onCancel} className="text-gray-400 hover:text-gray-600">
<X size={20} />
</button>
)}
</div>
<form onSubmit={handleSubmit} className="space-y-5">
<RatingField label={t('ratings.furnitureIntegrity')} value={furnitureIntegrityRating} onChange={setFurnitureIntegrityRating} />
<RatingField label={t('ratings.termsCompliance')} value={termsComplianceRating} onChange={setTermsComplianceRating} />
<RatingField label={t('ratings.renterBehavior')} value={renterBehaviorRating} onChange={setRenterBehaviorRating} />
<div>
<label className="block text-sm font-medium text-gray-700">{t('ratings.commentLabel')}</label>
<textarea
rows={3}
value={comment}
onChange={(e) => setComment(e.target.value)}
className="w-full mt-1 px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
placeholder={t('ratings.commentPlaceholder')}
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-3 rounded-xl transition flex items-center justify-center gap-2 disabled:opacity-50"
>
{loading && <Loader2 className="w-5 h-5 animate-spin" />}
{loading ? t('ratings.sending') : t('ratings.submitRating')}
</button>
</form>
</div>
);
}

View File

@ -0,0 +1,317 @@
// import { useState } from 'react';
// import { motion } from 'framer-motion';
// import { Star, Edit2, X, Check, Clock } from 'lucide-react';
// import StarRating from './StarRating.js';
// import toast, { Toaster } from 'react-hot-toast';
// import { rateProperty, rateCustomer, getUserPropertyRating, canRateProperty } from '../../utils/ratings.js';
// const RatingForm = ({
// propertyId,
// userId,
// propertyOwner = false,
// initialRating = 0,
// initialComment = '',
// onSubmitSuccess
// }) => {
// const [rating, setRating] = useState(initialRating);
// const [comment, setComment] = useState(initialComment);
// const [loading, setLoading] = useState(false);
// const [showForm, setShowForm] = useState(false);
// const [userRating, setUserRating] = useState(null);
// // Check if user has already rated
// useState(() => {
// async function fetchUserRating() {
// try {
// const rating = await getUserPropertyRating(propertyId, userId);
// if (rating) {
// setUserRating(rating);
// setRating(rating.rating);
// setComment(rating.comment || '');
// }
// } catch (error) {
// console.error('[RatingForm] Failed to fetch user rating:', error);
// }
// }
// if (propertyId && userId) {
// fetchUserRating();
// }
// }, [propertyId, userId]);
// const handleSubmit = async (e) => {
// e.preventDefault();
// if (!rating) {
// toast.error('يرجى إعطاء تقييم من 1 إلى 5 نجوم');
// return;
// }
// setLoading(true);
// try {
// const ratingData = {
// propertyId,
// customerId: userId,
// rating,
// comment: comment.trim() || null
// };
// await rateProperty(ratingData);
// toast.success('تم إرسال التقييم بنجاح!');
// // Reset form
// setRating(0);
// setComment('');
// setShowForm(false);
// if (onSubmitSuccess) {
// onSubmitSuccess();
// }
// } catch (error) {
// console.error('[RatingForm] Failed to submit rating:', error);
// toast.error('حدث خطأ أثناء إرسال التقييم. يرجى المحاولة مرة أخرى.');
// } finally {
// setLoading(false);
// }
// };
// const handleEdit = () => {
// setShowForm(true);
// setRating(userRating?.rating || 0);
// setComment(userRating?.comment || '');
// };
// const handleCancel = () => {
// setShowForm(false);
// setRating(userRating?.rating || 0);
// setComment(userRating?.comment || '');
// };
// if (!propertyId || !userId) {
// return null;
// }
// return (
// <div className="space-y-4">
// <Toaster position="top-center" reverseOrder={false} />
// {/* Display existing rating */}
// {userRating && !showForm && (
// <motion.div
// initial={{ opacity: 0, y: 20 }}
// animate={{ opacity: 1, y: 0 }}
// className="bg-gray-50 rounded-xl p-4 border border-gray-200"
// >
// <div className="flex items-center justify-between mb-3">
// <div className="flex items-center gap-2">
// <Star className="w-5 h-5 text-amber-500" />
// <span className="font-medium text-gray-900">{userRating.rating}</span>
// <span className="text-sm text-gray-500">من 5</span>
// </div>
// <button
// onClick={handleEdit}
// className="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm hover:bg-gray-200 transition-colors flex items-center gap-1"
// >
// <Edit2 className="w-4 h-4" />
// تعديل
// </button>
// </div>
// {userRating.comment && (
// <div className="text-gray-600 text-sm mb-3 line-clamp-3">
// "{userRating.comment}"
// </div>
// )}
// <div className="flex items-center gap-2 text-xs text-gray-400">
// <Clock className="w-3 h-3" />
// <span>{userRating.createdAt ? new Date(userRating.createdAt).toLocaleDateString('ar-SA') : ''}</span>
// </div>
// </motion.div>
// )}
// {/* Rating form */}
// {showForm && (
// <motion.div
// initial={{ opacity: 0, y: 20 }}
// animate={{ opacity: 1, y: 0 }}
// className="bg-white rounded-xl p-6 border border-gray-200 shadow-sm"
// >
// <form onSubmit={handleSubmit}>
// <div className="mb-4">
// <label className="block text-sm font-medium text-gray-700 mb-2">
// تقييمك للعقار
// </label>
// <div className="flex items-center gap-2">
// <StarRating
// rating={rating}
// onRatingChange={setRating}
// readOnly={false}
// size={28}
// color="#ffc107"
// />
// <span className="text-lg font-bold text-gray-900">{rating || '1'}</span>
// <span className="text-sm text-gray-400">/5</span>
// </div>
// </div>
// <div className="mb-4">
// <label className="block text-sm font-medium text-gray-700 mb-2">
// تعليق (اختياري)
// </label>
// <textarea
// rows="3"
// value={comment}
// onChange={(e) => setComment(e.target.value)}
// placeholder="شارك تجربتك مع العقار..."
// className="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-amber-500 transition-all resize-none"
// />
// </div>
// <div className="flex gap-3">
// <button
// type="button"
// onClick={handleCancel}
// disabled={loading}
// className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg font-medium hover:bg-gray-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
// >
// إلغاء
// </button>
// <button
// type="submit"
// disabled={loading || !rating}
// className="flex-1 px-4 py-2 bg-amber-500 text-white rounded-lg font-medium hover:bg-amber-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-1"
// >
// {loading ? (
// <div className="w-4 h-4 border-2 border-white/50 border-t-white rounded-full animate-spin" />
// ) : (
// <Check className="w-5 h-5" />
// )}
// {loading ? 'إرسال' : 'إرسال التقييم'}
// </button>
// </div>
// </form>
// </motion.div>
// )}
// {/* Add rating button */}
// {!userRating && !showForm && (
// <motion.div
// initial={{ opacity: 0, y: 20 }}
// animate={{ opacity: 1, y: 0 }}
// className="bg-amber-50 border-2 border-amber-200 rounded-xl p-4 text-center cursor-pointer hover:border-amber-300 transition-all"
// onClick={() => setShowForm(true)}
// >
// <Star className="w-8 h-8 text-amber-500 mx-auto mb-2" />
// <h3 className="font-bold text-amber-700 mb-2">قيّم هذا العقار</h3>
// <p className="text-sm text-amber-600">شارك تجربتك مع المستأجرين الآخرين</p>
// </motion.div>
// )}
// </div>
// );
// };
// export default RatingForm;
'use client';
import { useState } from 'react';
import { X, Loader2 } from 'lucide-react';
import toast from 'react-hot-toast';
import StarRating from './StarRating';
import { addPropertyRating } from '../../utils/ratings';
const RatingField = ({ label, value, onChange }) => (
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">{label} <span className="text-red-500">*</span></label>
<StarRating rating={value} onRatingChange={onChange} size={28} />
{value === 0 && <p className="text-xs text-red-500">مطلوب</p>}
</div>
);
export default function PropertyRatingForm({ reservationId, onSuccess, onCancel }) {
const [cleanRating, setCleanRating] = useState(0);
const [servicesRating, setServicesRating] = useState(0);
const [ownerBehaviorRating, setOwnerBehaviorRating] = useState(0);
const [experienceRating, setExperienceRating] = useState(0);
const [comment, setComment] = useState('');
const [loading, setLoading] = useState(false);
const validate = () => {
if (cleanRating === 0) return 'نظافة العقار';
if (servicesRating === 0) return 'جودة الخدمات';
if (ownerBehaviorRating === 0) return 'سلوك المالك';
if (experienceRating === 0) return 'التجربة العامة';
return null;
};
const handleSubmit = async (e) => {
e.preventDefault();
const missing = validate();
if (missing) {
toast.error(`يرجى تقييم: ${missing}`);
return;
}
setLoading(true);
try {
await addPropertyRating({
reservationId,
cleanRating,
servicesRating,
ownerBehaviorRating,
experienceRating,
comment: comment.trim() || null,
});
toast.success('تم إرسال التقييم بنجاح!');
onSuccess?.();
} catch (err) {
console.error(err);
toast.error('حدث خطأ، حاول مرة أخرى');
} finally {
setLoading(false);
}
};
return (
<div className="bg-white rounded-2xl shadow-lg border border-gray-200 p-6 max-w-lg mx-auto">
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-bold text-gray-900">تقييم العقار</h3>
{onCancel && (
<button onClick={onCancel} className="text-gray-400 hover:text-gray-600">
<X size={20} />
</button>
)}
</div>
<form onSubmit={handleSubmit} className="space-y-5">
<RatingField label="نظافة العقار" value={cleanRating} onChange={setCleanRating} />
<RatingField label="جودة الخدمات" value={servicesRating} onChange={setServicesRating} />
<RatingField label="سلوك المالك" value={ownerBehaviorRating} onChange={setOwnerBehaviorRating} />
<RatingField label="التجربة العامة" value={experienceRating} onChange={setExperienceRating} />
<div>
<label className="block text-sm font-medium text-gray-700">تعليق (اختياري)</label>
<textarea
rows={3}
value={comment}
onChange={(e) => setComment(e.target.value)}
className="w-full mt-1 px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
placeholder="شارك تجربتك..."
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-3 rounded-xl transition flex items-center justify-center gap-2 disabled:opacity-50"
>
{loading && <Loader2 className="w-5 h-5 animate-spin" />}
{loading ? 'جاري الإرسال...' : 'إرسال التقييم'}
</button>
</form>
</div>
);
}

View File

@ -0,0 +1,286 @@
// 'use client';
// import { useState, useEffect } from 'react';
// import { motion } from 'framer-motion';
// import { Star } from 'lucide-react';
// import { getPropertyRatings } from '../../utils/ratings.js';
// import toast, { Toaster } from 'react-hot-toast';
// const RatingList = ({ propertyId, userId }) => {
// const [reviews, setReviews] = useState([]);
// const [loading, setLoading] = useState(true);
// const [error, setError] = useState(null);
// useEffect(() => {
// const fetchReviews = async () => {
// if (!propertyId) {
// setLoading(false);
// return;
// }
// try {
// setLoading(true);
// const data = await getPropertyReviews(propertyId);
// setReviews(data || []);
// setError(null);
// } catch (err) {
// console.error('[RatingList] Failed to fetch reviews:', err);
// setError('فشل تحميل التقييمات');
// setReviews([]);
// } finally {
// setLoading(false);
// }
// };
// fetchReviews();
// }, [propertyId]);
// if (loading) {
// return (
// <motion.div
// initial={{ opacity: 0, y: 20 }}
// animate={{ opacity: 1, y: 0 }}
// className="text-center py-8"
// >
// <div className="w-10 h-10 border-2 border-gray-200 border-t-gray-500 rounded-full animate-spin mx-auto mb-4" />
// <p className="text-gray-500">جاري تحميل التقييمات...</p>
// </motion.div>
// );
// }
// if (error) {
// return (
// <motion.div
// initial={{ opacity: 0, y: 20 }}
// animate={{ opacity: 1, y: 0 }}
// className="text-center py-8"
// >
// <p className="text-red-500">{error}</p>
// </motion.div>
// );
// }
// if (reviews.length === 0) {
// return (
// <motion.div
// initial={{ opacity: 0, y: 20 }}
// animate={{ opacity: 1, y: 0 }}
// className="text-center py-8"
// >
// <p className="text-gray-500">لا توجد تقييمات حتى الآن. كن أول من يقيم هذا العقار!</p>
// </motion.div>
// );
// }
// // Calculate average rating
// const averageRating = reviews.reduce((sum, review) => sum + review.rating, 0) / reviews.length;
// return (
// <motion.div
// initial={{ opacity: 0, y: 20 }}
// animate={{ opacity: 1, y: 0 }}
// className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
// >
// <Toaster position="top-center" reverseOrder={false} />
// <div className="space-y-4">
// {/* Header with average rating */}
// <div className="flex items-center justify-between pb-3 border-b border-gray-100">
// <div>
// <h2 className="text-xl font-bold text-gray-900">تقييمات المستأجرين</h2>
// <p className="text-sm text-gray-500">
// {reviews.length} تقييمات
// </p>
// </div>
// <div className="flex items-center gap-2">
// <div className="flex items-center gap-1">
// {Array.from({ length: 5 }).map((_, index) => (
// <Star
// key={index}
// className={`w-5 h-5 ${index < Math.floor(averageRating) ? 'text-amber-500' : 'text-gray-300'}`}
// />
// ))}
// {averageRating % 1 !== 0 && (
// <Star className="w-5 h-5 text-amber-400" />
// )}
// <span className="font-bold text-gray-900 ml-2">{averageRating.toFixed(1)}</span>
// </div>
// </div>
// </div>
// {/* Reviews list */}
// <div className="space-y-4">
// {reviews.map((review, index) => (
// <div key={index} className="border-t border-gray-100 pt-4 first:border-t-0 first:pt-0">
// <div className="flex justify-between items-start mb-2">
// <div className="flex items-start gap-3">
// <div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0">
// <Star className="w-6 h-6 text-gray-600" />
// </div>
// <div>
// <div className="font-medium text-gray-900">{review.userName || 'مستأجر'}</div>
// <div className="flex items-center gap-1 mt-1 text-sm">
// {Array.from({ length: 5 }).map((_, starIndex) => (
// <Star
// key={starIndex}
// className={`w-4 h-4 ${starIndex < review.rating ? 'text-amber-500' : 'text-gray-300'}`}
// />
// ))}
// <span className="ml-1 text-xs text-gray-500">({review.rating}/5)</span>
// </div>
// </div>
// </div>
// <div className="text-xs text-gray-400">
// {review.createdAt ? new Date(review.createdAt).toLocaleDateString('ar-SA') : ''}
// </div>
// </div>
// {review.comment && (
// <p className="text-gray-700 text-sm leading-relaxed">{review.comment}</p>
// )}
// </div>
// ))}
// </div>
// </div>
// </motion.div>
// );
// };
// export default RatingList;
'use client';
import { useState, useEffect } from 'react';
import { Star, User, Calendar, ChevronDown, Loader2 } from 'lucide-react';
import { getPropertyRatings, getPropertyAverageRating } from '../../utils/ratings';
const RatingItem = ({ rating }) => {
const overall = (
rating.cleanRating + rating.servicesRating +
rating.ownerBehaviorRating + rating.experienceRating
) / 4;
return (
<div className="border-b border-gray-100 py-4 last:border-0">
<div className="flex justify-between items-start mb-2">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-amber-100 rounded-full flex items-center justify-center">
<User className="w-4 h-4 text-amber-600" />
</div>
<span className="font-medium text-gray-800">{rating.customerName || 'مستأجر'}</span>
</div>
<div className="flex items-center gap-1 bg-amber-50 px-2 py-1 rounded-full">
<Star className="w-3 h-3 text-amber-500 fill-amber-500" />
<span className="text-sm font-bold">{overall.toFixed(1)}</span>
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-sm text-gray-600 mb-2">
<div>النظافة: {rating.cleanRating}/5</div>
<div>الخدمات: {rating.servicesRating}/5</div>
<div>سلوك المالك: {rating.ownerBehaviorRating}/5</div>
<div>التجربة: {rating.experienceRating}/5</div>
</div>
{rating.comment && (
<p className="text-gray-700 text-sm mt-2 pr-4 border-r-2 border-amber-200">"{rating.comment}"</p>
)}
<div className="flex items-center gap-1 text-xs text-gray-400 mt-2">
<Calendar className="w-3 h-3" />
<span>{new Date(rating.createdAt).toLocaleDateString('ar-SA')}</span>
</div>
</div>
);
};
export default function PropertyRatingList({ propertyId }) {
const [ratings, setRatings] = useState([]);
const [average, setAverage] = useState(null);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(false);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const fetchRatings = async (reset = false) => {
const currentPage = reset ? 1 : page;
try {
if (reset) setLoading(true);
else setLoadingMore(true);
const result = await getPropertyRatings(propertyId, currentPage, 10);
const items = result?.items || result?.data?.items || result || [];
const totalPages = result?.totalPages || result?.data?.totalPages || 1;
setRatings(prev => reset ? items : [...prev, ...items]);
setHasMore(currentPage < totalPages);
if (reset) setPage(1);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
setLoadingMore(false);
}
};
const fetchAverage = async () => {
try {
const avg = await getPropertyAverageRating(propertyId);
setAverage(avg);
} catch (err) {
console.error(err);
}
};
useEffect(() => {
if (propertyId) {
fetchRatings(true);
fetchAverage();
}
}, [propertyId]);
const loadMore = () => {
const nextPage = page + 1;
setPage(nextPage);
fetchRatings(false);
};
if (loading && ratings.length === 0) {
return (
<div className="text-center py-8">
<Loader2 className="w-8 h-8 animate-spin text-amber-500 mx-auto" />
<p className="text-gray-500 mt-2">جاري تحميل التقييمات...</p>
</div>
);
}
return (
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
<div className="flex justify-between items-center mb-4 pb-3 border-b border-gray-100">
<h3 className="text-xl font-bold text-gray-900">تقييمات المستأجرين</h3>
{average !== null && average > 0 && (
<div className="flex items-center gap-1 bg-amber-50 px-3 py-1 rounded-full">
<Star className="w-5 h-5 text-amber-500 fill-amber-500" />
<span className="font-bold text-lg">{average.toFixed(1)}</span>
<span className="text-gray-500 text-sm">/5</span>
</div>
)}
</div>
{ratings.length === 0 ? (
<p className="text-center text-gray-500 py-6">لا توجد تقييمات حتى الآن. كن أول من يقيم هذا العقار.</p>
) : (
<>
{ratings.map((r, idx) => <RatingItem key={idx} rating={r} />)}
{hasMore && (
<button
onClick={loadMore}
disabled={loadingMore}
className="w-full mt-4 py-2 text-amber-600 hover:text-amber-700 font-medium text-sm flex items-center justify-center gap-1"
>
{loadingMore ? <Loader2 className="w-4 h-4 animate-spin" /> : <ChevronDown className="w-4 h-4" />}
{loadingMore ? 'جاري التحميل...' : 'عرض المزيد'}
</button>
)}
</>
)}
</div>
);
}

View File

@ -0,0 +1,134 @@
// import { useState } from 'react';
// import { motion } from 'framer-motion';
// import { Star } from 'lucide-react';
// const StarRating = ({
// rating,
// onRatingChange,
// maxStars = 5,
// size = 24,
// color = '#ffc107',
// readOnly = false,
// className = ''
// }) => {
// const [hoverRating, setHoverRating] = useState(null);
// const handleClick = (value) => {
// if (!readOnly && onRatingChange) {
// onRatingChange(value);
// }
// };
// const handleMouseEnter = (value) => {
// if (!readOnly) {
// setHoverRating(value);
// }
// };
// const handleMouseLeave = () => {
// if (!readOnly) {
// setHoverRating(null);
// }
// };
// const getStarIcon = (index) => {
// const currentRating = hoverRating !== null ? hoverRating : rating;
// if (currentRating > index) {
// const hasHalfStar = currentRating % 1 > 0.5 && index + 0.5 <= currentRating;
// if (hasHalfStar) {
// // For half star, we'll use a combination approach or just show full star
// // Since we don't have StarOutline, we'll approximate with full stars
// return <Star className={`w-${size} h-${size} text-${color}`} />;
// }
// return <Star className={`w-${size} h-${size} text-${color}`} />;
// }
// return <Star className={`w-${size} h-${size} text-gray-400`} />;
// };
// return (
// <div className={`flex gap-1 ${className}`} onMouseLeave={handleMouseLeave}>
// {[...Array(maxStars)].map((_, index) => (
// <motion.div
// key={index}
// whileHover={{ scale: readOnly ? 1 : 1.1 }}
// onClick={() => handleClick(index + 1)}
// onMouseEnter={() => handleMouseEnter(index + 1)}
// >
// {getStarIcon(index)}
// </motion.div>
// ))}
// </div>
// );
// };
// export default StarRating;
// // Helper functions
// export function getStarCount(rating, maxStars = 5) {
// return Math.round(rating * maxStars) / maxStars;
// }
// export function formatRating(rating) {
// if (rating === 0) return 'لا يوجد تقييم';
// return `${rating.toFixed(1)}`; // Show 1 decimal place
// }
// export function getRatingColor(rating) {
// if (rating >= 4.5) return 'text-green-600';
// if (rating >= 3.5) return 'text-yellow-600';
// if (rating >= 2.5) return 'text-orange-600';
// return 'text-red-600';
// }
// export function getRatingText(rating) {
// if (rating >= 4.5) return 'ممتاز';
// if (rating >= 3.5) return 'جيد جداً';
// if (rating >= 2.5) return 'جيد';
// if (rating >= 1.5) return 'مقبول';
// return 'ضعيف';
// }
'use client';
import { useState } from 'react';
import { Star } from 'lucide-react';
const StarRating = ({ rating = 0, onRatingChange, size = 28, readOnly = false }) => {
const [hoverRating, setHoverRating] = useState(0);
const handleClick = (value) => {
if (!readOnly && onRatingChange) onRatingChange(value);
};
return (
<div
className="flex gap-1"
onMouseLeave={() => !readOnly && setHoverRating(0)}
>
{[1, 2, 3, 4, 5].map((star) => {
const filled = (hoverRating || rating) >= star;
return (
<button
key={star}
type="button"
onClick={() => handleClick(star)}
onMouseEnter={() => !readOnly && setHoverRating(star)}
className="focus:outline-none transition-transform hover:scale-110"
disabled={readOnly}
>
<Star
size={size}
className={`${filled ? 'fill-amber-500 text-amber-500' : 'text-gray-300 fill-transparent'}`}
/>
</button>
);
})}
</div>
);
};
export default StarRating;

View File

@ -17,7 +17,11 @@ export const useFavorites = () => {
function mapApiFavorite(item) { function mapApiFavorite(item) {
const info = item.propertyInformation || {}; const info = item.propertyInformation || {};
let details = {}; let details = {};
try { details = JSON.parse(info.detailsJSON || '{}'); } catch {} if (typeof info.detailsJSON === 'object' && info.detailsJSON) {
details = info.detailsJSON;
} else {
try { details = JSON.parse(info.detailsJSON || '{}'); } catch {}
}
const price = item.monthlyRent || item.dailyRent || 0; const price = item.monthlyRent || item.dailyRent || 0;
const priceUnit = item.monthlyRent ? 'monthly' : 'daily'; const priceUnit = item.monthlyRent ? 'monthly' : 'daily';

View File

@ -0,0 +1,75 @@
'use client';
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { getUserNotifications } from '../utils/api';
import AuthService from '../services/AuthService';
const NotificationsContext = createContext();
export const useNotifications = () => {
const context = useContext(NotificationsContext);
if (!context) {
throw new Error('useNotifications must be used within NotificationsProvider');
}
return context;
};
export function NotificationsProvider({ children }) {
const [notifications, setNotifications] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const fetchNotifications = useCallback(async () => {
if (!AuthService.isAuthenticated()) {
setNotifications([]);
setUnreadCount(0);
return;
}
setIsLoading(true);
try {
const data = await getUserNotifications();
const notificationsArray = Array.isArray(data) ? data : [];
setNotifications(notificationsArray);
// Assuming all are unread for now, or add logic to check 'read' field if exists
setUnreadCount(notificationsArray.length);
} catch (error) {
console.error('Error fetching notifications:', error);
setNotifications([]);
setUnreadCount(0);
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
fetchNotifications();
}, [fetchNotifications]);
const markAsRead = useCallback((id) => {
setNotifications(prev =>
prev.map(n => (n.id === id ? { ...n, read: true } : n))
);
setUnreadCount(prev => Math.max(0, prev - 1));
}, []);
const markAllAsRead = useCallback(() => {
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
setUnreadCount(0);
}, []);
const value = {
notifications,
unreadCount,
isLoading,
fetchNotifications,
markAsRead,
markAllAsRead,
};
return (
<NotificationsContext.Provider value={value}>
{children}
</NotificationsContext.Provider>
);
}

View File

@ -0,0 +1,32 @@
"use client";
import { createContext, useContext, useState, useEffect } from "react";
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("dark");
useEffect(() => {
const stored = localStorage.getItem("theme");
if (stored === "dark" || (!stored && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
setTheme("dark");
document.documentElement.classList.add("dark");
}
}, []);
const toggleTheme = () => {
const next = theme === "light" ? "dark" : "light";
setTheme(next);
document.documentElement.classList.toggle("dark", next === "dark");
localStorage.setItem("theme", next);
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);

View File

@ -1,38 +1,38 @@
/**
* BookingStatus Enum
* Backend values are strings
* Used in: Reservation workflow
*/
const BookingStatus = Object.freeze({ const BookingStatus = Object.freeze({
PENDING: 'pending', PENDING: 'pending',
OWNER_APPROVED: 'owner_approved', ownerConfirmed: 'ownerConfirmed',
ADMIN_APPROVED: 'admin_approved', depositPaid: 'depositPaid',
ACTIVE: 'active', depositConfirmed: 'depositConfirmed',
COMPLETED: 'completed', completed: 'completed',
REJECTED: 'rejected', cancelled: 'cancelled',
CANCELLED: 'cancelled',
}); });
// Map status → Arabic label
const BookingStatusLabels = Object.freeze({ const BookingStatusLabels = Object.freeze({
[BookingStatus.PENDING]: 'بانتظار الموافقة', [BookingStatus.PENDING]: 'قيد الانتظار',
[BookingStatus.OWNER_APPROVED]: وافقة المالك', [BookingStatus.ownerConfirmed]: ؤكد من المالك',
[BookingStatus.ADMIN_APPROVED]: 'موافقة الإدارة', [BookingStatus.depositPaid]: 'تم دفع السلفة',
[BookingStatus.ACTIVE]: 'إيجار نشط', [BookingStatus.depositConfirmed]: 'تم تأكيد الدفع',
[BookingStatus.COMPLETED]: 'منتهي', [BookingStatus.completed]: 'منتهي',
[BookingStatus.REJECTED]: رفوض', [BookingStatus.cancelled]: لغي',
[BookingStatus.CANCELLED]: 'ملغي',
}); });
// Map status → color class (Tailwind bg)
const BookingStatusColors = Object.freeze({ const BookingStatusColors = Object.freeze({
[BookingStatus.PENDING]: 'yellow', [BookingStatus.PENDING]: 'yellow',
[BookingStatus.OWNER_APPROVED]: 'blue', [BookingStatus.ownerConfirmed]: 'blue',
[BookingStatus.ADMIN_APPROVED]: 'green', [BookingStatus.depositPaid]: 'orange',
[BookingStatus.ACTIVE]: 'purple', [BookingStatus.depositConfirmed]: 'green',
[BookingStatus.COMPLETED]: 'gray', [BookingStatus.completed]: 'teal',
[BookingStatus.REJECTED]: 'red', [BookingStatus.cancelled]: 'red',
[BookingStatus.CANCELLED]: 'red',
}); });
export { BookingStatus, BookingStatusLabels, BookingStatusColors }; const BookingStatusTranslationKeys = Object.freeze({
[BookingStatus.PENDING]: 'bookingStatus.pending',
[BookingStatus.ownerConfirmed]: 'bookingStatus.ownerConfirmed',
[BookingStatus.depositPaid]: 'bookingStatus.depositPaid',
[BookingStatus.depositConfirmed]: 'bookingStatus.depositConfirmed',
[BookingStatus.completed]: 'bookingStatus.completed',
[BookingStatus.cancelled]: 'bookingStatus.cancelled',
});
export { BookingStatus, BookingStatusLabels, BookingStatusColors, BookingStatusTranslationKeys };

View File

@ -1,33 +1,72 @@
/** /**
* BuildingType Enum * BuildingType Enum
* Backend values are numeric (0, 1, 2) * Backend values are numeric
* Used in: PropertyInformation.buildingType * Used in: PropertyInformation.buildingType
*/ */
const BuildingType = Object.freeze({ const BuildingType = Object.freeze({
APARTMENT: 0, APARTMENT: 0,
VILLA: 1, VILLA: 1,
HOUSE: 2, HOUSE: 2,
SWEET: 3,
ROOM: 4,
STUDIO: 5,
OFFICE: 6,
FARMS: 7,
SHOP: 8,
WAREHOUSE: 9,
}); });
// Map numeric value → Arabic label
const BuildingTypeLabels = Object.freeze({ const BuildingTypeLabels = Object.freeze({
[BuildingType.APARTMENT]: 'شقة', [BuildingType.APARTMENT]: 'شقة',
[BuildingType.VILLA]: 'فيلا', [BuildingType.VILLA]: 'فيلا',
[BuildingType.HOUSE]: 'بيت', [BuildingType.HOUSE]: 'بيت',
[BuildingType.SWEET]: 'سويت',
[BuildingType.ROOM]: 'غرفة',
[BuildingType.STUDIO]: 'استوديو',
[BuildingType.OFFICE]: 'مكتب',
[BuildingType.FARMS]: 'مزرعة',
[BuildingType.SHOP]: 'متجر',
[BuildingType.WAREHOUSE]: 'مستودع',
}); });
// Map numeric value → English key (for UI filters)
const BuildingTypeKeys = Object.freeze({ const BuildingTypeKeys = Object.freeze({
[BuildingType.APARTMENT]: 'apartment', [BuildingType.APARTMENT]: 'apartment',
[BuildingType.VILLA]: 'villa', [BuildingType.VILLA]: 'villa',
[BuildingType.HOUSE]: 'house', [BuildingType.HOUSE]: 'house',
[BuildingType.SWEET]: 'sweet',
[BuildingType.ROOM]: 'room',
[BuildingType.STUDIO]: 'studio',
[BuildingType.OFFICE]: 'office',
[BuildingType.FARMS]: 'farms',
[BuildingType.SHOP]: 'shop',
[BuildingType.WAREHOUSE]: 'warehouse',
}); });
// Reverse map: English key → numeric value
const BuildingTypeByKey = Object.freeze({ const BuildingTypeByKey = Object.freeze({
apartment: BuildingType.APARTMENT, apartment: BuildingType.APARTMENT,
villa: BuildingType.VILLA, villa: BuildingType.VILLA,
house: BuildingType.HOUSE, house: BuildingType.HOUSE,
sweet: BuildingType.SWEET,
suite: BuildingType.SWEET,
room: BuildingType.ROOM,
studio: BuildingType.STUDIO,
office: BuildingType.OFFICE,
farms: BuildingType.FARMS,
shop: BuildingType.SHOP,
warehouse: BuildingType.WAREHOUSE,
}); });
export { BuildingType, BuildingTypeLabels, BuildingTypeKeys, BuildingTypeByKey }; const BuildingTypeTranslationKeys = Object.freeze({
[BuildingType.APARTMENT]: 'buildingType.apartment',
[BuildingType.VILLA]: 'buildingType.villa',
[BuildingType.HOUSE]: 'buildingType.house',
[BuildingType.SWEET]: 'buildingType.sweet',
[BuildingType.ROOM]: 'buildingType.room',
[BuildingType.STUDIO]: 'buildingType.studio',
[BuildingType.OFFICE]: 'buildingType.office',
[BuildingType.FARMS]: 'buildingType.farms',
[BuildingType.SHOP]: 'buildingType.shop',
[BuildingType.WAREHOUSE]: 'buildingType.warehouse',
});
export { BuildingType, BuildingTypeLabels, BuildingTypeKeys, BuildingTypeByKey, BuildingTypeTranslationKeys };

View File

@ -0,0 +1,19 @@
const CancellationReason = Object.freeze({
NONE: 0,
DEPOSIT_NOT_PAID: 1,
OWNER_CANCELED: 2,
});
const CancellationReasonLabels = Object.freeze({
[CancellationReason.NONE]: 'بدون سبب',
[CancellationReason.DEPOSIT_NOT_PAID]: 'عدم دفع العربون',
[CancellationReason.OWNER_CANCELED]: 'إلغاء من المالك',
});
const CancellationReasonTranslationKeys = Object.freeze({
[CancellationReason.NONE]: 'cancellationReason.none',
[CancellationReason.DEPOSIT_NOT_PAID]: 'cancellationReason.noDeposit',
[CancellationReason.OWNER_CANCELED]: 'cancellationReason.ownerCancellation',
});
export { CancellationReason, CancellationReasonLabels, CancellationReasonTranslationKeys };

View File

@ -18,7 +18,21 @@ const City = Object.freeze({
QAMISHLI: 'القامشلي', QAMISHLI: 'القامشلي',
RURAL_DAMASCUS: 'ريف دمشق', RURAL_DAMASCUS: 'ريف دمشق',
}); });
export const CityEnum = Object.freeze({
DAMASCUS: 0,
ALEPPO: 1,
HOMS: 2,
LATAKIA: 3,
DARAA: 4,
TARTOUS: 5,
SUWEIDA: 6,
DEIR_EZZOR: 7,
RAQQA: 8,
IDLIB: 9,
HASAKAH: 10,
Qamishli: 11,
RURAL_DAMASCUS: 12,
});
// All cities as a flat array // All cities as a flat array
const CitiesList = Object.freeze(Object.values(City)); const CitiesList = Object.freeze(Object.values(City));
@ -35,4 +49,22 @@ function extractCity(address) {
return ''; return '';
} }
export { City, CitiesList, extractCity }; const CityTranslationKeys = Object.freeze({
[City.DAMASCUS]: 'city.damascus',
[City.ALEPPO]: 'city.aleppo',
[City.HOMS]: 'city.homs',
[City.LATAKIA]: 'city.latakia',
[City.DARAA]: 'city.daraa',
[City.TARTOUS]: 'city.tartous',
[City.SUWEIDA]: 'city.suweida',
[City.DEIR_EZZOR]: 'city.deirEzzor',
[City.RAQQA]: 'city.raqqa',
[City.IDLIB]: 'city.idlib',
[City.HASAKAH]: 'city.hasakah',
[City.QAMISHLI]: 'city.qamishli',
[City.RURAL_DAMASCUS]: 'city.ruralDamascus',
});
export { City, CitiesList, extractCity, CityTranslationKeys, };

View File

@ -16,4 +16,10 @@ const CommissionTypeLabels = Object.freeze({
[CommissionType.FROM_BOTH]: 'من الاثنين', [CommissionType.FROM_BOTH]: 'من الاثنين',
}); });
export { CommissionType, CommissionTypeLabels }; const CommissionTypeTranslationKeys = Object.freeze({
[CommissionType.FROM_OWNER]: 'commissionType.fromOwner',
[CommissionType.FROM_TENANT]: 'commissionType.fromTenant',
[CommissionType.FROM_BOTH]: 'commissionType.fromBoth',
});
export { CommissionType, CommissionTypeLabels, CommissionTypeTranslationKeys };

View File

@ -5,16 +5,29 @@
const Currency = Object.freeze({ const Currency = Object.freeze({
SYP: 1, SYP: 1,
USD: 2, USD: 2,
EUR: 3,
TRY: 4,
}); });
const CurrencyLabels = Object.freeze({ const CurrencyLabels = Object.freeze({
[Currency.SYP]: 'ليرة سورية', [Currency.SYP]: 'ليرة سورية',
[Currency.USD]: 'دولار أمريكي', [Currency.USD]: 'دولار أمريكي',
[Currency.EUR]: 'يورو',
[Currency.TRY]: 'ليرة تركية',
}); });
const CurrencySymbols = Object.freeze({ const CurrencySymbols = Object.freeze({
[Currency.SYP]: 'SYP', [Currency.SYP]: 'SYP',
[Currency.USD]: 'USD', [Currency.USD]: 'USD',
[Currency.EUR]: 'EUR',
[Currency.TRY]: 'TRY',
}); });
export { Currency, CurrencyLabels, CurrencySymbols }; const CurrencyTranslationKeys = Object.freeze({
[Currency.SYP]: 'currency.syp',
[Currency.USD]: 'currency.usd',
[Currency.EUR]: 'currency.eur',
[Currency.TRY]: 'currency.try',
});
export { Currency, CurrencyLabels, CurrencySymbols, CurrencyTranslationKeys };

View File

@ -14,4 +14,9 @@ const CustomerTypeLabels = Object.freeze({
[CustomerType.FAMILY]: 'عائلي', [CustomerType.FAMILY]: 'عائلي',
}); });
export { CustomerType, CustomerTypeLabels }; const CustomerTypeTranslationKeys = Object.freeze({
[CustomerType.PERSONAL]: 'customerType.personal',
[CustomerType.FAMILY]: 'customerType.family',
});
export { CustomerType, CustomerTypeLabels, CustomerTypeTranslationKeys };

7
app/enums/DeviceType.js Normal file
View File

@ -0,0 +1,7 @@
const DeviceType = Object.freeze({
ANDROID: 'Android',
IOS: 'Ios',
WEB: 'Web',
});
export { DeviceType };

19
app/enums/DocumentType.js Normal file
View File

@ -0,0 +1,19 @@
/**
* Document type enum for owner property submission.
* Values are intentionally English keys so the UI can store normalized identifiers.
*/
const DocumentType = Object.freeze({
PASSPORT: 'Passport',
IDCARD: 'IDCard',
BOTH: 'Both',
});
const DocumentTypeLabels = Object.freeze({
[DocumentType.PASSPORT]: 'جواز سفر',
[DocumentType.IDCARD]: 'بطاقة شخصية',
[DocumentType.BOTH]: 'كلاهما',
});
const DocumentTypeList = Object.freeze(Object.values(DocumentType));
export { DocumentType, DocumentTypeLabels, DocumentTypeList };

39
app/enums/Governorate.js Normal file
View File

@ -0,0 +1,39 @@
/**
* Syrian governorates enum using English normalized values.
* Labels are provided separately for Arabic display.
*/
const Governorate = Object.freeze({
DAMASCUS: 'Damascus',
ALEPPO: 'Aleppo',
LATAKIA: 'Latakia',
HAMA: 'Hama',
TARTOUS: 'Tartous',
DEIR_EZZOR: 'DeirEzzor',
RAQQA: 'Raqqa',
IDLIB: 'Idlib',
DRAA: 'Daraa',
SUWEIDA: 'Suweida',
HASAKAH: 'Hasakah',
QUNEITRA: 'Quneitra',
RURAL_DAMASCUS: 'RuralDamascus',
});
const GovernorateLabels = Object.freeze({
[Governorate.DAMASCUS]: 'دمشق',
[Governorate.ALEPPO]: 'حلب',
[Governorate.LATAKIA]: 'اللاذقية',
[Governorate.HAMA]: 'حماة',
[Governorate.TARTOUS]: 'طرطوس',
[Governorate.DEIR_EZZOR]: 'دير الزور',
[Governorate.RAQQA]: 'الرقة',
[Governorate.IDLIB]: 'إدلب',
[Governorate.DRAA]: 'درعا',
[Governorate.SUWEIDA]: 'السويداء',
[Governorate.HASAKAH]: 'الحسكة',
[Governorate.QUNEITRA]: 'القنيطرة',
[Governorate.RURAL_DAMASCUS]: 'ريف دمشق',
});
const GovernorateList = Object.freeze(Object.values(Governorate));
export { Governorate, GovernorateLabels, GovernorateList };

View File

@ -20,4 +20,9 @@ const IdentityTypeFlags = Object.freeze({
[IdentityType.PASSPORT]: '🛂', [IdentityType.PASSPORT]: '🛂',
}); });
export { IdentityType, IdentityTypeLabels, IdentityTypeFlags }; const IdentityTypeTranslationKeys = Object.freeze({
[IdentityType.SYRIAN]: 'identityType.syrian',
[IdentityType.PASSPORT]: 'identityType.passport',
});
export { IdentityType, IdentityTypeLabels, IdentityTypeFlags, IdentityTypeTranslationKeys };

11
app/enums/Language.js Normal file
View File

@ -0,0 +1,11 @@
const Language = Object.freeze({
ARABIC: 'Arabic',
ENGLISH: 'English',
});
const LanguageLabels = Object.freeze({
[Language.ARABIC]: 'العربية',
[Language.ENGLISH]: 'English',
});
export { Language, LanguageLabels };

View File

@ -14,4 +14,9 @@ const OwnerTypeLabels = Object.freeze({
[OwnerType.REAL_ESTATE_AGENCY]: 'وكالة عقارية', [OwnerType.REAL_ESTATE_AGENCY]: 'وكالة عقارية',
}); });
export { OwnerType, OwnerTypeLabels }; const OwnerTypeTranslationKeys = Object.freeze({
[OwnerType.PERSON]: 'ownerType.person',
[OwnerType.REAL_ESTATE_AGENCY]: 'ownerType.agency',
});
export { OwnerType, OwnerTypeLabels, OwnerTypeTranslationKeys };

View File

@ -38,4 +38,20 @@ const PropertyServiceLabels = Object.freeze({
// All services as array // All services as array
const PropertyServicesList = Object.freeze(Object.values(PropertyService)); const PropertyServicesList = Object.freeze(Object.values(PropertyService));
export { PropertyService, PropertyServiceLabels, PropertyServicesList }; const PropertyServiceTranslationKeys = Object.freeze({
[PropertyService.ELECTRICITY]: 'propertyService.electricity',
[PropertyService.INTERNET]: 'propertyService.internet',
[PropertyService.HEATING]: 'propertyService.heating',
[PropertyService.WATER]: 'propertyService.water',
[PropertyService.POOL]: 'propertyService.pool',
[PropertyService.PRIVATE_GARDEN]: 'propertyService.privateGarden',
[PropertyService.PARKING]: 'propertyService.parking',
[PropertyService.SECURITY_247]: 'propertyService.security247',
[PropertyService.CENTRAL_HEATING]: 'propertyService.centralHeating',
[PropertyService.CENTRAL_AIR_CONDITIONING]: 'propertyService.centralAirConditioning',
[PropertyService.EQUIPPED_KITCHEN]: 'propertyService.equippedKitchen',
[PropertyService.MAIDS_ROOM]: 'propertyService.maidsRoom',
[PropertyService.ELEVATOR]: 'propertyService.elevator',
});
export { PropertyService, PropertyServiceLabels, PropertyServicesList, PropertyServiceTranslationKeys };

View File

@ -5,29 +5,32 @@
*/ */
const PropertyStatus = Object.freeze({ const PropertyStatus = Object.freeze({
AVAILABLE: 0, AVAILABLE: 0,
BOOKED: 1, NOT_AVAILABLE: 1,
MAINTENANCE: 2, BOOKED: 2,
}); });
// Map numeric value → Arabic label
const PropertyStatusLabels = Object.freeze({ const PropertyStatusLabels = Object.freeze({
[PropertyStatus.AVAILABLE]: 'متاح', [PropertyStatus.AVAILABLE]: 'متاح',
[PropertyStatus.NOT_AVAILABLE]: 'غير متاح',
[PropertyStatus.BOOKED]: 'محجوز', [PropertyStatus.BOOKED]: 'محجوز',
[PropertyStatus.MAINTENANCE]: 'صيانة',
}); });
// Map numeric value → English key (for UI filters)
const PropertyStatusKeys = Object.freeze({ const PropertyStatusKeys = Object.freeze({
[PropertyStatus.AVAILABLE]: 'available', [PropertyStatus.AVAILABLE]: 'available',
[PropertyStatus.NOT_AVAILABLE]: 'notAvailable',
[PropertyStatus.BOOKED]: 'booked', [PropertyStatus.BOOKED]: 'booked',
[PropertyStatus.MAINTENANCE]: 'maintenance',
}); });
// Reverse map: English key → numeric value
const PropertyStatusByKey = Object.freeze({ const PropertyStatusByKey = Object.freeze({
available: PropertyStatus.AVAILABLE, available: PropertyStatus.AVAILABLE,
notAvailable: PropertyStatus.NOT_AVAILABLE,
booked: PropertyStatus.BOOKED, booked: PropertyStatus.BOOKED,
maintenance: PropertyStatus.MAINTENANCE,
}); });
export { PropertyStatus, PropertyStatusLabels, PropertyStatusKeys, PropertyStatusByKey }; const PropertyStatusTranslationKeys = Object.freeze({
[PropertyStatus.AVAILABLE]: 'propertyStatus.available',
[PropertyStatus.NOT_AVAILABLE]: 'propertyStatus.notAvailable',
[PropertyStatus.BOOKED]: 'propertyStatus.booked',
});
export { PropertyStatus, PropertyStatusLabels, PropertyStatusKeys, PropertyStatusByKey, PropertyStatusTranslationKeys };

View File

@ -18,4 +18,10 @@ const PropertyTermLabels = Object.freeze({
// All terms as array // All terms as array
const PropertyTermsList = Object.freeze(Object.values(PropertyTerm)); const PropertyTermsList = Object.freeze(Object.values(PropertyTerm));
export { PropertyTerm, PropertyTermLabels, PropertyTermsList }; const PropertyTermTranslationKeys = Object.freeze({
[PropertyTerm.NO_SMOKING]: 'propertyTerm.noSmoking',
[PropertyTerm.NO_ANIMALS]: 'propertyTerm.noAnimals',
[PropertyTerm.NO_PARTIES]: 'propertyTerm.noParties',
});
export { PropertyTerm, PropertyTermLabels, PropertyTermsList, PropertyTermTranslationKeys };

View File

@ -13,4 +13,9 @@ const RentPropertyConditionLabels = Object.freeze({
[RentPropertyCondition.WITHOUT_FURNITURE]: 'غير مفروش', [RentPropertyCondition.WITHOUT_FURNITURE]: 'غير مفروش',
}); });
export { RentPropertyCondition, RentPropertyConditionLabels }; const RentPropertyConditionTranslationKeys = Object.freeze({
[RentPropertyCondition.WITH_FURNITURE]: 'rentCondition.furnished',
[RentPropertyCondition.WITHOUT_FURNITURE]: 'rentCondition.unfurnished',
});
export { RentPropertyCondition, RentPropertyConditionLabels, RentPropertyConditionTranslationKeys };

View File

@ -14,4 +14,10 @@ const RentPropertyTypeLabels = Object.freeze({
[RentPropertyType.SEMI_FURNISHED]: 'مفروش جزئياً', [RentPropertyType.SEMI_FURNISHED]: 'مفروش جزئياً',
}); });
export { RentPropertyType, RentPropertyTypeLabels }; const RentPropertyTypeTranslationKeys = Object.freeze({
[RentPropertyType.FURNISHED]: 'rentPropertyType.fullyFurnished',
[RentPropertyType.UNFURNISHED]: 'rentPropertyType.unfurnished',
[RentPropertyType.SEMI_FURNISHED]: 'rentPropertyType.partiallyFurnished',
});
export { RentPropertyType, RentPropertyTypeLabels, RentPropertyTypeTranslationKeys };

View File

@ -13,4 +13,9 @@ const RentTypeLabels = Object.freeze({
[RentType.DAILY]: 'يومي', [RentType.DAILY]: 'يومي',
}); });
export { RentType, RentTypeLabels }; const RentTypeTranslationKeys = Object.freeze({
[RentType.MONTHLY]: 'rentType.monthly',
[RentType.DAILY]: 'rentType.daily',
});
export { RentType, RentTypeLabels, RentTypeTranslationKeys };

View File

@ -0,0 +1,16 @@
const SalePropertiesStatus = Object.freeze({
PENDING: 0,
CONFIRMED: 1,
});
const SalePropertiesStatusLabels = Object.freeze({
[SalePropertiesStatus.PENDING]: 'قيد الانتظار',
[SalePropertiesStatus.CONFIRMED]: 'مؤكد',
});
const SalePropertiesStatusTranslationKeys = Object.freeze({
[SalePropertiesStatus.PENDING]: 'saleStatus.pending',
[SalePropertiesStatus.CONFIRMED]: 'saleStatus.confirmed',
});
export { SalePropertiesStatus, SalePropertiesStatusLabels, SalePropertiesStatusTranslationKeys };

View File

@ -0,0 +1,16 @@
const TransactionType = Object.freeze({
ONLINE: 0,
MANUAL: 1,
});
const TransactionTypeLabels = Object.freeze({
[TransactionType.ONLINE]: 'أونلاين',
[TransactionType.MANUAL]: 'يدوي',
});
const TransactionTypeTranslationKeys = Object.freeze({
[TransactionType.ONLINE]: 'transactionType.online',
[TransactionType.MANUAL]: 'transactionType.manual',
});
export { TransactionType, TransactionTypeLabels, TransactionTypeTranslationKeys };

View File

@ -7,21 +7,28 @@ const UserRole = Object.freeze({
GUEST: 'guest', GUEST: 'guest',
CUSTOMER: 'customer', CUSTOMER: 'customer',
OWNER: 'owner', OWNER: 'owner',
ADMIN: 'admin', AGENT: 'agent',
}); });
const UserRoleLabels = Object.freeze({ const UserRoleLabels = Object.freeze({
[UserRole.GUEST]: 'زائر', [UserRole.GUEST]: 'زائر',
[UserRole.CUSTOMER]: 'مستأجر', [UserRole.CUSTOMER]: 'مستأجر',
[UserRole.OWNER]: 'مالك عقار', [UserRole.OWNER]: 'مالك عقار',
[UserRole.ADMIN]: 'مدير النظام', [UserRole.AGENT]: 'وسيط عقاري',
}); });
const UserRoleColors = Object.freeze({ const UserRoleColors = Object.freeze({
[UserRole.GUEST]: 'gray', [UserRole.GUEST]: 'gray',
[UserRole.CUSTOMER]: 'blue', [UserRole.CUSTOMER]: 'blue',
[UserRole.OWNER]: 'amber', [UserRole.OWNER]: 'amber',
[UserRole.ADMIN]: 'red', [UserRole.AGENT]: 'purple',
}); });
export { UserRole, UserRoleLabels, UserRoleColors }; const UserRoleTranslationKeys = Object.freeze({
[UserRole.GUEST]: 'userRole.guest',
[UserRole.CUSTOMER]: 'userRole.customer',
[UserRole.OWNER]: 'userRole.owner',
[UserRole.AGENT]: 'userRole.agent',
});
export { UserRole, UserRoleLabels, UserRoleColors, UserRoleTranslationKeys };

View File

@ -18,4 +18,11 @@ export { RentPropertyType, RentPropertyTypeLabels } from './RentPropertyType';
export { RentType, RentTypeLabels } from './RentType'; export { RentType, RentTypeLabels } from './RentType';
export { PropertyService, PropertyServiceLabels, PropertyServicesList } from './PropertyService'; export { PropertyService, PropertyServiceLabels, PropertyServicesList } from './PropertyService';
export { PropertyTerm, PropertyTermLabels, PropertyTermsList } from './PropertyTerm'; export { PropertyTerm, PropertyTermLabels, PropertyTermsList } from './PropertyTerm';
export { DocumentType, DocumentTypeLabels, DocumentTypeList } from './DocumentType';
export { Governorate, GovernorateLabels, GovernorateList } from './Governorate';
export { Currency, CurrencyLabels, CurrencySymbols } from './Currency'; export { Currency, CurrencyLabels, CurrencySymbols } from './Currency';
export { DeviceType } from './DeviceType';
export { SalePropertiesStatus, SalePropertiesStatusLabels } from './SalePropertiesStatus';
export { TransactionType, TransactionTypeLabels } from './TransactionType';
export { CancellationReason, CancellationReasonLabels } from './CancellationReason';
export { Language, LanguageLabels } from './Language';

121
app/faq/page.js Normal file
View File

@ -0,0 +1,121 @@
'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { HelpCircle, ChevronDown, Search } from 'lucide-react';
export default function FAQPage() {
const { t } = useTranslation();
const [openIndex, setOpenIndex] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const faqs = [
{ question: t('faq.q1'), answer: t('faq.a1') },
{ question: t('faq.q2'), answer: t('faq.a2') },
{ question: t('faq.q3'), answer: t('faq.a3') },
{ question: t('faq.q4'), answer: t('faq.a4') },
{ question: t('faq.q5'), answer: t('faq.a5') },
{ question: t('faq.q6'), answer: t('faq.a6') },
{ question: t('faq.q7'), answer: t('faq.a7') },
{ question: t('faq.q8'), answer: t('faq.a8') },
];
const filteredFaqs = faqs.filter(
(faq) =>
faq.question.includes(searchTerm) ||
faq.answer.includes(searchTerm)
);
const toggleFAQ = (index) => {
setOpenIndex(openIndex === index ? null : index);
};
return (
<div className="min-h-screen py-12" dir="rtl">
<div className="container mx-auto px-4 max-w-4xl">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-12"
>
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
<HelpCircle className="w-10 h-10 text-amber-600" />
</div>
<h1 className="text-4xl font-bold text-gray-900 mb-4">{t('faq.pageTitle')}</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
{t('faq.pageDescription')}
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="relative mb-8"
>
<Search className="absolute right-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
placeholder={t('faq.searchPlaceholder')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pr-12 pl-4 py-4 bg-white border border-gray-200 rounded-2xl shadow-sm focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400"
/>
</motion.div>
<div className="space-y-4">
{filteredFaqs.map((faq, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow"
>
<button
onClick={() => toggleFAQ(index)}
className="w-full flex items-center justify-between p-5 text-right"
>
<span className="text-lg font-semibold text-gray-900 flex-1">
{faq.question}
</span>
<motion.div
animate={{ rotate: openIndex === index ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<ChevronDown className="w-5 h-5 text-gray-500 shrink-0" />
</motion.div>
</button>
<AnimatePresence>
{openIndex === index && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<p className="px-5 pb-5 text-gray-600 leading-relaxed border-t border-gray-100 pt-4">
{faq.answer}
</p>
</motion.div>
)}
</AnimatePresence>
</motion.div>
))}
</div>
{filteredFaqs.length === 0 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-12"
>
<p className="text-gray-500 text-lg">{t('faq.noResults')}</p>
</motion.div>
)}
</div>
</div>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -1,60 +1,53 @@
'use client'; "use client";
import { useEffect, useState } from 'react'; import { useEffect, useState } from "react";
import { useRouter } from 'next/navigation'; import { useTranslation } from "react-i18next";
import { motion } from 'framer-motion'; import { useRouter } from "next/navigation";
import Link from 'next/link'; import { motion } from "framer-motion";
import Image from 'next/image'; import Link from "next/link";
import { Heart, MapPin, Bed, Bath, Square, X, ImageIcon } from 'lucide-react'; import Image from "next/image";
import { useFavorites } from '@/app/contexts/FavoritesContext'; import { Heart, MapPin, Bed, Bath, Square, X, ImageIcon } from "lucide-react";
import AuthService from '@/app/services/AuthService'; import { useFavorites } from "@/app/contexts/FavoritesContext";
import AuthService from "@/app/services/AuthService";
import Loading from "../loading";
export default function FavoritesPage() { export default function FavoritesPage() {
const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const { favorites, isLoading: favoritesLoading, removeFavorite } = useFavorites(); const { favorites, isLoading: favoritesLoading, removeFavorite } = useFavorites();
const [isAdmin, setIsAdmin] = useState(false);
useEffect(() => { useEffect(() => {
if (AuthService.isAdmin()) { // Admin check removed
router.push('/'); // if (AuthService.isAdmin()) {
return; // router.push('/');
} // return;
setIsAdmin(AuthService.isAdmin()); // }
// setIsAdmin(AuthService.isAdmin());
}, [router]); }, [router]);
const formatCurrency = (amount) => { const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ل.س'; return amount?.toLocaleString() + " " + t("currency-syp-suffix");
}; };
if (favoritesLoading && favorites.length === 0) { if (favoritesLoading && favorites.length === 0) {
return ( return <Loading />;
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-600">جاري التحميل...</p>
</div>
</div>
);
} }
return ( return (
<div className="min-h-screen bg-gray-50 py-8"> <div className="min-h-screen bg-gray-50 py-8">
<div className="container mx-auto px-4 max-w-6xl"> <div className="container mx-auto px-4 max-w-6xl">
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">المفضلة</h1> <h1 className="text-3xl font-bold text-gray-900 mb-2">{t("favorites")}</h1>
<p className="text-gray-600">العقارات التي قمت بحفظها</p> <p className="text-gray-600">{t("saved-properties")}</p>
</div> </div>
{favorites.length === 0 ? ( {favorites.length === 0 ? (
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"> <div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
<Heart className="w-16 h-16 text-gray-300 mx-auto mb-4" /> <Heart className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد عقارات في المفضلة</h3> <h3 className="text-xl font-bold text-gray-700 mb-2">{t("no-favorites")}</h3>
<p className="text-gray-500 mb-6">يمكنك إضافة العقارات التي تعجبك بالنقر على أيقونة القلب</p> <p className="text-gray-500 mb-6">{t("add-favorites-hint")}</p>
<Link <Link href="/properties" className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors">
href="/properties" {t("browse-properties")}
className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors"
>
استعرض العقارات
</Link> </Link>
</div> </div>
) : ( ) : (
@ -68,13 +61,7 @@ export default function FavoritesPage() {
> >
<div className="relative h-48 bg-gray-100"> <div className="relative h-48 bg-gray-100">
{property.images && property.images[0] ? ( {property.images && property.images[0] ? (
<Image <Image src={property.images[0]} alt={property.title} fill className="object-cover" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" />
src={property.images[0]}
alt={property.title}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
) : ( ) : (
<div className="w-full h-full flex items-center justify-center"> <div className="w-full h-full flex items-center justify-center">
<ImageIcon className="w-12 h-12 text-gray-400" /> <ImageIcon className="w-12 h-12 text-gray-400" />
@ -93,7 +80,7 @@ export default function FavoritesPage() {
<div> <div>
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs font-medium"> <span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs font-medium">
{property.type === 'apartment' ? 'شقة' : property.type === 'villa' ? 'فيلا' : 'بيت'} {property.type === "apartment" ? t("buildingType.apartment") : property.type === "villa" ? t("buildingType.villa") : t("buildingType.house")}
</span> </span>
</div> </div>
<h3 className="font-bold text-gray-900 mb-1 line-clamp-1">{property.title}</h3> <h3 className="font-bold text-gray-900 mb-1 line-clamp-1">{property.title}</h3>
@ -106,7 +93,7 @@ export default function FavoritesPage() {
</div> </div>
<div className="text-left"> <div className="text-left">
<div className="text-xl font-bold text-gray-900">{formatCurrency(property.price)}</div> <div className="text-xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
<div className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? 'يوم' : 'شهر'}</div> <div className="text-xs text-gray-500">/{property.priceUnit === "daily" ? t("day") : t("month")}</div>
</div> </div>
</div> </div>
@ -127,11 +114,8 @@ export default function FavoritesPage() {
</div> </div>
</div> </div>
<Link <Link href={`/property/${property.id}`} className="block w-full bg-amber-500 text-white py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors text-center">
href={`/property/${property.id}`} {t("viewDetails")}
className="block w-full bg-amber-500 text-white py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors text-center"
>
عرض التفاصيل
</Link> </Link>
</div> </div>
</motion.div> </motion.div>

View File

@ -1,14 +0,0 @@
'use client';
import { motion } from 'framer-motion';
export default function Loading() {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-400 text-lg">جاري التحميل...</p>
</motion.div>
</div>
);
}

View File

@ -1,30 +1,78 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { motion } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { useRouter } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { Mail, ArrowLeft, CheckCircle } from 'lucide-react'; import toast, { Toaster } from 'react-hot-toast';
import { Mail, Key, Lock, CheckCircle, ArrowLeft, RefreshCw } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { requestForgetPasswordOtp, verifyForgetPasswordOtp } from '../utils/api';
import InteractiveBackground from '../components/Animation/Background';
export default function ForgotPasswordPage() { export default function ForgotPasswordPage() {
const router = useRouter();
const { t } = useTranslation();
const [step, setStep] = useState(1);
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [code, setCode] = useState('');
const [newPassword, setNewPassword] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const handleSubmit = async (e) => { const handleRequestOtp = async (e) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true);
setTimeout(() => { if (!email.trim()) {
toast.error(t('forgotPassword.emailRequired'));
return;
}
setIsLoading(true);
try {
await requestForgetPasswordOtp(email);
toast.success(t('forgotPassword.otpSent'));
setStep(2);
} catch (err) {
toast.error(err?.message || t('forgotPassword.otpSendFailed'));
} finally {
setIsLoading(false); setIsLoading(false);
setIsSubmitted(true); }
}, 1500);
}; };
const handleVerifyOtp = async (e) => {
e.preventDefault();
if (!code.trim()) {
toast.error(t('forgotPassword.otpRequired'));
return;
}
if (newPassword.length < 6) {
toast.error(t('validation.passwordMinLength'));
return;
}
setIsLoading(true);
try {
await verifyForgetPasswordOtp(email, code, newPassword);
toast.success(t('forgotPassword.resetSuccess'));
setTimeout(() => router.push('/login'), 1200);
} catch (err) {
toast.error(err?.message || t('forgotPassword.otpVerifyFailed'));
} finally {
setIsLoading(false);
}
};
const inputClass = "w-full pr-12 pl-4 py-3 bg-white/10 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 transition-all";
return ( return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 flex items-center justify-center p-4 relative overflow-hidden"> <div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
<Toaster position="top-center" reverseOrder={false} />
<InteractiveBackground/>
<div className="absolute inset-0 overflow-hidden"> <div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-purple-500 rounded-full opacity-20 blur-3xl animate-pulse"></div> <div className="absolute -top-40 -right-40 w-80 h-80 bg-amber-400 rounded-full opacity-20 blur-3xl animate-pulse"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-pink-500 rounded-full opacity-20 blur-3xl animate-pulse delay-1000"></div> <div className="absolute -bottom-40 -left-40 w-80 h-80 bg-orange-400 rounded-full opacity-20 blur-3xl animate-pulse delay-1000"></div>
</div> </div>
<motion.div <motion.div
@ -38,89 +86,160 @@ export default function ForgotPasswordPage() {
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
className="absolute -top-16 left-0" className="absolute -top-16 left-0"
> >
<Link href="/login" className="flex items-center gap-2 text-gray-300 hover:text-white transition-colors group"> <Link href="/login" className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors group">
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" /> <ArrowLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
<span>العودة لتسجيل الدخول</span> <span>{t('forgotPassword.backToLogin')}</span>
</Link> </Link>
</motion.div> </motion.div>
<div className="bg-white/10 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/20 overflow-hidden"> <div className="bg-white/80 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/60 overflow-hidden">
<div className="bg-gradient-to-r from-purple-500 to-pink-500 p-8 text-center"> <div className="bg-gradient-to-l from-amber-500 to-amber-600 p-8 text-center">
<motion.h1 <motion.h1
initial={{ y: 20, opacity: 0 }} initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }} animate={{ y: 0, opacity: 1 }}
className="text-3xl font-bold text-white mb-2" className="text-3xl font-bold text-white mb-2"
> >
استعادة كلمة المرور {t('forgotPassword.title')}
</motion.h1> </motion.h1>
<motion.p <motion.p
initial={{ y: 20, opacity: 0 }} initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }} animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.1 }} transition={{ delay: 0.1 }}
className="text-purple-100" className="text-amber-100"
> >
سنرسل لك رابط لإعادة تعيين كلمة المرور {step === 1 ? t('forgotPassword.step1Instruction') : t('forgotPassword.step2Instruction')}
</motion.p> </motion.p>
</div> </div>
<div className="p-8"> <div className="p-8">
{!isSubmitted ? ( <AnimatePresence mode="wait">
<form onSubmit={handleSubmit} className="space-y-6"> {step === 1 && (
<div> <motion.form
<label className="block text-sm font-medium text-gray-200 mb-2"> key="step1"
البريد الإلكتروني initial={{ opacity: 0, x: -30 }}
</label> animate={{ opacity: 1, x: 0 }}
<div className="relative group"> exit={{ opacity: 0, x: 30 }}
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> onSubmit={handleRequestOtp}
<Mail className="w-5 h-5 text-gray-400 group-focus-within:text-purple-500 transition-colors" /> className="space-y-6"
>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{t('forgotPassword.emailLabel')}
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Mail className="w-5 h-5 text-gray-400 group-focus-within:text-amber-500 transition-colors" />
</div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className={inputClass}
placeholder={t('forgotPassword.emailPlaceholder')}
dir="ltr"
required
/>
</div> </div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-600 rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-400 transition-all"
placeholder="أدخل بريدك الإلكتروني"
required
/>
</div> </div>
</div>
<button <motion.button
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
className="w-full bg-gradient-to-r from-purple-500 to-pink-500 text-white py-3 rounded-xl font-bold text-lg hover:from-purple-600 hover:to-pink-600 transition-all transform hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-purple-500/25" whileHover={{ scale: isLoading ? 1 : 1.02 }}
whileTap={{ scale: isLoading ? 1 : 0.98 }}
className="w-full bg-gradient-to-l from-amber-500 to-amber-600 text-white py-3 rounded-xl font-bold text-lg hover:from-amber-600 hover:to-amber-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-amber-500/25"
>
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span>{t('forgotPassword.sending')}</span>
</div>
) : (
t('forgotPassword.sendOtp')
)}
</motion.button>
</motion.form>
)}
{step === 2 && (
<motion.form
key="step2"
initial={{ opacity: 0, x: 30 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -30 }}
onSubmit={handleVerifyOtp}
className="space-y-6"
> >
{isLoading ? ( <div>
<div className="flex items-center justify-center gap-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" /> {t('forgotPassword.otpLabel')}
<span>جاري الإرسال...</span> </label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Key className="w-5 h-5 text-gray-400 group-focus-within:text-amber-500 transition-colors" />
</div>
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
className={inputClass}
placeholder={t('forgotPassword.otpPlaceholder')}
dir="ltr"
required
/>
</div> </div>
) : ( </div>
'إرسال رابط الاستعادة'
)} <div>
</button> <label className="block text-sm font-medium text-gray-700 mb-2">
</form> {t('forgotPassword.newPasswordLabel')}
) : ( </label>
<motion.div <div className="relative group">
initial={{ scale: 0.9, opacity: 0 }} <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
animate={{ scale: 1, opacity: 1 }} <Lock className="w-5 h-5 text-gray-400 group-focus-within:text-amber-500 transition-colors" />
className="text-center py-6" </div>
> <input
<div className="w-20 h-20 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-4"> type="password"
<CheckCircle className="w-10 h-10 text-green-500" /> value={newPassword}
</div> onChange={(e) => setNewPassword(e.target.value)}
<h3 className="text-xl font-bold text-white mb-2">تم الإرسال بنجاح!</h3> className={inputClass}
<p className="text-gray-300 mb-6"> placeholder={t('forgotPassword.newPasswordPlaceholder')}
تم إرسال رابط استعادة كلمة المرور إلى {email} required
</p> minLength={6}
<Link />
href="/login" </div>
className="inline-block px-6 py-3 bg-white/10 text-white rounded-xl hover:bg-white/20 transition-colors" </div>
>
العودة لتسجيل الدخول <motion.button
</Link> type="submit"
</motion.div> disabled={isLoading}
)} whileHover={{ scale: isLoading ? 1 : 1.02 }}
whileTap={{ scale: isLoading ? 1 : 0.98 }}
className="w-full bg-gradient-to-l from-amber-500 to-amber-600 text-white py-3 rounded-xl font-bold text-lg hover:from-amber-600 hover:to-amber-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-amber-500/25"
>
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span>{t('forgotPassword.verifying')}</span>
</div>
) : (
t('forgotPassword.resetPassword')
)}
</motion.button>
<div className="text-center">
<button
type="button"
onClick={() => setStep(1)}
className="text-sm text-amber-600 hover:text-amber-700 transition-colors flex items-center justify-center gap-1 mx-auto"
>
<RefreshCw className="w-4 h-4" />
<span>{t('forgotPassword.changeEmail')}</span>
</button>
</div>
</motion.form>
)}
</AnimatePresence>
</div> </div>
</div> </div>
</motion.div> </motion.div>

View File

@ -1,5 +1,7 @@
@import "tailwindcss"; @import "tailwindcss";
@custom-variant dark (&:is(.dark *));
/* ─── Madani Arabic Font ─── */ /* ─── Madani Arabic Font ─── */
@font-face { @font-face {
font-family: 'Madani Arabic'; font-family: 'Madani Arabic';
@ -92,6 +94,26 @@
} }
} }
.dark {
--background: #0f172a;
--foreground: #f5e6d3;
color-scheme: dark;
}
.dark {
--color-white: #1e293b;
--color-gray-50: #0f172a;
--color-gray-100: #1e293b;
--color-gray-200: #334155;
--color-gray-300: #475569;
--color-gray-400: #64748b;
--color-gray-500: #94a3b8;
--color-gray-600: #cbd5e1;
--color-gray-700: #e2e8f0;
--color-gray-800: #f1f5f9;
--color-gray-900: #f8fafc;
}
html, body { html, body {
font-family: 'Madani Arabic', 'Noto Sans Arabic', 'Cairo', Arial, sans-serif; font-family: 'Madani Arabic', 'Noto Sans Arabic', 'Cairo', Arial, sans-serif;
} }
@ -135,6 +157,13 @@ body {
transform: scale(1.1); transform: scale(1.1);
} }
.nav-icon {
color: #1e293b;
}
.dark .nav-icon {
color: #ffffff;
}
.property-tooltip { .property-tooltip {
background: white !important; background: white !important;
color: #1f2937 !important; color: #1f2937 !important;

View File

@ -0,0 +1,57 @@
import { useTranslation } from "react-i18next";
export const useFilterOptions = () => {
const { t } = useTranslation();
const cities = [
{ id: "all", label: t("allCities") },
{ id: "دمشق", label: t("damascus") },
{ id: "حلب", label: t("aleppo") },
{ id: "حمص", label: t("homs") },
{ id: "اللاذقية", label: t("latakia") },
{ id: "درعا", label: t("daraa") },
];
const propertyTypes = [
{ id: "all", label: t("all") },
{ id: "apartment", label: t("residentialApartments") },
{ id: "studio", label: t("studio") },
{ id: "commercial", label: t("commercialProperty") },
{ id: "villa", label: t("villaFarm") },
];
const priceRanges = [
{ id: "all", label: t("allPrices") },
{ id: "0-500", label: t("priceRange1") },
{ id: "500-1000", label: t("priceRange2") },
{ id: "1000-2000", label: t("priceRange3") },
{ id: "2000-3000", label: t("priceRange4") },
{ id: "3000+", label: t("priceRange5") },
];
const identityTypes = [
{ id: "syrian", label: t("syrianId") },
{ id: "passport", label: t("passport") },
];
const ownerSources = [
{ id: "all", label: t("all") },
{ id: "owner", label: t("fromOwner") },
{ id: "agency", label: t("fromAgency") },
];
const rentPeriods = [
{ id: "all", label: t("all") },
{ id: "daily", label: t("dailyRent") },
{ id: "monthly", label: t("monthlyRent") },
];
return {
cities,
propertyTypes,
priceRanges,
identityTypes,
ownerSources,
rentPeriods,
};
};

View File

@ -0,0 +1,99 @@
import { useEffect, useState } from "react";
import { getRentProperties, getSaleProperties } from ".././utils/api";
import { mapApiProperty } from "../HelperFunction/ApiProperty";
import { useTranslation } from "react-i18next";
export default function useApplyFilters() {
const [allProperties, setAllProperties] = useState([]);
const [rentProperties, setRentProperties] = useState([]);
const [saleProperties, setSaleProperties] = useState([]);
const [searchFilters, setSearchFilters] = useState(null);
const [filteredProperties, setFilteredProperties] = useState([]);
const { t } = useTranslation();
useEffect(() => {
async function fetchProperties() {
try {
const [rentData, saleData] = await Promise.all([getRentProperties().catch(() => []), getSaleProperties().catch(() => [])]);
const rentList = Array.isArray(rentData) ? rentData : [];
const saleList = Array.isArray(saleData) ? saleData : [];
const mappedRent = rentList.map((p, i) => mapApiProperty(t, p, i));
const mappedSale = saleList.map((p, i) => mapApiProperty(t, p, rentList.length + i));
setRentProperties(mappedRent);
setSaleProperties(mappedSale);
setAllProperties([...mappedRent, ...mappedSale]);
} catch (err) {
console.error("[Home] Failed to fetch properties:", err);
} finally {
}
}
fetchProperties();
}, []);
const applyFilters = (filters) => {
setSearchFilters(filters);
let propertiesToFilter = [];
if (filters.mode === "rent") {
propertiesToFilter = rentProperties;
} else if (filters.mode === "buy" || filters.mode === "sell") {
propertiesToFilter = saleProperties;
} else {
propertiesToFilter = allProperties;
}
const filtered = propertiesToFilter.filter((property) => {
if (filters.city && filters.city !== "all" && property.location.city !== filters.city) {
return false;
}
if (filters.propertyType && filters.propertyType !== "all" && property.type !== filters.propertyType) {
return false;
}
if (filters.priceRange && filters.priceRange !== "all") {
const priceUSD = property.priceUSD;
switch (filters.priceRange) {
case "0-500":
if (priceUSD > 500) return false;
break;
case "500-1000":
if (priceUSD < 501 || priceUSD > 1000) return false;
break;
case "1000-2000":
if (priceUSD < 1001 || priceUSD > 2000) return false;
break;
case "2000-3000":
if (priceUSD < 2001 || priceUSD > 300) return false;
break;
case "3000+":
if (priceUSD < 3001) return false;
break;
}
}
if (filters.ownerSource && filters.ownerSource !== "all") {
if (filters.ownerSource === "owner" && property.ownerSource !== "owner") return false;
if (filters.ownerSource === "agency" && property.ownerSource !== "agency") return false;
}
if (filters.rentPeriod && filters.rentPeriod !== "all" && property.listingType === "rent") {
if (filters.rentPeriod === "daily" && !property.priceDisplay.daily) return false;
if (filters.rentPeriod === "monthly" && !property.priceDisplay.monthly) return false;
}
if (filters.availableToday) {
if (property.status !== "available") return false;
}
if (filters.identityType && property.allowedIdentities) {
if (!property.allowedIdentities.includes(filters.identityType)) {
return false;
}
}
return true;
});
setFilteredProperties(filtered);
};
const resetFilters = () => {
setSearchFilters(null);
setFilteredProperties([]);
};
return { filteredProperties, searchFilters, applyFilters, resetFilters };
}

18
app/hooks/useAuth.js Normal file
View File

@ -0,0 +1,18 @@
// app/hooks/useAuth.js
import AuthService from "../services/AuthService";
export default function useAuth() {
const authUser = AuthService.getUser();
const role = authUser?.roles;
return {
user: authUser,
name: authUser?.name ?? "Not found",
email: authUser?.email ?? "Not found",
isOwner: role?.includes("Owner"),
isAgent: role?.includes("RealEstateAgent",""),
isCustomer: role?.includes("Customer"),
role: role,
isGuest: !authUser,
isAuthenticated: !!authUser,
};
}

File diff suppressed because it is too large Load Diff

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