Compare commits

..

171 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
158 changed files with 23823 additions and 14509 deletions

View File

@ -10,7 +10,7 @@ jobs:
- name: Clone repository
uses: actions/checkout@v2
with:
repository: Rahaf/SweetHome
repository: amer2004/SweetHome
github-server-url: http://45.93.137.91:3000
- 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 Image from "next/image";
import { NavLink, MobileNavLink } from "./components/NavLinks";
import { FavoritesProvider } from '@/app/contexts/FavoritesContext';
import FloatingSidebar from '@/app/components/FloatingSidebar';
import BottomNav from "./components/BottomNav";
import {
Globe,
LogIn,
@ -17,24 +16,17 @@ import {
Building,
PlusCircle,
Heart,
MessageCircle,
Settings,
Edit,
Phone,
Mail,
MapPin,
Camera,
Shield,
Bell,
Home,
ChevronDown,
Menu,
X,
TrendingUp,
CalendarDays,
Clock,
Users,
DollarSign,
Star,
FileText,
} from "lucide-react";
import { useState, useEffect, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
@ -43,6 +35,9 @@ import { UserRole, UserRoleLabels } from "./enums/UserRole";
import "./i18n/config";
import NotificationHandler from "./components/NotificationHandler";
import { useNotifications } from "@/app/contexts/NotificationsContext";
import Loading from "./loading";
export default function ClientLayout({ children }) {
const { t, i18n } = useTranslation();
const pathname = usePathname();
@ -54,6 +49,8 @@ export default function ClientLayout({ children }) {
const currentYear = new Date().getFullYear();
const menuRef = useRef(null);
const { unreadCount } = useNotifications();
useEffect(() => {
setIsMounted(true);
const savedLanguage = localStorage.getItem("language") || "ar";
@ -77,9 +74,7 @@ export default function ClientLayout({ children }) {
name: authUser.name || authUser.email,
email: authUser.email,
phone: authUser.phone,
role: AuthService.isAdmin() ? UserRole.ADMIN
: AuthService.isOwner() ? UserRole.OWNER
: UserRole.CUSTOMER,
role: AuthService.isOwner() ? UserRole.OWNER : AuthService.isAgent() ? UserRole.AGENT : UserRole.CUSTOMER,
});
} else {
setUser(null);
@ -125,17 +120,14 @@ export default function ClientLayout({ children }) {
window.location.href = "/";
};
const isAuthPage = [
"/login",
"/register",
"/forgot-password",
"/auth/choose-role",
].includes(pathname);
const isAuthPage = ["/login", "/blocked", "/register", "/forgot-password", "/auth/choose-role"].includes(pathname);
const isProfilePage = pathname === "/profile";
const isHomePage = pathname === "/";
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 isAuthenticated = !!user;
@ -148,122 +140,99 @@ export default function ClientLayout({ children }) {
if (!isMounted) {
return (
<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"></div>
<p className="text-gray-600">جاري التحميل...</p>
</div>
</div>
<Loading/>
);
}
return (
<>
{!isAuthPage && (
<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">
<div className="min-h-screen flex flex-col">
{!isAuthPage && !isAuthenticated && (
<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={`flex justify-between items-center h-20 ${currentLanguage === "ar" ? "flex-row-reverse" : ""}`}
>
<div className={`flex justify-between items-center h-20 ${currentLanguage === "ar" ? "flex-row-reverse" : ""}`}>
<div className="flex items-center">
<Link href="/" className="flex items-center space-x-3 group">
<div className="relative w-[150px] h-[60px]">
<Image
src="/logo.png"
alt={t("logoAlt")}
fill
className="object-contain"
priority
/>
<Image src="/logo.png" alt={t("logoAlt")} fill className="object-contain" priority />
</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")}
<span className="text-amber-600">
{t("brandNamePart2")}
</span>
<span className={isHomePage ? "text-amber-400" : "text-amber-600"}>{t("brandNamePart2")}</span>
</span>
</Link>
</div>
<div className="hidden md:flex items-center space-x-4">
<div
className={`flex items-center space-x-1 ${currentLanguage === "ar" ? "flex-row-reverse space-x-reverse" : ""}`}
>
<div className={`flex items-center space-x-1 ${currentLanguage === "ar" ? "flex-row-reverse space-x-reverse" : ""}`}>
{/* Download App Dropdown */}
<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">
<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="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" />
</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>
<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>
<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">
<a href="/files/SweetHome.apk" download
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-green-50 transition-colors">
<a href="/files/SweetHome.apk" download 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">
<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>
</div>
<div>
<p className="font-semibold text-gray-900 text-sm">Android</p>
<p className="text-xs text-green-600">تحميل APK</p>
<p className="font-semibold text-gray-900 text-sm">{t("android")}</p>
<p className="text-xs text-green-600">{t("downloadAPK")}</p>
</div>
</a>
<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">
<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>
</div>
<div>
<p className="font-semibold text-gray-400 text-sm">iOS</p>
<p className="text-xs text-gray-400">قريباً</p>
<p className="font-semibold text-gray-400 text-sm">{t("ios")}</p>
<p className="text-xs text-gray-400">{t("comingSoon")}</p>
</div>
</div>
</div>
</div>
</div>
<NavLink href="/">الرئيسية</NavLink>
<NavLink href="/properties">عقاراتنا</NavLink>
{isAdmin && (
<NavLink href="/admin">
<span className="flex items-center gap-2">
<Shield className="w-4 h-4" />
الإدارة
</span>
</NavLink>
)}
<NavLink light={isHomePage} href="/">
{t("home")}
</NavLink>
<NavLink light={isHomePage} href="/properties">
{t("ourProperties")}
</NavLink>
{isOwner && (
<>
<NavLink href="/owner/properties">
<NavLink light={isHomePage} href="/owner/properties">
<span className="flex items-center gap-2">
<Building className="w-4 h-4" />
عقاراتي
{t("myProperties")}
</span>
</NavLink>
<NavLink href="/owner/reservations">
<NavLink light={isHomePage} href="/owner/reservations">
<span className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
الحجوزات
{t("reservations")}
</span>
</NavLink>
{/* <NavLink href="/owner/calendar">
<span className="flex items-center gap-2">
<CalendarDays className="w-4 h-4" />
التقويم
</span>
</NavLink> */}
<NavLink href="/owner/profits">
<NavLink light={isHomePage} href="/owner/profits">
<span className="flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
الأرباح
{t("profits")}
</span>
</NavLink>
</>
@ -271,16 +240,16 @@ export default function ClientLayout({ children }) {
{!user && !isAuthPage && (
<>
<NavLink href="/login">
<NavLink light={isHomePage} href="/login">
<span className="flex items-center gap-2">
<LogIn className="w-4 h-4" />
تسجيل الدخول
{t("login")}
</span>
</NavLink>
<NavLink href="/auth/choose-role">
<NavLink light={isHomePage} href="/auth/choose-role">
<span className="flex items-center gap-2">
<UserPlus className="w-4 h-4" />
إنشاء حساب
{t("createAccount")}
</span>
</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="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">
{getUserInitial()}
</div>
<div className="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center text-xl font-bold">{getUserInitial()}</div>
<div>
<p className="font-bold">
{user?.name || "مستخدم"}
</p>
<p className="text-xs text-amber-100">
{user?.email || ""}
</p>
<p className="text-xs text-amber-100 mt-1">
{UserRoleLabels[user?.role] || 'زائر'}
</p>
<p className="font-bold">{user?.name || t("userLabel")}</p>
<p className="text-xs text-amber-100">{user?.email || ""}</p>
<p className="text-xs text-amber-100 mt-1">{t(UserRoleLabels[user?.role] ? `userRole.${user?.role.toLowerCase()}` : "visitor")}</p>
</div>
</div>
</div>
<div className="p-2">
<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)}
>
<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)}>
<UserCircle className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">الملف الشخصي</p>
<p className="text-xs text-gray-500">
عرض وتعديل معلوماتك
</p>
<p className="font-medium">{t("profile")}</p>
<p className="text-xs text-gray-500">{t("viewAndEditInfo")}</p>
</div>
</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>
</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"
onClick={() => setShowUserMenu(false)}
>
<Heart className="w-5 h-5 text-amber-500" />
<CalendarDays className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">المفضلة</p>
<p className="text-xs text-gray-500">
العقارات المحفوظة
</p>
<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="/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>
</Link>
@ -376,10 +353,8 @@ export default function ClientLayout({ children }) {
>
<Building className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">عقاراتي</p>
<p className="text-xs text-gray-500">
إدارة عقاراتك
</p>
<p className="font-medium">{t("myProperties")}</p>
<p className="text-xs text-gray-500">{t("manageYourProperties")}</p>
</div>
</Link>
@ -390,10 +365,8 @@ export default function ClientLayout({ children }) {
>
<PlusCircle className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">إضافة عقار</p>
<p className="text-xs text-gray-500">
أضف عقاراً جديداً
</p>
<p className="font-medium">{t("addProperty")}</p>
<p className="text-xs text-gray-500">{t("addNewProperty")}</p>
</div>
</Link>
@ -404,10 +377,8 @@ export default function ClientLayout({ children }) {
>
<Calendar className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">الحجوزات</p>
<p className="text-xs text-gray-500">
إدارة حجوزاتك
</p>
<p className="font-medium">{t("reservations")}</p>
<p className="text-xs text-gray-500">{t("manageReservations")}</p>
</div>
</Link>
@ -418,10 +389,8 @@ export default function ClientLayout({ children }) {
>
<CalendarDays className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">التقويم</p>
<p className="text-xs text-gray-500">
جدول توفر العقارات
</p>
<p className="font-medium">{t("calendar")}</p>
<p className="text-xs text-gray-500">{t("propertyAvailability")}</p>
</div>
</Link>
@ -432,86 +401,20 @@ export default function ClientLayout({ children }) {
>
<TrendingUp className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">الأرباح</p>
<p className="text-xs text-gray-500">
إحصائيات وأرباح
</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>
<p className="font-medium">{t("profits")}</p>
<p className="text-xs text-gray-500">{t("statisticsAndProfits")}</p>
</div>
</Link>
<Link
href="/admin/users"
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"
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)}
>
<DollarSign className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">دفتر الحسابات</p>
<p className="text-xs text-gray-500">
إدارة المعاملات المالية
</p>
<p className="font-medium">{t("accountBook")}</p>
<p className="text-xs text-gray-500">{t("financialRecords")}</p>
</div>
</Link>
</>
@ -528,10 +431,40 @@ export default function ClientLayout({ children }) {
>
<Calendar className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">حجوزاتي</p>
<p className="text-xs text-gray-500">
عرض حجوزاتك
</p>
<p className="font-medium">{t("myBookings")}</p>
<p className="text-xs text-gray-500">{t("viewBookings")}</p>
</div>
</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>
</Link>
</>
@ -539,16 +472,11 @@ export default function ClientLayout({ children }) {
<div className="border-t border-gray-100 my-2"></div>
<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"
>
<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">
<LogOut className="w-5 h-5" />
<div>
<p className="font-medium">تسجيل الخروج</p>
<p className="text-xs text-red-400">
إنهاء الجلسة الحالية
</p>
<p className="font-medium">{t("logout")}</p>
<p className="text-xs text-red-400">{t("endSession")}</p>
</div>
</button>
</div>
@ -563,9 +491,7 @@ export default function ClientLayout({ children }) {
<motion.button
whileHover={{ scale: 1.1, rotate: 360 }}
whileTap={{ scale: 0.9 }}
onClick={() =>
changeLanguage(currentLanguage === "en" ? "ar" : "en")
}
onClick={() => 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"
>
<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"
>
<span className="sr-only">{t("openMainMenu")}</span>
{isMobileMenuOpen ? (
<X className="w-6 h-6" />
) : (
<Menu className="w-6 h-6" />
)}
{isMobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
</div>
@ -601,79 +523,66 @@ export default function ClientLayout({ children }) {
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
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">
<MobileNavLink href="/" onClick={closeMobileMenu}>
<MobileNavLink light={isHomePage} href="/" onClick={closeMobileMenu}>
{t("home")}
</MobileNavLink>
<MobileNavLink href="/properties" onClick={closeMobileMenu}>
<MobileNavLink light={isHomePage} href="/properties" onClick={closeMobileMenu}>
{t("ourProducts")}
</MobileNavLink>
{/* Download App - Mobile */}
<div className="border-t border-gray-200 my-2"></div>
<p className="px-3 py-1 text-xs text-gray-400 font-medium">تحميل التطبيق</p>
<a href="/files/SweetHome.apk" download onClick={closeMobileMenu}
className="flex items-center gap-2 px-3 py-2 rounded-md text-green-600 hover:bg-green-50 transition-colors">
<div className={`border-t my-2 ${isHomePage ? "border-white/10" : "border-gray-200"}`}></div>
<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}
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">
<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>
<span className="text-sm font-medium">Android - تحميل APK</span>
<span className="text-sm font-medium">
{t("android")} - {t("downloadAPK")}
</span>
</a>
<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">
<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>
<span className="text-sm">iOS - قريباً</span>
<span className="text-sm">
{t("ios")} - {t("comingSoon")}
</span>
</div>
<div className="border-t border-gray-200 my-2"></div>
{isAdmin && (
<MobileNavLink href="/admin" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<Shield className="w-4 h-4" />
الإدارة
</span>
</MobileNavLink>
)}
<div className={`border-t my-2 ${isHomePage ? "border-white/10" : "border-gray-200"}`}></div>
{isOwner && (
<>
<MobileNavLink
href="/owner/properties"
onClick={closeMobileMenu}
>
<MobileNavLink light={isHomePage} href="/owner/properties" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<Building className="w-4 h-4" />
عقاراتي
{t("myProperties")}
</span>
</MobileNavLink>
<MobileNavLink
href="/owner/reservations"
onClick={closeMobileMenu}
>
<MobileNavLink light={isHomePage} href="/owner/reservations" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
الحجوزات
{t("reservations")}
</span>
</MobileNavLink>
<MobileNavLink
href="/owner/calendar"
onClick={closeMobileMenu}
>
<MobileNavLink light={isHomePage} href="/owner/calendar" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<CalendarDays className="w-4 h-4" />
التقويم
{t("calendar")}
</span>
</MobileNavLink>
<MobileNavLink
href="/owner/profits"
onClick={closeMobileMenu}
>
<MobileNavLink light={isHomePage} href="/owner/profits" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
الأرباح
{t("profits")}
</span>
</MobileNavLink>
</>
@ -681,20 +590,17 @@ export default function ClientLayout({ children }) {
{!user && (
<>
<div className="border-t border-gray-200 my-2"></div>
<MobileNavLink href="/login" onClick={closeMobileMenu}>
<div className={`border-t my-2 ${isHomePage ? "border-white/10" : "border-gray-200"}`}></div>
<MobileNavLink light={isHomePage} href="/login" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<LogIn className="w-4 h-4" />
تسجيل الدخول
{t("login")}
</span>
</MobileNavLink>
<MobileNavLink
href="/auth/choose-role"
onClick={closeMobileMenu}
>
<MobileNavLink light={isHomePage} href="/auth/choose-role" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<UserPlus className="w-4 h-4" />
إنشاء حساب
{t("createAccount")}
</span>
</MobileNavLink>
</>
@ -705,76 +611,108 @@ export default function ClientLayout({ children }) {
</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
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>
{children}
<FloatingSidebar isRTL={currentLanguage === 'ar'} isAdmin={isAdmin} />
</FavoritesProvider>
{!isAuthPage && isAuthenticated && (
<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"}`}>
<div className="flex items-center gap-2">
<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>
{!isAuthPage && !isProfilePage && (
<footer className="bg-gray-900 text-white py-12">
{pathname === "/" && (
<footer className="py-20" style={{backgroundColor:"#0f172a"}}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div
className={`grid grid-cols-1 md:grid-cols-4 gap-8 ${currentLanguage === "ar" ? "text-right" : "text-left"}`}
>
<div className={`grid grid-cols-1 md:grid-cols-4 gap-8 ${currentLanguage === "ar" ? "text-right" : "text-left"}`}>
<div className="space-y-4">
<div
className={`flex items-center ${currentLanguage === "ar" ? "flex-row-reverse" : "space-x-3"}`}
>
<div className={`flex items-center ${currentLanguage === "ar" ? "flex-row-reverse" : "space-x-3"}`}>
<div className="relative w-10 h-10">
<Image
src="/logo.png"
alt={t("logoAlt")}
fill
className="object-contain"
sizes="40px"
/>
<Image src="/logo.png" alt={t("logoAlt")} fill className="object-contain" sizes="40px" />
</div>
<span className="text-3xl font-bold">
{t("brandNamePart1")}
<span className="text-amber-400">
{t("brandNamePart2")}
</span>
<span className="text-amber-400">{t("brandNamePart2")}</span>
</span>
</div>
<p className="text-gray-400 text-sm">
{t("footerDescription")}
</p>
<p className="text-gray-400 text-sm">{t("footerDescription")}</p>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">
{t("quickLinks")}
</h3>
<h3 className="text-lg font-semibold mb-4">{t("quickLinks")}</h3>
<ul className="space-y-2">
<li>
<Link
href="/"
className="text-gray-400 hover:text-white transition-colors block py-1"
>
<Link href="/" className="text-gray-400 hover:text-white transition-colors block py-1">
{t("home")}
</Link>
</li>
<li>
<Link
href="/properties"
className="text-gray-400 hover:text-white transition-colors block py-1"
>
<Link href="/properties" className="text-gray-400 hover:text-white transition-colors block py-1">
{t("ourProducts")}
</Link>
</li>
{isAdmin && (
<li>
<Link
href="/admin"
className="text-gray-400 hover:text-white transition-colors block py-1"
>
الإدارة
</Link>
</li>
)}
</ul>
</div>
<div>
@ -782,7 +720,9 @@ export default function ClientLayout({ children }) {
<ul className="space-y-3 text-gray-400">
<li className="flex items-center gap-2">
<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 className="flex items-center gap-2">
<Mail className="w-5 h-5" />
@ -800,6 +740,6 @@ export default function ClientLayout({ children }) {
</footer>
)}
<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,113 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
import AuthService from '@/app/services/AuthService';
import Link from 'next/link';
export default function AddAdminPage() {
const [isAdmin, setIsAdmin] = useState(false);
const [checked, setChecked] = useState(false);
const [formState, setFormState] = useState({ fullName: '', email: '', password: '' });
const [saved, setSaved] = useState(false);
useEffect(() => {
setIsAdmin(AuthService.isAuthenticated() && AuthService.isAdmin());
setChecked(true);
}, []);
const handleChange = (field) => (event) => {
setFormState((prev) => ({ ...prev, [field]: event.target.value }));
};
const handleSubmit = (event) => {
event.preventDefault();
setSaved(true);
console.log('Add admin payload', formState);
};
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>
);
}
if (!isAdmin) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="max-w-md text-center bg-white rounded-3xl shadow-lg border border-gray-200 p-8">
<Link href="/" className="inline-flex items-center justify-center px-6 py-3 rounded-full bg-amber-500 text-white hover:bg-amber-600 transition-colors">
العودة للرئيسية
</Link>
</div>
</div>
);
}
return (
<main className="min-h-screen bg-slate-50 p-6 md:p-10">
<div className="max-w-4xl mx-auto">
<div className="flex items-center justify-between mb-8">
<div>
<p className="text-sm text-amber-600 uppercase tracking-[0.2em]">لوحة المدير</p>
<h1 className="text-3xl font-bold text-slate-900 mt-3">إضافة مدير جديد</h1>
<p className="text-slate-500 mt-2">انشئ حساب مسؤول جديد مع صلاحيات الإدارة.</p>
</div>
</div>
<div className="grid gap-6 md:grid-cols-[1.5fr_0.8fr]">
<section className="rounded-[28px] bg-white p-8 shadow-sm border border-slate-200">
<h2 className="text-xl font-semibold mb-6">بيانات المدير</h2>
<form onSubmit={handleSubmit} className="space-y-5">
<label className="block">
<span className="text-sm font-medium text-slate-700">الاسم الكامل</span>
<input
type="text"
value={formState.fullName}
onChange={handleChange('fullName')}
className="mt-2 w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 outline-none focus:border-amber-500 focus:ring-2 focus:ring-amber-100"
placeholder="مثال: محمد الأحمد"
required
/>
</label>
<label className="block">
<span className="text-sm font-medium text-slate-700">البريد الإلكتروني</span>
<input
type="email"
value={formState.email}
onChange={handleChange('email')}
className="mt-2 w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 outline-none focus:border-amber-500 focus:ring-2 focus:ring-amber-100"
placeholder="admin@example.com"
required
/>
</label>
<label className="block">
<span className="text-sm font-medium text-slate-700">كلمة المرور</span>
<input
type="password"
value={formState.password}
onChange={handleChange('password')}
className="mt-2 w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 outline-none focus:border-amber-500 focus:ring-2 focus:ring-amber-100"
placeholder="••••••••"
required
/>
</label>
<button type="submit" className="inline-flex items-center justify-center rounded-2xl bg-amber-600 px-6 py-3 text-white font-semibold shadow-lg shadow-amber-100 transition hover:bg-amber-700">
حفظ المدير الجديد
</button>
</form>
{saved && (
<div className="mt-6 rounded-3xl bg-emerald-50 border border-emerald-200 p-4 text-emerald-700">
تم حفظ بيانات المدير بنجاح
</div>
)}
</section>
</div>
</div>
</main>
);
}

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

@ -1,85 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
import AuthService from '@/app/services/AuthService';
import Link from 'next/link';
const initialPolicy = `1. نحترم خصوصيتك ونلتزم بحماية بياناتك الشخصية.
2. يتم استخدام المعلومات لتحسين تجربة المستخدم وتأمين الخدمة.
3. لا نشارك البيانات مع أطراف خارجية بدون موافقتك.
4. يمكنك طلب حذف بياناتك من النظام في أي وقت.`;
export default function PrivacyPolicyAdminPage() {
const [isAdmin, setIsAdmin] = useState(false);
const [checked, setChecked] = useState(false);
const [policyText, setPolicyText] = useState(initialPolicy);
const [saved, setSaved] = useState(false);
useEffect(() => {
setIsAdmin(AuthService.isAuthenticated() && AuthService.isAdmin());
setChecked(true);
}, []);
const handleSave = (event) => {
event.preventDefault();
setSaved(true);
console.log('Privacy policy updated:', policyText);
};
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>
);
}
if (!isAdmin) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="max-w-md text-center bg-white rounded-3xl shadow-lg border border-gray-200 p-8">
<p className="text-gray-600 mb-6">هذه الصفحة لتحرير سياسة الخصوصية ولا يمكن الوصول إليها إلا للمدير.</p>
<Link href="/" className="inline-flex items-center justify-center px-6 py-3 rounded-full bg-amber-500 text-white hover:bg-amber-600 transition-colors">
العودة للرئيسية
</Link>
</div>
</div>
);
}
return (
<main className="min-h-screen bg-slate-50 p-6 md:p-10">
<div className="max-w-4xl mx-auto">
<div className="mb-8 rounded-[28px] bg-white p-8 shadow-sm border border-slate-200">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-sm text-amber-600 uppercase tracking-[0.2em]">لوحة المدير</p>
<p className="text-slate-500 mt-2">قم بتحديث نص سياسة الخصوصية</p>
</div>
</div>
</div>
<form onSubmit={handleSave} className="space-y-6 rounded-[28px] bg-white p-8 shadow-sm border border-slate-200">
<div>
<label className="block text-sm font-medium text-slate-700 mb-3">نص سياسة الخصوصية</label>
<textarea
value={policyText}
onChange={(e) => setPolicyText(e.target.value)}
rows={12}
className="w-full rounded-3xl border border-slate-200 bg-slate-50 px-5 py-4 text-slate-700 outline-none focus:border-amber-500 focus:ring-2 focus:ring-amber-100"
/>
</div>
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<button type="submit" className="rounded-2xl bg-amber-600 px-6 py-3 text-white font-semibold shadow-lg shadow-amber-100 transition hover:bg-amber-700">
حفظ السياسة
</button>
</div>
{saved && (
<div className="rounded-3xl bg-emerald-50 border border-emerald-200 p-4 text-emerald-700">
تمت حفظ سياسة الخصوصية بنجاح
</div>
)}
</form>
</div>
</main>
);
}

View File

@ -6,7 +6,7 @@ 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">
<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">
<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" />

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 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() {
const { t, i18n } = useTranslation();
const containerVariants = {
hidden: { opacity: 0 },
visible: {
@ -35,7 +38,8 @@ export default function ChooseRolePage() {
};
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">
{[...Array(20)].map((_, i) => (
<motion.div
@ -77,7 +81,7 @@ export default function ChooseRolePage() {
<motion.div whileHover={{ x: -5 }}>
<ArrowLeft className="w-4 h-4" />
</motion.div>
<span>العودة للرئيسية</span>
<span>{t('backToHome')}</span>
</Link>
</motion.div>
@ -86,17 +90,17 @@ export default function ChooseRolePage() {
className="text-center mb-12"
>
<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}
>
مرحباً بك في SweetHome
{t('chooseRole.welcomeToSweetHome')}
</motion.h1>
<p className="text-xl text-gray-400">
اختر نوع الحساب المناسب لك
{t('chooseRole.subtitle')}
</p>
</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
variants={itemVariants}
whileHover={{ scale: 1.05, y: -5 }}
@ -123,23 +127,23 @@ export default function ChooseRolePage() {
<Building className="w-10 h-10 text-white" />
</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">
تريد عرض عقار للإيجار أو البيع؟ انضم كمالك عقار وابدأ الآن
{t('chooseRole.ownerDesc')}
</p>
<ul className="space-y-2 text-sm text-amber-100">
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
إضافة عقارات غير محدودة
{t('chooseRole.ownerFeature1')}
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
إدارة الحجوزات بسهولة
{t('chooseRole.ownerFeature2')}
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
تواصل مباشر مع المستأجرين
{t('chooseRole.ownerFeature3')}
</li>
</ul>
@ -148,7 +152,7 @@ export default function ChooseRolePage() {
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
إنشاء حساب مالك
{t('chooseRole.createOwnerAccount')}
</motion.button>
</div>
</div>
@ -181,23 +185,23 @@ export default function ChooseRolePage() {
<Home className="w-10 h-10 text-white" />
</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">
تبحث عن عقار للإيجار أو الشراء؟ انضم كعميل وابحث عن منزل أحلامك
{t('chooseRole.tenantDesc')}
</p>
<ul className="space-y-2 text-sm text-blue-100">
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
آلاف العقارات المتاحة
{t('chooseRole.tenantFeature1')}
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
بحث متقدم بفلاتر ذكية
{t('chooseRole.tenantFeature2')}
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
حجز وإستئجار فوري
{t('chooseRole.tenantFeature3')}
</li>
</ul>
@ -206,7 +210,65 @@ export default function ChooseRolePage() {
whileHover={{ scale: 1.02 }}
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>
</div>
</div>
@ -218,12 +280,12 @@ export default function ChooseRolePage() {
variants={itemVariants}
className="text-center text-gray-400 mt-8"
>
لديك حساب بالفعل؟{' '}
{t('chooseRole.alreadyHaveAccount')}{' '}
<Link
href="/login"
className="text-amber-400 hover:text-amber-300 font-medium transition-colors"
>
تسجيل الدخول
{t('login')}
</Link>
</motion.p>
</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 { motion } from 'framer-motion';
import Link from 'next/link';
import { Heart, Bell, CreditCard, Shield, UserPlus } from 'lucide-react';
import { Heart, Bell, CreditCard, Settings } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useFavorites } from '@/app/contexts/FavoritesContext';
import { useNotifications } from '@/app/contexts/NotificationsContext';
import AuthService from '@/app/services/AuthService';
export default function FloatingSidebar({ isRTL, isAdmin }) {
export default function FloatingSidebar({ isRTL }) {
const { t } = useTranslation();
const { favorites } = useFavorites();
const { unreadCount } = useNotifications();
const [tooltip, setTooltip] = useState(null);
let timeoutId = null;
@ -22,15 +27,16 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
setTooltip(null);
};
const side = isRTL ? 'left' : 'right';
const positionStyle = {
[side]: 0,
if (!AuthService.isAuthenticated()) return null;
const positionStyle = {
left: '16px',
top: '50%',
transform: 'translateY(-50%)',
};
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' } },
};
@ -43,143 +49,111 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
const renderTooltip = (id, label) => {
if (tooltip !== id) return null;
return (
<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">
{label}
<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 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 (
<motion.div
className="fixed z-50"
className="fixed z-40 pointer-events-none"
style={positionStyle}
variants={cardVariants}
initial="initial"
animate="animate"
>
<div className="bg-white/90 backdrop-blur-md rounded-2xl shadow-lg border border-gray-200/60 py-3 px-2 flex flex-col gap-3 transition-all duration-300 hover:shadow-xl hover:bg-white/95">
{isAdmin ? (
<>
<motion.div
className="relative group"
variants={buttonVariants}
initial="rest"
whileHover="hover"
whileTap="tap"
onMouseEnter={() => showTooltip('addAdmin')}
onMouseLeave={hideTooltip}
>
<Link
href="/admin/add-admin"
className="flex items-center justify-center w-12 h-12 rounded-xl bg-amber-50 border border-amber-200 text-amber-600 hover:bg-amber-100 transition-colors"
>
<UserPlus className="w-6 h-6" />
</Link>
{renderTooltip('addAdmin', 'إضافة أدمن')}
</motion.div>
<motion.div
className="relative group"
variants={buttonVariants}
initial="rest"
whileHover="hover"
whileTap="tap"
onMouseEnter={() => showTooltip('editPrivacy')}
onMouseLeave={hideTooltip}
>
<Link
href="/admin/privacy"
className="flex items-center justify-center w-12 h-12 rounded-xl bg-slate-50 border border-slate-200 text-slate-700 hover:bg-slate-100 transition-colors"
>
<Shield className="w-6 h-6" />
</Link>
{renderTooltip('editPrivacy', 'تعديل سياسة الخصوصية')}
</motion.div>
</>
) : (
<>
<motion.div
className="relative group"
variants={buttonVariants}
initial="rest"
whileHover="hover"
whileTap="tap"
onMouseEnter={() => showTooltip('favorites')}
onMouseLeave={hideTooltip}
>
<Link
href="/favorites"
className="flex items-center justify-center w-12 h-12 rounded-xl transition-colors"
>
<div className="relative">
<Heart className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" />
{favorites.length > 0 && (
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="absolute -right-1 -top-1 w-5 h-5 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}
</motion.span>
)}
</div>
</Link>
{renderTooltip('favorites', 'المفضلة')}
</motion.div>
<motion.div
className="relative group"
variants={buttonVariants}
initial="rest"
whileHover="hover"
whileTap="tap"
onMouseEnter={() => showTooltip('notifications')}
onMouseLeave={hideTooltip}
>
<Link
href="/notifications"
className="flex items-center justify-center w-12 h-12 rounded-xl transition-colors"
>
<div className="relative">
<Bell className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" />
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="absolute -right-1 -top-1 w-5 h-5 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>
</div>
</Link>
{renderTooltip('notifications', 'الإشعارات')}
</motion.div>
<motion.div
className="relative group"
variants={buttonVariants}
initial="rest"
whileHover="hover"
whileTap="tap"
onMouseEnter={() => showTooltip('payments')}
onMouseLeave={hideTooltip}
>
<Link
href="/payments"
className="flex items-center justify-center w-12 h-12 rounded-xl transition-colors"
>
<CreditCard className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" />
</Link>
{renderTooltip('payments', 'المدفوعات')}
</motion.div>
</>
)}
<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
className="relative group"
variants={buttonVariants}
initial="rest"
whileHover="hover"
whileTap="tap"
onMouseEnter={() => showTooltip('favorites')}
onMouseLeave={hideTooltip}
>
<Link
href="/favorites"
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
>
<div className="relative">
<Heart className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
{favorites.length > 0 && (
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
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}
</motion.span>
)}
</div>
</Link>
{renderTooltip('favorites', t('favorites'))}
</motion.div>
<motion.div
className="relative group"
variants={buttonVariants}
initial="rest"
whileHover="hover"
whileTap="tap"
onMouseEnter={() => showTooltip('notifications')}
onMouseLeave={hideTooltip}
>
<Link
href="/notifications"
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
>
<div className="relative">
<Bell className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
{unreadCount > 0 && (
<motion.span
initial={{ scale: 0 }}
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"
>
{unreadCount}
</motion.span>
)}
</div>
</Link>
{renderTooltip('notifications', t('notifications'))}
</motion.div>
<motion.div
className="relative group"
variants={buttonVariants}
initial="rest"
whileHover="hover"
whileTap="tap"
onMouseEnter={() => showTooltip('payments')}
onMouseLeave={hideTooltip}
>
<Link
href="/payments"
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
>
<CreditCard className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
</Link>
{renderTooltip('payments', t('payments'))}
</motion.div>
<motion.div
className="relative group"
variants={buttonVariants}
initial="rest"
whileHover="hover"
whileTap="tap"
onMouseEnter={() => showTooltip('settings')}
onMouseLeave={hideTooltip}
>
<Link
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>
</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 { LogIn, UserPlus } from 'lucide-react';
export function NavLink({ href, children }) {
export function NavLink({ href, children, light }) {
const pathname = usePathname();
const isActive = pathname === href || pathname.startsWith(href + '/');
return (
@ -11,19 +11,19 @@ export function NavLink({ href, children }) {
href={href}
className={`px-5 py-3 rounded-lg font-semibold transition-all duration-300 text-lg relative group ${
isActive
? 'text-amber-600 bg-amber-50'
: 'text-gray-700 hover:text-amber-600 hover:bg-gray-100'
? light ? 'text-amber-300 bg-white/10' : 'text-amber-600 bg-amber-50'
: light ? 'text-white/90 hover:text-amber-300 hover:bg-white/10' : 'text-gray-700 hover:text-amber-600 hover:bg-gray-100'
}`}
>
{children}
<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>
</Link>
);
}
export function MobileNavLink({ href, children, onClick }) {
export function MobileNavLink({ href, children, onClick, light }) {
const pathname = usePathname();
const isActive = pathname === href || pathname.startsWith(href + '/');
return (
@ -32,8 +32,8 @@ export function MobileNavLink({ href, children, onClick }) {
onClick={onClick}
className={`block px-3 py-2 rounded-md text-base font-medium transition-colors ${
isActive
? 'text-amber-600 bg-amber-50'
: 'text-gray-700 hover:text-amber-600 hover:bg-gray-50'
? light ? 'text-amber-300 bg-white/10' : 'text-amber-600 bg-amber-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}

View File

@ -1,10 +1,11 @@
"use client";
import { useEffect, useState, useRef } from "react";
import { initializeApp, getApps } from "firebase/app";
import { getMessaging, getToken, onMessage } from "firebase/messaging";
import AuthService from "../services/AuthService";
import { setFCMToken } from "../utils/api";
//فاير بيز
const firebaseConfig = {
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
authDomain: "sweet-home-b2766.firebaseapp.com",
@ -15,6 +16,7 @@ const firebaseConfig = {
measurementId: "G-M2V95NBJLX",
};
//عم نعمل تهيئة للاعدادات
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
export default function NotificationHandler() {
@ -25,11 +27,10 @@ export default function NotificationHandler() {
useEffect(() => {
function checkAuth() {
if (initialized.current) return;
if (!AuthService.getToken()) return;
initialized.current = true;
if ("Notification" in window) {
//عم نشوف اذا حط سماح او رفض
if (Notification.permission === "default") {
setShowPrompt(true);
} else if (Notification.permission === "granted") {
@ -71,21 +72,7 @@ export default function NotificationHandler() {
});
if (fcmToken) {
console.log("[FCM] Token:", fcmToken.substring(0, 20) + "...");
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");
}
await setFCMToken(fcmToken, 2);
}
onMessage(messaging, (payload) => {
@ -104,7 +91,6 @@ export default function NotificationHandler() {
// This MUST be synchronous from a user gesture
const permission = await Notification.requestPermission();
console.log("[FCM] Permission result:", permission);
if (permission === "granted") {
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, 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>
{CitiesList.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';
import { useState } from 'react';
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { Search, MapPin, Home, DollarSign } from 'lucide-react';
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
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 }) {
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({
city: '',
propertyType: '',
priceRange: '',
identityType: 'syrian'
city: "all",
propertyType: "all",
priceRange: "all",
identityType: "syrian",
ownerSource: "all",
rentPeriod: "all",
availableToday: false,
});
const cities = [
{ id: 'all', label: 'جميع المدن' },
{ 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 [showLoginDialog, setShowLoginDialog] = useState(false);
const handleTabClick = (tab) => {
setActiveTab(tab);
};
const handleSearch = () => {
onSearch({
...filters,
propertyType: filters.propertyType || 'all',
city: filters.city || 'all',
priceRange: filters.priceRange || 'all'
mode: activeTab,
city: filters.city || "all",
propertyType: filters.propertyType || "all",
priceRange: filters.priceRange || "all",
ownerSource: filters.ownerSource || "all",
rentPeriod: filters.rentPeriod || "all",
});
};
return (
<motion.div
className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 sm:p-8 border border-white/20 shadow-2xl"
initial={{ opacity: 0, y: 30 }}
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) => (
<motion.button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-4 py-2 rounded-lg font-medium text-sm transition-all ${
activeTab === tab
? 'bg-amber-500 text-white'
: 'bg-white/20 text-white hover:bg-white/30'
}`}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{t(`${tab}Tab`)}
</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>
<>
<motion.div
className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 sm:p-8 border border-white/20 shadow-2xl"
initial={{ opacity: 0, y: 30 }}
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) => {
const isActive = activeTab === tab;
return (
<button
key={tab}
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"}`}
>
{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>
</button>
);
})}
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
<div className="flex items-center gap-1">
<Home className="w-4 h-4" />
{t("rentTypeLabel")}
</div>
</label>
<select
value={filters.propertyType}
onChange={(e) => setFilters({...filters, propertyType: 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'
}}
>
{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
{/* city */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<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"))} />
{activeTab === "rent" && (
<FilterSelect
icon={<Home className="w-4 h-4" />}
value={filters.propertyType}
onChange={(e) => setFilters({ ...filters, city: e.target.value })}
option={propertyTypes}
label={t("rentTypeLabel")}
/>
)}
{/* price */}
<FilterSelect
icon={<DollarSign className="w-4 h-4" />}
value={filters.priceRange}
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"
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'
}}
>
{priceRanges.map(range => (
<option key={range.id} value={range.id}>{range.label}</option>
))}
</select>
onChange={(e) => setFilters({ ...filters, priceRange: e.target.value })}
option={priceRanges}
label={t("priceLabel")}
/>
{/* type */}
<FilterSelect value={filters.identityType} onChange={(e) => setFilters({ ...filters, identityType: e.target.value })} option={identityTypes} label={t("identityTypeLabel")} />
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
<div className="flex items-center gap-1">
نوع الوثيقة
</div>
</label>
<select
value={filters.identityType}
onChange={(e) => setFilters({...filters, identityType: 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'
}}
>
{identityTypes.map(type => (
<option key={type.id} value={type.id}>{type.label}</option>
))}
</select>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4">
{/* owener */}
<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 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`}>
{/* checkbox */}
<label className="mt-4 flex items-center gap-3 text-amber-50 text-sm">
<input
type="checkbox"
checked={filters.availableToday}
onChange={(e) => setFilters({ ...filters, availableToday: e.target.checked })}
className="w-5 h-5 text-amber-500 rounded border-gray-300 bg-white"
/>
<span className="font-medium">{t("availableToday")}</span>
</label>
</div>
</div>
</div>
<div className="mt-6">
<motion.button
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"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Search className="w-5 h-5" />
{t("searchButton")}
</motion.button>
</div>
</motion.div>
<div className="mt-6">
<motion.button
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"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Search className="w-5 h-5" />
{t("searchButton")}
</motion.button>
</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) {
const info = item.propertyInformation || {};
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 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({
PENDING: 'pending',
OWNER_APPROVED: 'owner_approved',
ADMIN_APPROVED: 'admin_approved',
ACTIVE: 'active',
COMPLETED: 'completed',
REJECTED: 'rejected',
CANCELLED: 'cancelled',
ownerConfirmed: 'ownerConfirmed',
depositPaid: 'depositPaid',
depositConfirmed: 'depositConfirmed',
completed: 'completed',
cancelled: 'cancelled',
});
// Map status → Arabic label
const BookingStatusLabels = Object.freeze({
[BookingStatus.PENDING]: 'بانتظار الموافقة',
[BookingStatus.OWNER_APPROVED]: وافقة المالك',
[BookingStatus.ADMIN_APPROVED]: 'موافقة الإدارة',
[BookingStatus.ACTIVE]: 'إيجار نشط',
[BookingStatus.COMPLETED]: 'منتهي',
[BookingStatus.REJECTED]: رفوض',
[BookingStatus.CANCELLED]: 'ملغي',
[BookingStatus.PENDING]: 'قيد الانتظار',
[BookingStatus.ownerConfirmed]: ؤكد من المالك',
[BookingStatus.depositPaid]: 'تم دفع السلفة',
[BookingStatus.depositConfirmed]: 'تم تأكيد الدفع',
[BookingStatus.completed]: 'منتهي',
[BookingStatus.cancelled]: لغي',
});
// Map status → color class (Tailwind bg)
const BookingStatusColors = Object.freeze({
[BookingStatus.PENDING]: 'yellow',
[BookingStatus.OWNER_APPROVED]: 'blue',
[BookingStatus.ADMIN_APPROVED]: 'green',
[BookingStatus.ACTIVE]: 'purple',
[BookingStatus.COMPLETED]: 'gray',
[BookingStatus.REJECTED]: 'red',
[BookingStatus.CANCELLED]: 'red',
[BookingStatus.ownerConfirmed]: 'blue',
[BookingStatus.depositPaid]: 'orange',
[BookingStatus.depositConfirmed]: 'green',
[BookingStatus.completed]: 'teal',
[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
* Backend values are numeric (0, 1, 2)
* Backend values are numeric
* Used in: PropertyInformation.buildingType
*/
const BuildingType = Object.freeze({
APARTMENT: 0,
VILLA: 1,
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({
[BuildingType.APARTMENT]: 'شقة',
[BuildingType.VILLA]: 'فيلا',
[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({
[BuildingType.APARTMENT]: 'apartment',
[BuildingType.VILLA]: 'villa',
[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({
apartment: BuildingType.APARTMENT,
villa: BuildingType.VILLA,
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: 'القامشلي',
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
const CitiesList = Object.freeze(Object.values(City));
@ -35,4 +49,22 @@ function extractCity(address) {
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]: 'من الاثنين',
});
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({
SYP: 1,
USD: 2,
EUR: 3,
TRY: 4,
});
const CurrencyLabels = Object.freeze({
[Currency.SYP]: 'ليرة سورية',
[Currency.USD]: 'دولار أمريكي',
[Currency.EUR]: 'يورو',
[Currency.TRY]: 'ليرة تركية',
});
const CurrencySymbols = Object.freeze({
[Currency.SYP]: 'SYP',
[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]: 'عائلي',
});
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]: '🛂',
});
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]: 'وكالة عقارية',
});
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
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({
AVAILABLE: 0,
BOOKED: 1,
MAINTENANCE: 2,
NOT_AVAILABLE: 1,
BOOKED: 2,
});
// Map numeric value → Arabic label
const PropertyStatusLabels = Object.freeze({
[PropertyStatus.AVAILABLE]: 'متاح',
[PropertyStatus.NOT_AVAILABLE]: 'غير متاح',
[PropertyStatus.BOOKED]: 'محجوز',
[PropertyStatus.MAINTENANCE]: 'صيانة',
});
// Map numeric value → English key (for UI filters)
const PropertyStatusKeys = Object.freeze({
[PropertyStatus.AVAILABLE]: 'available',
[PropertyStatus.NOT_AVAILABLE]: 'notAvailable',
[PropertyStatus.BOOKED]: 'booked',
[PropertyStatus.MAINTENANCE]: 'maintenance',
});
// Reverse map: English key → numeric value
const PropertyStatusByKey = Object.freeze({
available: PropertyStatus.AVAILABLE,
notAvailable: PropertyStatus.NOT_AVAILABLE,
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
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]: 'غير مفروش',
});
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]: 'مفروش جزئياً',
});
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]: 'يومي',
});
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',
CUSTOMER: 'customer',
OWNER: 'owner',
ADMIN: 'admin',
AGENT: 'agent',
});
const UserRoleLabels = Object.freeze({
[UserRole.GUEST]: 'زائر',
[UserRole.CUSTOMER]: 'مستأجر',
[UserRole.OWNER]: 'مالك عقار',
[UserRole.ADMIN]: 'مدير النظام',
[UserRole.AGENT]: 'وسيط عقاري',
});
const UserRoleColors = Object.freeze({
[UserRole.GUEST]: 'gray',
[UserRole.CUSTOMER]: 'blue',
[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 { PropertyService, PropertyServiceLabels, PropertyServicesList } from './PropertyService';
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 { 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 { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import Link from 'next/link';
import Image from 'next/image';
import { Heart, MapPin, Bed, Bath, Square, X, ImageIcon } from 'lucide-react';
import { useFavorites } from '@/app/contexts/FavoritesContext';
import AuthService from '@/app/services/AuthService';
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/navigation";
import { motion } from "framer-motion";
import Link from "next/link";
import Image from "next/image";
import { Heart, MapPin, Bed, Bath, Square, X, ImageIcon } from "lucide-react";
import { useFavorites } from "@/app/contexts/FavoritesContext";
import AuthService from "@/app/services/AuthService";
import Loading from "../loading";
export default function FavoritesPage() {
const { t } = useTranslation();
const router = useRouter();
const { favorites, isLoading: favoritesLoading, removeFavorite } = useFavorites();
const [isAdmin, setIsAdmin] = useState(false);
useEffect(() => {
if (AuthService.isAdmin()) {
router.push('/');
return;
}
setIsAdmin(AuthService.isAdmin());
// Admin check removed
// if (AuthService.isAdmin()) {
// router.push('/');
// return;
// }
// setIsAdmin(AuthService.isAdmin());
}, [router]);
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ل.س';
return amount?.toLocaleString() + " " + t("currency-syp-suffix");
};
if (favoritesLoading && favorites.length === 0) {
return (
<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 <Loading />;
}
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="container mx-auto px-4 max-w-6xl">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">المفضلة</h1>
<p className="text-gray-600">العقارات التي قمت بحفظها</p>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t("favorites")}</h1>
<p className="text-gray-600">{t("saved-properties")}</p>
</div>
{favorites.length === 0 ? (
<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" />
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد عقارات في المفضلة</h3>
<p className="text-gray-500 mb-6">يمكنك إضافة العقارات التي تعجبك بالنقر على أيقونة القلب</p>
<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"
>
استعرض العقارات
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("no-favorites")}</h3>
<p className="text-gray-500 mb-6">{t("add-favorites-hint")}</p>
<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">
{t("browse-properties")}
</Link>
</div>
) : (
@ -68,13 +61,7 @@ export default function FavoritesPage() {
>
<div className="relative h-48 bg-gray-100">
{property.images && property.images[0] ? (
<Image
src={property.images[0]}
alt={property.title}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
<Image 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">
<ImageIcon className="w-12 h-12 text-gray-400" />
@ -93,7 +80,7 @@ export default function FavoritesPage() {
<div>
<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">
{property.type === 'apartment' ? 'شقة' : property.type === 'villa' ? 'فيلا' : 'بيت'}
{property.type === "apartment" ? t("buildingType.apartment") : property.type === "villa" ? t("buildingType.villa") : t("buildingType.house")}
</span>
</div>
<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 className="text-left">
<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>
@ -127,11 +114,8 @@ export default function FavoritesPage() {
</div>
</div>
<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"
>
عرض التفاصيل
<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">
{t("viewDetails")}
</Link>
</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';
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 { 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() {
const router = useRouter();
const { t } = useTranslation();
const [step, setStep] = useState(1);
const [email, setEmail] = useState('');
const [code, setCode] = useState('');
const [newPassword, setNewPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const handleSubmit = async (e) => {
const handleRequestOtp = async (e) => {
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);
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 (
<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 -top-40 -right-40 w-80 h-80 bg-purple-500 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 -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-orange-400 rounded-full opacity-20 blur-3xl animate-pulse delay-1000"></div>
</div>
<motion.div
@ -38,89 +86,160 @@ export default function ForgotPasswordPage() {
animate={{ opacity: 1, x: 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">
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
<span>العودة لتسجيل الدخول</span>
<Link href="/login" className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors group">
<ArrowLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
<span>{t('forgotPassword.backToLogin')}</span>
</Link>
</motion.div>
<div className="bg-white/10 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/20 overflow-hidden">
<div className="bg-gradient-to-r from-purple-500 to-pink-500 p-8 text-center">
<div className="bg-white/80 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/60 overflow-hidden">
<div className="bg-gradient-to-l from-amber-500 to-amber-600 p-8 text-center">
<motion.h1
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className="text-3xl font-bold text-white mb-2"
>
استعادة كلمة المرور
{t('forgotPassword.title')}
</motion.h1>
<motion.p
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
className="text-purple-100"
className="text-amber-100"
>
سنرسل لك رابط لإعادة تعيين كلمة المرور
{step === 1 ? t('forgotPassword.step1Instruction') : t('forgotPassword.step2Instruction')}
</motion.p>
</div>
<div className="p-8">
{!isSubmitted ? (
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-200 mb-2">
البريد الإلكتروني
</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-purple-500 transition-colors" />
<AnimatePresence mode="wait">
{step === 1 && (
<motion.form
key="step1"
initial={{ opacity: 0, x: -30 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 30 }}
onSubmit={handleRequestOtp}
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>
<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>
<button
type="submit"
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"
<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('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 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>جاري الإرسال...</span>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{t('forgotPassword.otpLabel')}
</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>
) : (
'إرسال رابط الاستعادة'
)}
</button>
</form>
) : (
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="text-center py-6"
>
<div className="w-20 h-20 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle className="w-10 h-10 text-green-500" />
</div>
<h3 className="text-xl font-bold text-white mb-2">تم الإرسال بنجاح!</h3>
<p className="text-gray-300 mb-6">
تم إرسال رابط استعادة كلمة المرور إلى {email}
</p>
<Link
href="/login"
className="inline-block px-6 py-3 bg-white/10 text-white rounded-xl hover:bg-white/20 transition-colors"
>
العودة لتسجيل الدخول
</Link>
</motion.div>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{t('forgotPassword.newPasswordLabel')}
</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 text-gray-400 group-focus-within:text-amber-500 transition-colors" />
</div>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className={inputClass}
placeholder={t('forgotPassword.newPasswordPlaceholder')}
required
minLength={6}
/>
</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('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>
</motion.div>

View File

@ -1,5 +1,7 @@
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
/* ─── Madani Arabic Font ─── */
@font-face {
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 {
font-family: 'Madani Arabic', 'Noto Sans Arabic', 'Cairo', Arial, sans-serif;
}
@ -135,6 +157,13 @@ body {
transform: scale(1.1);
}
.nav-icon {
color: #1e293b;
}
.dark .nav-icon {
color: #ffffff;
}
.property-tooltip {
background: white !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 };
}

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