added next-intl, composed next-auth and next-intl in middleware, fixed some bugs, completed details page, added google auth
							
								
								
									
										10
									
								
								lib/next-auth.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -4,7 +4,13 @@ declare module "next-auth" { | |||||||
|   interface Session { |   interface Session { | ||||||
|     refresh_token: string; |     refresh_token: string; | ||||||
|     access_token: string; |     access_token: string; | ||||||
|     expires_in: string; |     expires_in?: string; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   interface User { | ||||||
|  |     refresh_token: string; | ||||||
|  |     access_token: string; | ||||||
|  |     expires_in?: string; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -14,6 +20,6 @@ declare module "next-auth/jwt" { | |||||||
|   interface JWT { |   interface JWT { | ||||||
|     refresh_token: string; |     refresh_token: string; | ||||||
|     access_token: string; |     access_token: string; | ||||||
|     expires_in: string; |     expires_in?: string; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										186
									
								
								messages/en.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,186 @@ | |||||||
|  | { | ||||||
|  |   "general": { | ||||||
|  |     "date": "Date", | ||||||
|  |     "address": "Address", | ||||||
|  |     "status": "Status", | ||||||
|  |     "description": "Description", | ||||||
|  |     "reviews": "Reviews", | ||||||
|  |     "rating": "Rating", | ||||||
|  |     "review": "Review", | ||||||
|  |     "write_comment": "Write Comment", | ||||||
|  |     "search": "Search", | ||||||
|  |     "search_for": "Search For", | ||||||
|  |     "city": "City", | ||||||
|  |     "added_roads": "Added Roads", | ||||||
|  |     "broken_roads": "Broken Roads", | ||||||
|  |     "accident_hotspots": "Accident Hotspots", | ||||||
|  |     "local_defects": "Local Defects", | ||||||
|  |     "repair_plans": "Repair Plans", | ||||||
|  |     "repaired": "Repaired", | ||||||
|  |     "fixed_local_defects": "Fixed Local Defects", | ||||||
|  |     "news": "News", | ||||||
|  |     "details": "Details", | ||||||
|  |     "navigation": "Navigation", | ||||||
|  |     "contacts": "Contacts", | ||||||
|  |     "download_our_app": "Download our app", | ||||||
|  |     "back": "Back", | ||||||
|  |     "save": "Save", | ||||||
|  |     "saving": "Saving", | ||||||
|  |     "cancel": "Cancel", | ||||||
|  |     "cancellation": "Cancellation", | ||||||
|  |     "save_changes": "Save Changes", | ||||||
|  |     "send": "Send", | ||||||
|  |     "receive": "Receive", | ||||||
|  |     "delete": "Delete", | ||||||
|  |     "show_on_map": "Show on Map", | ||||||
|  |     "author_of_appeal": "Author of Appeal", | ||||||
|  |     "enter_city": "Enter City", | ||||||
|  |     "page_not_found": "Page Not Found (404)", | ||||||
|  |     "incorrect_address_or_nonexistent_page": "Incorrect Address or Nonexistent Page.", | ||||||
|  |     "home": "Home", | ||||||
|  |     "first_name": "First Name", | ||||||
|  |     "last_name": "Last Name", | ||||||
|  |     "email": "Email" | ||||||
|  |   }, | ||||||
|  |   "navigation": { | ||||||
|  |     "home": "Home", | ||||||
|  |     "about_us": "About Us", | ||||||
|  |     "statistics": "Statistics", | ||||||
|  |     "news": "News", | ||||||
|  |     "volunteers": "Volunteers", | ||||||
|  |     "profile": "Profile", | ||||||
|  |     "login": "Login" | ||||||
|  |   }, | ||||||
|  |   "home": { | ||||||
|  |     "title": "Roads of Kyrgyzstan", | ||||||
|  |     "subtitle": "Let's Make Roads Safe!", | ||||||
|  |     "info": "Current information about the state of roads", | ||||||
|  |     "report_broken_road": "Report Broken Road", | ||||||
|  |     "road_map": "Road Map", | ||||||
|  |     "latest_news": "Stay informed about the latest news on traffic, construction, and events!", | ||||||
|  |     "enter_location": "Enter city, village, or region", | ||||||
|  |     "broken_roads": "Broken road", | ||||||
|  |     "accident_hotspots": "Accident hotspot", | ||||||
|  |     "local_defects": "Local defect", | ||||||
|  |     "repair_plans": "In repair plan", | ||||||
|  |     "repaired": "Repaired", | ||||||
|  |     "fixed_local_defects": "Fixed local defect", | ||||||
|  |     "rating": "Rating", | ||||||
|  |     "road_discussions": "Discussing roads: rating, experience, comfort on the way!", | ||||||
|  |     "enter_address": "Enter address", | ||||||
|  |     "read_more": "Read More" | ||||||
|  |   }, | ||||||
|  |   "about_us": { | ||||||
|  |     "name": "Transparency International-Kyrgyzstan", | ||||||
|  |     "description": "Branch of the international organization Transparency International in the Kyrgyz Republic.", | ||||||
|  |     "mission": "Promoting effective public policy and good governance to prevent corruption and strengthen democracy in the country.", | ||||||
|  |     "goals_and_priorities": { | ||||||
|  |       "anti-corruption_education": "Anti-corruption education of the population, raising public awareness of the importance and significance of the fight against corruption in Kyrgyzstan;", | ||||||
|  |       "study_of_corruption_practices": "Organization of the study of the practice and theory of combating corruption and the participation of civil society structures in Kyrgyzstan and other countries;", | ||||||
|  |       "supporting_citizens_and_organizations": "Assistance to citizens and organizations in the implementation of their constitutional rights and freedoms;", | ||||||
|  |       "international_experience": "Preferential orientation to international experience in reducing corruption, mastering its technologies and resources, as well as involving civil society structures in the international dialogue on combating corruption." | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "volunteers": { | ||||||
|  |     "activists": "Activists", | ||||||
|  |     "received_votes": "Received Votes", | ||||||
|  |     "left_votes": "Left Votes", | ||||||
|  |     "rating": "Rating" | ||||||
|  |   }, | ||||||
|  |   "profile": { | ||||||
|  |     "personal_cabinet": "Personal Cabinet", | ||||||
|  |     "personal_data": "Personal Data", | ||||||
|  |     "my_appeals": "My Appeals", | ||||||
|  |     "logout": "Logout", | ||||||
|  |     "write_appeal": "Write Appeal", | ||||||
|  |     "profile_photo": "Profile Photo", | ||||||
|  |     "others_identification": "With a profile photo, other people will recognize you, and it will be easier for you to determine which account you logged into.", | ||||||
|  |     "add_profile_photo": "Add Profile Photo", | ||||||
|  |     "profile_photo_updated": "Profile Photo Updated", | ||||||
|  |     "delete": "Delete", | ||||||
|  |     "change": "Change" | ||||||
|  |   }, | ||||||
|  |   "authorization": { | ||||||
|  |     "change_password": "Change Password", | ||||||
|  |     "old_password": "Old Password", | ||||||
|  |     "enter_old_password": "Enter Old Password", | ||||||
|  |     "new_password": "New Password", | ||||||
|  |     "enter_new_password": "Enter New Password", | ||||||
|  |     "confirm_new_password": "Confirm New Password", | ||||||
|  |     "confirm_new_password_prompt": "Please confirm the new password", | ||||||
|  |     "password": "Password", | ||||||
|  |     "forgot_password": "Forgot Password?", | ||||||
|  |     "login": "Login", | ||||||
|  |     "register": "Register", | ||||||
|  |     "sign_in_account": "Sign in to Account", | ||||||
|  |     "enter_credentials": "Please enter your credentials", | ||||||
|  |     "login_via_google": "Login via Google", | ||||||
|  |     "enter_password": "Enter Password", | ||||||
|  |     "password_requirements": "Minimum 8 characters, 1 uppercase letter, and 1 digit", | ||||||
|  |     "no_account_yet": "Don't have an account yet? Register", | ||||||
|  |     "registration": "Registration", | ||||||
|  |     "register_now": "Register Now", | ||||||
|  |     "already_have_account": "Already have an account? Sign in", | ||||||
|  |     "enter_email": "Enter Email", | ||||||
|  |     "enter_email_for_code": "Enter email, and we will send a code to reset the password", | ||||||
|  |     "send_code": "Send Code", | ||||||
|  |     "confirm_code": "Confirm Code", | ||||||
|  |     "enter_code": "Enter Code", | ||||||
|  |     "enter_reset_code": "Enter code to reset and recover the password", | ||||||
|  |     "reset_code": "Reset Code", | ||||||
|  |     "reset_password": "Reset Password", | ||||||
|  |     "check_email": "Check Your Email", | ||||||
|  |     "code_sent_to": "We sent a code to the email name@gmail.com", | ||||||
|  |     "confirmation_code": "Confirmation Code", | ||||||
|  |     "confirm": "Confirm", | ||||||
|  |     "resend_code_in": "Resend Code in", | ||||||
|  |     "resend_code": "Resend Code" | ||||||
|  |   }, | ||||||
|  |   "send_report": { | ||||||
|  |     "how_to_mark_road_section": "How to mark a road section?", | ||||||
|  |     "mark_road_instructions": "Place a pin and start drawing a road section (it can consist of any number of broken lines).", | ||||||
|  |     "remove_segment_instruction": "To remove a segment, click on the points again.", | ||||||
|  |     "add_problem_description": "Add a problem description", | ||||||
|  |     "enter_description": "Enter description", | ||||||
|  |     "add_photos": "Add Photos", | ||||||
|  |     "upload_photos_instructions": "Upload up to 5 photos related to the road you want to mark. Photos will help better understand the problem.", | ||||||
|  |     "attach_file": "Attach File (up to 5 MB)", | ||||||
|  |     "submit_for_moderation": "Submit for Moderation", | ||||||
|  |     "appeal_submitted": "Your appeal has been submitted", | ||||||
|  |     "thanks_for_appeal": "Thank you for your appeal. It is currently under moderation.", | ||||||
|  |     "view_my_appeals": "View My Appeals" | ||||||
|  |   }, | ||||||
|  |   "months": { | ||||||
|  |     "january": "January", | ||||||
|  |     "february": "February", | ||||||
|  |     "march": "March", | ||||||
|  |     "april": "April", | ||||||
|  |     "may": "May", | ||||||
|  |     "june": "June", | ||||||
|  |     "july": "July", | ||||||
|  |     "august": "August", | ||||||
|  |     "september": "September", | ||||||
|  |     "october": "October", | ||||||
|  |     "november": "November", | ||||||
|  |     "december": "December" | ||||||
|  |   }, | ||||||
|  |   "validation_errors": { | ||||||
|  |     "invalid_email_format": "Invalid email format.", | ||||||
|  |     "passwords_do_not_match": "Passwords do not match.", | ||||||
|  |     "required_field_not_filled": "Required field not filled.", | ||||||
|  |     "exceeded_maximum_length": "Exceeded maximum length of the field.", | ||||||
|  |     "login_required_before_commenting": "Please log in or register before leaving a comment.", | ||||||
|  |     "login_required_before_like": "Please log in or register before liking." | ||||||
|  |   }, | ||||||
|  |   "server_errors": { | ||||||
|  |     "invalid_email_or_password": "Invalid email or password.", | ||||||
|  |     "server_error_auth_attempt": "Server error during authentication attempt.", | ||||||
|  |     "login_failed": "Failed to log in. Something went wrong, please try again later.", | ||||||
|  |     "account_already_exists": "An account with this email already exists.", | ||||||
|  |     "account_not_found": "Account not found.", | ||||||
|  |     "invalid_activation_code": "Invalid activation code.", | ||||||
|  |     "invalid_activation_code_reset": "Invalid activation code for reset.", | ||||||
|  |     "invalid_password_reset_code": "Invalid password reset code.", | ||||||
|  |     "invalid_code": "Invalid code." | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										186
									
								
								messages/kg.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,186 @@ | |||||||
|  | { | ||||||
|  |   "general": { | ||||||
|  |     "date": "Күн", | ||||||
|  |     "address": "Дарек", | ||||||
|  |     "status": "Статус", | ||||||
|  |     "description": "Сүрөт", | ||||||
|  |     "reviews": "Комментарийлер", | ||||||
|  |     "rating": "Рейтинг", | ||||||
|  |     "review": "Комментарий", | ||||||
|  |     "write_comment": "Комментарий жазуу", | ||||||
|  |     "search": "Издөө", | ||||||
|  |     "search_for": "Издөө", | ||||||
|  |     "city": "Шаар", | ||||||
|  |     "added_roads": "Кошулган жолдор", | ||||||
|  |     "broken_roads": "Тас тастаган жолдор", | ||||||
|  |     "accident_hotspots": "Авариялуу жерлер", | ||||||
|  |     "local_defects": "Жерги дефекттер", | ||||||
|  |     "repair_plans": "Түзөө планттары", | ||||||
|  |     "repaired": "Түзөлгөн", | ||||||
|  |     "fixed_local_defects": "Жерги дефекттерди түзөлгөн", | ||||||
|  |     "news": "Жаңылыктар", | ||||||
|  |     "details": "Эчти маалымат", | ||||||
|  |     "navigation": "Навигация", | ||||||
|  |     "contacts": "Контакттар", | ||||||
|  |     "download_our_app": "Биздин приложениямызды жүктөп алыңыз", | ||||||
|  |     "back": "Кайтуу", | ||||||
|  |     "save": "Сактоо", | ||||||
|  |     "saving": "Сакталат", | ||||||
|  |     "cancel": "Жокко чыгаруу", | ||||||
|  |     "cancellation": "Жокко чыгаруу", | ||||||
|  |     "save_changes": "Өзгөртүүлөрдү сактоо", | ||||||
|  |     "send": "Жиберүү", | ||||||
|  |     "receive": "Алуу", | ||||||
|  |     "delete": "Жок кылуу", | ||||||
|  |     "show_on_map": "Картада көрсөтүү", | ||||||
|  |     "author_of_appeal": "Өтүнчүнүн автору", | ||||||
|  |     "enter_city": "Шаарды киргизиңиз", | ||||||
|  |     "page_not_found": "Бет табылган эмес (404)", | ||||||
|  |     "incorrect_address_or_nonexistent_page": "Туура эмес дарек же бет жок", | ||||||
|  |     "home": "Башкы бет", | ||||||
|  |     "first_name": "Аты", | ||||||
|  |     "last_name": "Фамилия", | ||||||
|  |     "email": "Электрондук почта" | ||||||
|  |   }, | ||||||
|  |   "navigation": { | ||||||
|  |     "home": "Башкы бет", | ||||||
|  |     "about_us": "Биз тууралуу", | ||||||
|  |     "statistics": "Статистика", | ||||||
|  |     "news": "Жаңылыктар", | ||||||
|  |     "volunteers": "Волонтёрлер", | ||||||
|  |     "profile": "Профиль", | ||||||
|  |     "login": "Кириш" | ||||||
|  |   }, | ||||||
|  |   "home": { | ||||||
|  |     "title": "Кыргызстандын жолдору", | ||||||
|  |     "subtitle": "Жолдорду бекемделүү жасаңыз!", | ||||||
|  |     "info": "Жолдордун жаңы күйү", | ||||||
|  |     "report_broken_road": "Тас тастаган жолду турганды таратуу", | ||||||
|  |     "road_map": "Жол картасы", | ||||||
|  |     "latest_news": "Трафик, өндүрүү жана тапшыруудагы соңгосу турган жаңылыктардан кабардар болуңуз!", | ||||||
|  |     "enter_location": "Шаар, айыл жана регионду киргизиңиз", | ||||||
|  |     "broken_roads": "Жол кирпич", | ||||||
|  |     "accident_hotspots": "Авариянын жатактоо жерлери", | ||||||
|  |     "local_defects": "Жерги дефект", | ||||||
|  |     "repair_plans": "Тозгоондоо жатактоо планында", | ||||||
|  |     "repaired": "Тозотулду", | ||||||
|  |     "fixed_local_defects": "Тозотулган жерги дефект", | ||||||
|  |     "rating": "Рейтинг", | ||||||
|  |     "road_discussions": "Жолдорду талкуулоо: рейтинг, тажрыйба, жолдоо боюнча комфорт!", | ||||||
|  |     "enter_address": "Даректи киргизиңиз", | ||||||
|  |     "read_more": "Көбүрөөк окуу" | ||||||
|  |   }, | ||||||
|  |   "transparency_international_kyrgyzstan": { | ||||||
|  |     "name": "Транспаренттыктык Интернационал-Кыргызстан", | ||||||
|  |     "description": "Транспаренттыктык Интернационалдын Кыргызстан Республикасы бөлүмү.", | ||||||
|  |     "mission": "Коррупциянын ыкмасы менен демократияны күтүтүп, эффективдүү жамааттык саясат жана жакшы мамлекеттүү башкаруунун бириктирилүү үчүн.", | ||||||
|  |     "goals_and_priorities": { | ||||||
|  |       "anti-corruption_education": "Коррупция менен борбордук бийикти ашуу, Кыргызстанда коррупцияга каршы болгондоо маанилүүдүн жана азаттыктардын маанилүүлүгү үчүн жалпы айткынуу;", | ||||||
|  |       "study_of_corruption_practices": "Коррупцияга каршы борбордукты тартуу жана учуруу теориясы менен, Кыргызстан менен башка өлкөлердеги кызмат көрсөтүүсү үчүн азаттыктыктын долбоорлорун жана каттоого жаткантыруу;", | ||||||
|  |       "supporting_citizens_and_organizations": "Граждандар менен биздин же компаниялардын конституциялык башкаруу менен жандуулаткануу;", | ||||||
|  |       "international_experience": "Коррупциянын аздоого чейинки жыйынтыкты башкаруу үчүн көздөр тартуу, улуттуктары менен биргелеп, коррупцияга каршы көрсөткүчтөрдү мамлекеттик диалогга киргизүү." | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "volunteers": { | ||||||
|  |     "activists": "Активисттер", | ||||||
|  |     "received_votes": "Алынган баллдар", | ||||||
|  |     "left_votes": "Калган баллдар", | ||||||
|  |     "rating": "Рейтинг" | ||||||
|  |   }, | ||||||
|  |   "profile": { | ||||||
|  |     "personal_cabinet": "Жеке кабинет", | ||||||
|  |     "personal_data": "Жеке дайындар", | ||||||
|  |     "my_appeals": "Менин жардам кылган жалпылыгым", | ||||||
|  |     "logout": "Чыгуу", | ||||||
|  |     "write_appeal": "Жардам кылуу", | ||||||
|  |     "profile_photo": "Профиль сүрөтү", | ||||||
|  |     "others_identification": "Профиль сүрөтү аркылуу башка адамдар сизди танышат, жана сизге кирген аккаунтту тандашуу өттүрүлгөн болот.", | ||||||
|  |     "add_profile_photo": "Профиль сүрөтү кошуу", | ||||||
|  |     "profile_photo_updated": "Профиль сүрөтү жаңыртылды", | ||||||
|  |     "delete": "Жок кылуу", | ||||||
|  |     "change": "Өзгөртүү" | ||||||
|  |   }, | ||||||
|  |   "authorization": { | ||||||
|  |     "change_password": "Сыр сөздү өзгөртүү", | ||||||
|  |     "old_password": "Эски сыр сөз", | ||||||
|  |     "enter_old_password": "Эски сыр сөздү киргизиңиз", | ||||||
|  |     "new_password": "Жаңы сыр сөз", | ||||||
|  |     "enter_new_password": "Жаңы сыр сөздү киргизиңиз", | ||||||
|  |     "confirm_new_password": "Жаңы сыр сөздү растоо", | ||||||
|  |     "confirm_new_password_prompt": "Жаңы сыр сөздү растоо, аны кайра чалыңыз", | ||||||
|  |     "password": "Сыр сөз", | ||||||
|  |     "forgot_password": "Сыр сөздү унуттуңузбу?", | ||||||
|  |     "login": "Кириш", | ||||||
|  |     "register": "Тизмеге кирүү", | ||||||
|  |     "sign_in_account": "Аккаунтка кириңиз", | ||||||
|  |     "enter_credentials": "Киргизген дайындарыңызды киргизиңиз", | ||||||
|  |     "login_via_google": "Google аркылуу кириңиз", | ||||||
|  |     "enter_password": "Сыр сөздү киргизиңиз", | ||||||
|  |     "password_requirements": "Минимум 8 белги, 1 башкы буюк тамга жана 1 сандар", | ||||||
|  |     "no_account_yet": "Өйткені, аккаунт жоок? Тизмеге кирүү", | ||||||
|  |     "registration": "Тизмеге кирүү", | ||||||
|  |     "register_now": "Азыр тизмеге кирүү", | ||||||
|  |     "already_have_account": "Аккаунт бар болсо, кириңиз", | ||||||
|  |     "enter_email": "Электрондук почтаны киргизиңиз", | ||||||
|  |     "enter_email_for_code": "Электрондук почта киргизиңиз, биз сизге сыр сөздү калыпта тапшыруу үчүн код жөнөтөт", | ||||||
|  |     "send_code": "Код жөнөтүү", | ||||||
|  |     "confirm_code": "Кодду растоо", | ||||||
|  |     "enter_code": "Кодду киргизиңиз", | ||||||
|  |     "enter_reset_code": "Сыр сөздү өзгөртүп жаңыртуу үчүн кодду киргизиңиз", | ||||||
|  |     "reset_code": "Сыр сөздү өзгөртүү коду", | ||||||
|  |     "reset_password": "Сыр сөздү өзгөртүү", | ||||||
|  |     "check_email": "Почтаны текшериңиз", | ||||||
|  |     "code_sent_to": "Биз кодду name@gmail.com почтасына жөнөттүк", | ||||||
|  |     "confirmation_code": "Тастыгы код", | ||||||
|  |     "confirm": "Растоо", | ||||||
|  |     "resend_code_in": "Кодду кайталап жөнөтүү", | ||||||
|  |     "resend_code": "Кодду кайталап жөнөтүү" | ||||||
|  |   }, | ||||||
|  |   "send_report": { | ||||||
|  |     "how_to_mark_road_section": "Жол бөлүмүн белгилөө үчүн", | ||||||
|  |     "mark_road_instructions": "Чек салып, жол бөлүмүн белгилөөгө ээсиңиз (ал булактардан турат).", | ||||||
|  |     "remove_segment_instruction": "Жол бөлүмүн каттоо үчүн бир маандын жакшысына басыңыз.", | ||||||
|  |     "add_problem_description": "Проблеманын сүрөтүн кошуңуз", | ||||||
|  |     "enter_description": "Сүрөттөмөнү киргизиңиз", | ||||||
|  |     "add_photos": "Фотографияларды кошуңуз", | ||||||
|  |     "upload_photos_instructions": "Жолдун байланышты 5 фотосун жүктөп алыңыз, анткени жататат жана туура түшүнүүдү макул болот.", | ||||||
|  |     "attach_file": "Файлды тиштөө (5 МБга чейин)", | ||||||
|  |     "submit_for_moderation": "Модерацияга жиберүү", | ||||||
|  |     "appeal_submitted": "Сиздин жалпылыгыңыз жиберилди", | ||||||
|  |     "thanks_for_appeal": "Сиздин жалпылыгыңуз үчүн рахмат. Азырынча аны модерацияда.", | ||||||
|  |     "view_my_appeals": "Менин жалпылыгымдарымды көрүү" | ||||||
|  |   }, | ||||||
|  |   "months": { | ||||||
|  |     "january": "Жанварь", | ||||||
|  |     "february": "Февраль", | ||||||
|  |     "march": "Март", | ||||||
|  |     "april": "Апрель", | ||||||
|  |     "may": "Май", | ||||||
|  |     "june": "Июнь", | ||||||
|  |     "july": "Июль", | ||||||
|  |     "august": "Август", | ||||||
|  |     "september": "Сентябрь", | ||||||
|  |     "october": "Октябрь", | ||||||
|  |     "november": "Ноябрь", | ||||||
|  |     "december": "Декабрь" | ||||||
|  |   }, | ||||||
|  |   "validation_errors": { | ||||||
|  |     "invalid_email_format": "Туура эмес электрондук почта форматы.", | ||||||
|  |     "passwords_do_not_match": "Сыр сөздөрдүн туура келмейт.", | ||||||
|  |     "required_field_not_filled": "Милдеттүү талаа толтурулган жок.", | ||||||
|  |     "exceeded_maximum_length": "Талаанын эң ылдам узундугу өткөнчү болду.", | ||||||
|  |     "login_required_before_commenting": "Комментарий бир ар каайыпты таштоо алганда, ал кайталап киргизиңиз же тизмеге киргизиңиз.", | ||||||
|  |     "login_required_before_like": "Лайк койгондо, кайталап киргизиңиз же тизмеге киргизиңиз керек." | ||||||
|  |   }, | ||||||
|  |   "server_errors": { | ||||||
|  |     "invalid_email_or_password": "Туура эмес почта же сыр сөз.", | ||||||
|  |     "server_error_auth_attempt": "Авторизация учуруу учурастыктан кайталап сервердеги ката.", | ||||||
|  |     "login_failed": "Кирүүгө мүмкүн болгон эмес. Негизги нече кайталап уруксат бериңиз.", | ||||||
|  |     "account_already_exists": "Бул почтага ар бир аккаунт бар.", | ||||||
|  |     "account_not_found": "Аккаунт табылган жок.", | ||||||
|  |     "invalid_activation_code": "Четке калган иштеш коду.", | ||||||
|  |     "invalid_activation_code_reset": "Сыр сөздү калыпта тапшыруу үчүн четке калган иштеш коду.", | ||||||
|  |     "invalid_password_reset_code": "Сыр сөздү өзгөртүү коду четке калган эмес.", | ||||||
|  |     "invalid_code": "Четке калган иштеш коду." | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										186
									
								
								messages/ru.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,186 @@ | |||||||
|  | { | ||||||
|  |   "general": { | ||||||
|  |     "date": "Дата", | ||||||
|  |     "address": "Адрес", | ||||||
|  |     "status": "Статус", | ||||||
|  |     "description": "Описание", | ||||||
|  |     "reviews": "Комментарии", | ||||||
|  |     "rating": "Рейтинг", | ||||||
|  |     "review": "Комментарий", | ||||||
|  |     "write_comment": "Написать комментарий", | ||||||
|  |     "search": "Поиск", | ||||||
|  |     "search_for": "Искать", | ||||||
|  |     "city": "Город", | ||||||
|  |     "added_roads": "Добавлено дорог", | ||||||
|  |     "broken_roads": "Разбитых дорог", | ||||||
|  |     "accident_hotspots": "Очагов аварийности", | ||||||
|  |     "local_defects": "Локальных дефектов", | ||||||
|  |     "repair_plans": "В планах ремонта", | ||||||
|  |     "repaired": "Отремонтировано", | ||||||
|  |     "fixed_local_defects": "Локальных дефектов исправлено", | ||||||
|  |     "news": "Новости", | ||||||
|  |     "details": "Подробнее", | ||||||
|  |     "navigation": "Навигация", | ||||||
|  |     "contacts": "Контакты", | ||||||
|  |     "download_our_app": "Скачивай наше приложение", | ||||||
|  |     "back": "Назад", | ||||||
|  |     "save": "Сохранить", | ||||||
|  |     "saving": "Сохранение", | ||||||
|  |     "cancel": "Отменить", | ||||||
|  |     "cancellation": "Отмена", | ||||||
|  |     "save_changes": "Сохранить изменения", | ||||||
|  |     "send": "Отправить", | ||||||
|  |     "receive": "Получить", | ||||||
|  |     "delete": "Удалить", | ||||||
|  |     "show_on_map": "Показать на карте", | ||||||
|  |     "author_of_appeal": "Автор обращения", | ||||||
|  |     "enter_city": "Введите населенный пункт", | ||||||
|  |     "page_not_found": "Страница не найдена (404)", | ||||||
|  |     "incorrect_address_or_nonexistent_page": "Неправильно набран адрес или такой страницы не существует.", | ||||||
|  |     "home": "На главную", | ||||||
|  |     "first_name": "Имя", | ||||||
|  |     "last_name": "Фамилия", | ||||||
|  |     "email": "Электронная почта" | ||||||
|  |   }, | ||||||
|  |   "navigation": { | ||||||
|  |     "home": "Главная", | ||||||
|  |     "about_us": "О нас", | ||||||
|  |     "statistics": "Статистика", | ||||||
|  |     "news": "Новости", | ||||||
|  |     "volunteers": "Волонтеры", | ||||||
|  |     "profile": "Профиль", | ||||||
|  |     "login": "Войти" | ||||||
|  |   }, | ||||||
|  |   "home": { | ||||||
|  |     "title": "Дороги Кыргызстана", | ||||||
|  |     "subtitle": "Сделаем дороги безопасными!", | ||||||
|  |     "info": "Актуальная информация о состоянии дорог", | ||||||
|  |     "report_broken_road": "Отметить разбитую дорогу", | ||||||
|  |     "road_map": "Карта дорог", | ||||||
|  |     "latest_news": "Будьте в курсе последних новостей о дорожном движении, строительствах и мероприятиях!", | ||||||
|  |     "enter_location": "Введите город, село или регион", | ||||||
|  |     "broken_roads": "Разбитая дорога", | ||||||
|  |     "accident_hotspots": "Очаг аварийности", | ||||||
|  |     "local_defects": "Локальный дефект", | ||||||
|  |     "repair_plans": "В плане ремонта", | ||||||
|  |     "repaired": "Отремонтировано", | ||||||
|  |     "fixed_local_defects": "Локальный дефект исправлен", | ||||||
|  |     "rating": "Рейтинг", | ||||||
|  |     "road_discussions": "Обсуждаем дороги: рейтинг, опыт, комфорт в пути!", | ||||||
|  |     "enter_address": "Введите адрес", | ||||||
|  |     "read_more": "Читать" | ||||||
|  |   }, | ||||||
|  |   "about_us": { | ||||||
|  |     "name": "Transparency International-Кыргызстан", | ||||||
|  |     "description": "Филиал международной организации Transparency International в Кыргызской Республике.", | ||||||
|  |     "mission": "Продвижение эффективной общественной политики и надлежащего управления в целях предотвращения коррупции и усиления демократии в стране.", | ||||||
|  |     "goals_and_priorities": { | ||||||
|  |       "anti-corruption_education": "Антикоррупционное просвещение населения, повышение общественного осознания значимости и важности борьбы с коррупцией в Кыргызстане;", | ||||||
|  |       "study_of_corruption_practices": "Организация изучения практики и теории борьбы с коррупцией и участия в ней структур гражданского общества в Кыргызстане и других странах;", | ||||||
|  |       "supporting_citizens_and_organizations": "Содействие гражданам и организациям в реализации их конституционных прав и свобод;", | ||||||
|  |       "international_experience": "Преимущественная ориентация на международный опыт уменьшения коррупции, освоение его технологий, ресурсов, а также включение структур гражданского общества в международный диалог борьбы с коррупцией." | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "volunteers": { | ||||||
|  |     "activists": "Активисты", | ||||||
|  |     "received_votes": "Получено голосов", | ||||||
|  |     "left_votes": "Оставлено голосов", | ||||||
|  |     "rating": "Рейтинг" | ||||||
|  |   }, | ||||||
|  |   "profile": { | ||||||
|  |     "personal_cabinet": "Личный кабинет", | ||||||
|  |     "personal_data": "Личные данные", | ||||||
|  |     "my_appeals": "Мои обращения", | ||||||
|  |     "logout": "Выйти из аккаунта", | ||||||
|  |     "write_appeal": "Написать обращение", | ||||||
|  |     "profile_photo": "Фото профиля", | ||||||
|  |     "others_identification": "По фото профиля другие люди смогут вас узнавать, а вам будет проще определять, в какой аккаунт вы вошли.", | ||||||
|  |     "add_profile_photo": "Добавить фото профиля", | ||||||
|  |     "profile_photo_updated": "Фото профиля обновлено", | ||||||
|  |     "delete": "Удалить", | ||||||
|  |     "change": "Сменить" | ||||||
|  |   }, | ||||||
|  |   "authorization": { | ||||||
|  |     "change_password": "Изменить пароль", | ||||||
|  |     "old_password": "Старый пароль", | ||||||
|  |     "enter_old_password": "Введите старый пароль", | ||||||
|  |     "new_password": "Новый пароль", | ||||||
|  |     "enter_new_password": "Введите новый пароль", | ||||||
|  |     "confirm_new_password": "Подтвердить новый пароль", | ||||||
|  |     "confirm_new_password_prompt": "Пожалуйста, подтвердите новый пароль", | ||||||
|  |     "password": "Пароль", | ||||||
|  |     "forgot_password": "Забыли пароль?", | ||||||
|  |     "login": "Войти", | ||||||
|  |     "register": "Зарегистрироваться", | ||||||
|  |     "sign_in_account": "Войдите в аккаунт", | ||||||
|  |     "enter_credentials": "Пожалуйста, введите свои данные", | ||||||
|  |     "login_via_google": "Войти через Google", | ||||||
|  |     "enter_password": "Введите пароль", | ||||||
|  |     "password_requirements": "Минимум 8 символов, 1 заглавная буква и цифра", | ||||||
|  |     "no_account_yet": "Еще нет аккаунта? Зарегистрируйтесь", | ||||||
|  |     "registration": "Регистрация", | ||||||
|  |     "register_now": "Зарегистрировать", | ||||||
|  |     "already_have_account": "Уже есть аккаунт? Войти в аккаунт", | ||||||
|  |     "enter_email": "Введите электронную почту", | ||||||
|  |     "enter_email_for_code": "Введите электронную почту и мы отправим код для восстановления пароля", | ||||||
|  |     "send_code": "Отправить код", | ||||||
|  |     "confirm_code": "Потвердить код", | ||||||
|  |     "enter_code": "Введите код", | ||||||
|  |     "enter_reset_code": "Введите код для сброса и восстановления пароля", | ||||||
|  |     "reset_code": "Код сброса пароля", | ||||||
|  |     "reset_password": "Сбросить пароль", | ||||||
|  |     "check_email": "Проверьте свою почту", | ||||||
|  |     "code_sent_to": "Мы отправили код на почту name@gmail.com", | ||||||
|  |     "confirmation_code": "Код подтверждения", | ||||||
|  |     "confirm": "Подтвердить", | ||||||
|  |     "resend_code_in": "Отправить код повторно через", | ||||||
|  |     "resend_code": "Отправить код повторно" | ||||||
|  |   }, | ||||||
|  |   "send_report": { | ||||||
|  |     "how_to_mark_road_section": "Как отметить участок дороги?", | ||||||
|  |     "mark_road_instructions": "Поставьте булавку и начните рисовать участок дороги (он может состоять из любого количества ломаных линий).", | ||||||
|  |     "remove_segment_instruction": "Чтобы удалить отрезок, нажмите на точки повторно.", | ||||||
|  |     "add_problem_description": "Добавьте описание проблемы", | ||||||
|  |     "enter_description": "Введите описание", | ||||||
|  |     "add_photos": "Добавьте фотографии", | ||||||
|  |     "upload_photos_instructions": "Загрузите до 5 фотографий, связанных с дорогой, которую вы хотите отметить. Фотографии помогут лучше понять проблему.", | ||||||
|  |     "attach_file": "Прикрепить файл (до 5 МБ)", | ||||||
|  |     "submit_for_moderation": "Отправить на модерацию", | ||||||
|  |     "appeal_submitted": "Ваше обращение отправлено", | ||||||
|  |     "thanks_for_appeal": "Спасибо за ваше обращение. На данный момент оно в модерации.", | ||||||
|  |     "view_my_appeals": "Смотреть мои обращения" | ||||||
|  |   }, | ||||||
|  |   "months": { | ||||||
|  |     "january": "Январь", | ||||||
|  |     "february": "Февраль", | ||||||
|  |     "march": "Март", | ||||||
|  |     "april": "Апрель", | ||||||
|  |     "may": "Май", | ||||||
|  |     "june": "Июнь", | ||||||
|  |     "july": "Июль", | ||||||
|  |     "august": "Август", | ||||||
|  |     "september": "Сентябрь", | ||||||
|  |     "october": "Октябрь", | ||||||
|  |     "november": "Ноябрь", | ||||||
|  |     "december": "Декабрь" | ||||||
|  |   }, | ||||||
|  |   "validation_errors": { | ||||||
|  |     "invalid_email_format": "Неверный формат электронной почты.", | ||||||
|  |     "passwords_do_not_match": "Пароли не совпадают.", | ||||||
|  |     "required_field_not_filled": "Обязательное поле не заполнено.", | ||||||
|  |     "exceeded_maximum_length": "Превышена максимальная длина поля.", | ||||||
|  |     "login_required_before_commenting": "Перед тем как оставить комментарий, пожалуйста, войдите или зарегистрируйтесь.", | ||||||
|  |     "login_required_before_like": "Перед тем как поставить лайк, пожалуйста, войдите или зарегистрируйтесь." | ||||||
|  |   }, | ||||||
|  |   "server_errors": { | ||||||
|  |     "invalid_email_or_password": "Неверная почта или пароль.", | ||||||
|  |     "server_error_auth_attempt": "Серверная ошибка при попытке авторизации.", | ||||||
|  |     "login_failed": "Не удалось войти в систему. Что-то пошло не так, попробуйте еще раз немного позже.", | ||||||
|  |     "account_already_exists": "Такая учетная запись уже существует.", | ||||||
|  |     "account_not_found": "Такой учетной записи не существует.", | ||||||
|  |     "invalid_activation_code": "Код не действителен.", | ||||||
|  |     "invalid_activation_code_reset": "Код активации не действителен.", | ||||||
|  |     "invalid_password_reset_code": "Код сброса пароля не действителен.", | ||||||
|  |     "invalid_code": "Неверный код." | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -1,3 +1,6 @@ | |||||||
|  | import createNextIntlPlugin from "next-intl/plugin"; | ||||||
|  | const withNextIntl = createNextIntlPlugin(); | ||||||
|  | 
 | ||||||
| /** @type {import('next').NextConfig} */ | /** @type {import('next').NextConfig} */ | ||||||
| const nextConfig = { | const nextConfig = { | ||||||
|   distDir: "build", |   distDir: "build", | ||||||
| @ -9,6 +12,10 @@ const nextConfig = { | |||||||
|       }, |       }, | ||||||
|     ], |     ], | ||||||
|   }, |   }, | ||||||
|  |   env: { | ||||||
|  |     CLIENT_ID: process.env.CLIENT_ID, | ||||||
|  |     CLIENT_SECRET: process.env.CLIENT_SECRET, | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default nextConfig; | export default withNextIntl(nextConfig); | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ | |||||||
|     "leaflet": "^1.9.4", |     "leaflet": "^1.9.4", | ||||||
|     "next": "14.1.0", |     "next": "14.1.0", | ||||||
|     "next-auth": "^4.24.5", |     "next-auth": "^4.24.5", | ||||||
|  |     "next-intl": "^3.9.0", | ||||||
|     "react": "^18", |     "react": "^18", | ||||||
|     "react-dom": "^18", |     "react-dom": "^18", | ||||||
|     "react-leaflet": "^4.2.1", |     "react-leaflet": "^4.2.1", | ||||||
|  | |||||||
| Before Width: | Height: | Size: 332 KiB After Width: | Height: | Size: 332 KiB | 
| @ -10,7 +10,7 @@ export const metadata: Metadata = { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const DynamicForm = dynamic( | const DynamicForm = dynamic( | ||||||
|   () => import("@/widgets/ReportForm/ReportForm"), |   () => import("@/widgets/forms/ReportForm/ReportForm"), | ||||||
|   { |   { | ||||||
|     ssr: false, |     ssr: false, | ||||||
|   } |   } | ||||||
							
								
								
									
										34
									
								
								src/app/[locale]/layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,34 @@ | |||||||
|  | import type { Metadata } from "next"; | ||||||
|  | import "./globals.scss"; | ||||||
|  | import "./App.scss"; | ||||||
|  | // import "@/shared/fonts/fonts.scss";
 | ||||||
|  | import Navbar from "@/widgets/Navbar/Navbar"; | ||||||
|  | import Footer from "@/widgets/Footer/Footer"; | ||||||
|  | import { NextIntlClientProvider, useMessages } from "next-intl"; | ||||||
|  | import { Providers } from "./Providers"; | ||||||
|  | 
 | ||||||
|  | export default function LocaleLayout({ | ||||||
|  |   children, | ||||||
|  |   params, | ||||||
|  | }: Readonly<{ | ||||||
|  |   children: React.ReactNode; | ||||||
|  |   params: { locale: string }; | ||||||
|  | }>) { | ||||||
|  |   const messages = useMessages(); | ||||||
|  |   return ( | ||||||
|  |     <html lang={params.locale}> | ||||||
|  |       <body> | ||||||
|  |         <NextIntlClientProvider | ||||||
|  |           locale={params.locale} | ||||||
|  |           messages={messages} | ||||||
|  |         > | ||||||
|  |           <Providers> | ||||||
|  |             <Navbar /> | ||||||
|  |             <div className="app">{children}</div> | ||||||
|  |             <Footer /> | ||||||
|  |           </Providers> | ||||||
|  |         </NextIntlClientProvider> | ||||||
|  |       </body> | ||||||
|  |     </html> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
| @ -1,7 +1,5 @@ | |||||||
| import "./News.scss"; | import "./News.scss"; | ||||||
| import Typography from "@/shared/ui/components/Typography/Typography"; | import Typography from "@/shared/ui/components/Typography/Typography"; | ||||||
| import { apiInstance } from "@/shared/config/apiConfig"; |  | ||||||
| import { INewsList } from "@/shared/types/news-type"; |  | ||||||
| import NewsList from "@/widgets/NewsList/NewsList"; | import NewsList from "@/widgets/NewsList/NewsList"; | ||||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||||
| 
 | 
 | ||||||
| @ -1,8 +1,8 @@ | |||||||
| import Header from "@/widgets/Header/Header"; | import Header from "@/widgets/home/Header/Header"; | ||||||
| import StatisticsSection from "@/widgets/StatisticsSection/StatisticsSection"; | import StatisticsSection from "@/widgets/home/StatisticsSection/StatisticsSection"; | ||||||
| import RatingSection from "@/widgets/RatingSection/RatingSection"; | import RatingSection from "@/widgets/home/RatingSection/RatingSection"; | ||||||
| import NewsSection from "@/widgets/NewsSection/NewsSection"; | import NewsSection from "@/widgets/home/NewsSection/NewsSection"; | ||||||
| import MapSection from "@/widgets/MapSection/MapSection"; | import MapSection from "@/widgets/home/MapSection/MapSection"; | ||||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||||
| 
 | 
 | ||||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||||
| @ -14,14 +14,10 @@ const AuthGuard = ({ children }: { children: React.ReactNode }) => { | |||||||
|       }; |       }; | ||||||
|       await apiInstance.post("/token/verify/", data); |       await apiInstance.post("/token/verify/", data); | ||||||
|     } catch (error: unknown) { |     } catch (error: unknown) { | ||||||
|       if (error instanceof AxiosError) { |  | ||||||
|         if (error.response?.data.code === "token_not_valid") { |  | ||||||
|       signOut({ |       signOut({ | ||||||
|         callbackUrl: "/", |         callbackUrl: "/", | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @ -6,7 +6,6 @@ import { AxiosError } from "axios"; | |||||||
| import { apiInstance } from "@/shared/config/apiConfig"; | import { apiInstance } from "@/shared/config/apiConfig"; | ||||||
| import { getServerSession } from "next-auth"; | import { getServerSession } from "next-auth"; | ||||||
| import { authConfig } from "@/shared/config/authConfig"; | import { authConfig } from "@/shared/config/authConfig"; | ||||||
| import { IProfile } from "@/shared/types/profile-type"; |  | ||||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||||
| 
 | 
 | ||||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||||
| @ -1,4 +1,4 @@ | |||||||
| import ProfileTable from "@/widgets/ProfileTable/ProfileTable"; | import ProfileTable from "@/widgets/tables/ProfileTable/ProfileTable"; | ||||||
| import React from "react"; | import React from "react"; | ||||||
| 
 | 
 | ||||||
| const MyReports = async ({ | const MyReports = async ({ | ||||||
| @ -1,5 +1,3 @@ | |||||||
| import React from "react"; |  | ||||||
| 
 |  | ||||||
| const page = () => { | const page = () => { | ||||||
|   return <div>page</div>; |   return <div>page</div>; | ||||||
| }; | }; | ||||||
| @ -4,7 +4,7 @@ import ProfileAvatar from "@/features/ProfileAvatar/ProfileAvatar"; | |||||||
| import { apiInstance } from "@/shared/config/apiConfig"; | import { apiInstance } from "@/shared/config/apiConfig"; | ||||||
| import { authConfig } from "@/shared/config/authConfig"; | import { authConfig } from "@/shared/config/authConfig"; | ||||||
| import { IProfile } from "@/shared/types/profile-type"; | import { IProfile } from "@/shared/types/profile-type"; | ||||||
| import ProfileForm from "@/widgets/ProfileForm/ProfileForm"; | import ProfileForm from "@/widgets/forms/ProfileForm/ProfileForm"; | ||||||
| import { AxiosError } from "axios"; | import { AxiosError } from "axios"; | ||||||
| import { getServerSession } from "next-auth"; | import { getServerSession } from "next-auth"; | ||||||
| import React from "react"; | import React from "react"; | ||||||
							
								
								
									
										37
									
								
								src/app/[locale]/report/[id]/ReportDetails.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,37 @@ | |||||||
|  | .report-details { | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  |   gap: 75px; | ||||||
|  |   &__container { | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-columns: 1.05fr 1fr; | ||||||
|  |     gap: 76px; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   &__map { | ||||||
|  |     grid-row: 2 / 3; | ||||||
|  |     grid-column: 1 / 3; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @media screen and (max-width: 1024px) { | ||||||
|  |   .report-details { | ||||||
|  |     &__container { | ||||||
|  |       grid-template-columns: 1fr; | ||||||
|  |       gap: 45px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     &__map { | ||||||
|  |       grid-row: 2 / 3; | ||||||
|  |       grid-column: 1 / 2; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @media screen and (max-width: 550px) { | ||||||
|  |   .report-details { | ||||||
|  |     &__container { | ||||||
|  |       gap: 40px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB | 
							
								
								
									
										66
									
								
								src/app/[locale]/report/[id]/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,66 @@ | |||||||
|  | import "./ReportDetails.scss"; | ||||||
|  | import { IReport } from "@/shared/types/report-type"; | ||||||
|  | 
 | ||||||
|  | import ReviewSection from "@/widgets/ReviewSection/ReviewSection"; | ||||||
|  | import { Metadata } from "next"; | ||||||
|  | import ReportInformation from "@/widgets/report-details/ReportInformation/ReportInformation"; | ||||||
|  | import ReportImages from "@/widgets/report-details/ReportImages/ReportImages"; | ||||||
|  | import dynamic from "next/dynamic"; | ||||||
|  | import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs"; | ||||||
|  | 
 | ||||||
|  | const DynamicMap = dynamic( | ||||||
|  |   () => import("@/widgets/report-details/ReportMap/ReportMap"), | ||||||
|  |   { | ||||||
|  |     ssr: false, | ||||||
|  |   } | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export const metadata: Metadata = { | ||||||
|  |   title: "KG ROAD | Обращение", | ||||||
|  |   description: | ||||||
|  |     "Страница обращения Kyrgyzstan Transperency International", | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const ReportDetails = async ({ | ||||||
|  |   params, | ||||||
|  | }: { | ||||||
|  |   params: { id: string }; | ||||||
|  | }) => { | ||||||
|  |   const getReportDetails = async () => { | ||||||
|  |     const res = await fetch( | ||||||
|  |       `${process.env.NEXT_PUBLIC_BASE_API}/report/${params.id}/`, | ||||||
|  |       { cache: "no-store" } | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     return res.json(); | ||||||
|  |   }; | ||||||
|  |   const report: IReport = await getReportDetails(); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div className="report-details page-padding"> | ||||||
|  |       <BreadCrumbs /> | ||||||
|  |       <div className="report-details__container"> | ||||||
|  |         <ReportInformation | ||||||
|  |           id={report.id} | ||||||
|  |           location={report.location[0]} | ||||||
|  |           date={report.created_at} | ||||||
|  |           description={report.description} | ||||||
|  |           category={report.category} | ||||||
|  |           total_likes={report.total_likes} | ||||||
|  |           author={report.author} | ||||||
|  |         /> | ||||||
|  |         <div className="report-details__map"> | ||||||
|  |           <DynamicMap | ||||||
|  |             location={report.location} | ||||||
|  |             category={report.category} | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |         <ReportImages images={report.image} /> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <ReviewSection endpoint="report" id={+params.id} /> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default ReportDetails; | ||||||
| @ -1,5 +1,5 @@ | |||||||
| import "@/shared/ui/auth-classes.scss"; | import "@/shared/ui/auth-classes.scss"; | ||||||
| import ForgotPasswordForm from "@/widgets/ForgotPasswordForm/ForgotPasswordForm"; | import ForgotPasswordForm from "@/widgets/forms/ForgotPasswordForm/ForgotPasswordForm"; | ||||||
| 
 | 
 | ||||||
| const ForgotPassword = () => { | const ForgotPassword = () => { | ||||||
|   return ( |   return ( | ||||||
| Before Width: | Height: | Size: 768 B After Width: | Height: | Size: 768 B | 
| @ -1,9 +1,9 @@ | |||||||
| import "@/shared/ui/auth-classes.scss"; | import "@/shared/ui/auth-classes.scss"; | ||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
| import sign_in_icon from "./icons/sign-in_icon.svg"; | import sign_in_icon from "./icons/sign-in_icon.svg"; | ||||||
| import SignInForm from "@/widgets/SignInForm/SignInForm"; | import SignInForm from "@/widgets/forms/SignInForm/SignInForm"; | ||||||
| import Link from "next/link"; |  | ||||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||||
|  | import { Link } from "@/shared/config/navigation"; | ||||||
| 
 | 
 | ||||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||||
|   title: "KG ROAD | Вход", |   title: "KG ROAD | Вход", | ||||||
| Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB | 
| @ -1,7 +1,7 @@ | |||||||
| import "@/shared/ui/auth-classes.scss"; | import "@/shared/ui/auth-classes.scss"; | ||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
| import key from "./icons/key.svg"; | import key from "./icons/key.svg"; | ||||||
| import ResetCodeForm from "@/widgets/ResetCodeForm/ResetCodeForm"; | import ResetCodeForm from "@/widgets/forms/ResetCodeForm/ResetCodeForm"; | ||||||
| 
 | 
 | ||||||
| const ResetCode = () => { | const ResetCode = () => { | ||||||
|   return ( |   return ( | ||||||
| Before Width: | Height: | Size: 739 B After Width: | Height: | Size: 739 B | 
| @ -1,7 +1,7 @@ | |||||||
| import "@/shared/ui/auth-classes.scss"; | import "@/shared/ui/auth-classes.scss"; | ||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
| import mail from "./icons/mail.svg"; | import mail from "./icons/mail.svg"; | ||||||
| import ConfirmEmailForm from "@/widgets/ConfirmEmailForm/ConfirmEmailForm"; | import ConfirmEmailForm from "@/widgets/forms/ConfirmEmailForm/ConfirmEmailForm"; | ||||||
| 
 | 
 | ||||||
| const ConfirmEmail = ({ | const ConfirmEmail = ({ | ||||||
|   searchParams, |   searchParams, | ||||||
| Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB | 
| @ -1,9 +1,9 @@ | |||||||
| import "@/shared/ui/auth-classes.scss"; | import "@/shared/ui/auth-classes.scss"; | ||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
| import flag from "./icons/flag.svg"; | import flag from "./icons/flag.svg"; | ||||||
| import Link from "next/link"; | import SignUpForm from "@/widgets/forms/SignUpForm/SignUpForm"; | ||||||
| import SignUpForm from "@/widgets/SignUpForm/SignUpForm"; |  | ||||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||||
|  | import { Link } from "@/shared/config/navigation"; | ||||||
| 
 | 
 | ||||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||||
|   title: "KG ROAD | Регистрация", |   title: "KG ROAD | Регистрация", | ||||||
| @ -1,9 +1,6 @@ | |||||||
| import Typography from "@/shared/ui/components/Typography/Typography"; | import Typography from "@/shared/ui/components/Typography/Typography"; | ||||||
| import "./Statistics.scss"; | import "./Statistics.scss"; | ||||||
| import { apiInstance } from "@/shared/config/apiConfig"; | import StatisticsTable from "@/widgets/tables/StatisticsTable/StatisticsTable"; | ||||||
| import { IStatistics } from "@/shared/types/statistics-type"; |  | ||||||
| import { AxiosError } from "axios"; |  | ||||||
| import StatisticsTable from "@/widgets/StatisticsTable/StatisticsTable"; |  | ||||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||||
| 
 | 
 | ||||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||||
| @ -1,6 +1,6 @@ | |||||||
| import Typography from "@/shared/ui/components/Typography/Typography"; | import Typography from "@/shared/ui/components/Typography/Typography"; | ||||||
| import "./Volunteers.scss"; | import "./Volunteers.scss"; | ||||||
| import VolunteersTable from "@/widgets/VolunteersTable/VolunteersTable"; | import VolunteersTable from "@/widgets/tables/VolunteersTable/VolunteersTable"; | ||||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||||
| 
 | 
 | ||||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||||
| @ -1,25 +1,9 @@ | |||||||
| import type { Metadata } from "next"; | import { ReactNode } from "react"; | ||||||
| import "./globals.scss"; |  | ||||||
| import "./App.scss"; |  | ||||||
| // import "@/shared/fonts/fonts.scss";
 |  | ||||||
| import { Providers } from "./Providers"; |  | ||||||
| import Navbar from "@/widgets/Navbar/Navbar"; |  | ||||||
| import Footer from "@/widgets/Footer/Footer"; |  | ||||||
| 
 | 
 | ||||||
| export default function RootLayout({ | type Props = { | ||||||
|   children, |   children: ReactNode; | ||||||
| }: Readonly<{ | }; | ||||||
|   children: React.ReactNode; | 
 | ||||||
| }>) { | export default function RootLayout({ children }: Props) { | ||||||
|   return ( |   return children; | ||||||
|     <html lang="en"> |  | ||||||
|       <body> |  | ||||||
|         <Providers> |  | ||||||
|           <Navbar /> |  | ||||||
|           <div className="app">{children}</div> |  | ||||||
|           <Footer /> |  | ||||||
|         </Providers> |  | ||||||
|       </body> |  | ||||||
|     </html> |  | ||||||
|   ); |  | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										13
									
								
								src/app/not-found.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,13 @@ | |||||||
|  | "use client"; | ||||||
|  | 
 | ||||||
|  | import Error from "next/error"; | ||||||
|  | 
 | ||||||
|  | export default function NotFound() { | ||||||
|  |   return ( | ||||||
|  |     <html lang="en"> | ||||||
|  |       <body> | ||||||
|  |         <Error statusCode={404} /> | ||||||
|  |       </body> | ||||||
|  |     </html> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @ -1,138 +0,0 @@ | |||||||
| import "./ReportDetails.scss"; |  | ||||||
| import Image from "next/image"; |  | ||||||
| import RoadType from "@/entities/RoadType/RoadType"; |  | ||||||
| import ReportLike from "@/features/ReportLike/ReportLike"; |  | ||||||
| import { apiInstance } from "@/shared/config/apiConfig"; |  | ||||||
| import { IReport } from "@/shared/types/report-type"; |  | ||||||
| import { |  | ||||||
|   ROAD_TYPES, |  | ||||||
|   ROAD_TYPES_COLORS, |  | ||||||
| } from "@/shared/variables/road-types"; |  | ||||||
| import calendar from "./icons/calendar.svg"; |  | ||||||
| import map_pin from "./icons/map-pin.svg"; |  | ||||||
| import def_image from "./icons/def_image.svg"; |  | ||||||
| import ReviewSection from "@/widgets/ReviewSection/ReviewSection"; |  | ||||||
| import { Metadata } from "next"; |  | ||||||
| 
 |  | ||||||
| export const metadata: Metadata = { |  | ||||||
|   title: "KG ROAD | Обращение", |  | ||||||
|   description: |  | ||||||
|     "Страница обращения Kyrgyzstan Transperency International", |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const ReportDetails = async ({ |  | ||||||
|   params, |  | ||||||
| }: { |  | ||||||
|   params: { id: string }; |  | ||||||
| }) => { |  | ||||||
|   const getReportDetails = async () => { |  | ||||||
|     const res = await fetch( |  | ||||||
|       `${process.env.NEXT_PUBLIC_BASE_API}/report/${params.id}/`, |  | ||||||
|       { cache: "no-store" } |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     return res.json(); |  | ||||||
|   }; |  | ||||||
|   const report: IReport = await getReportDetails(); |  | ||||||
| 
 |  | ||||||
|   const months: Record<string, string> = { |  | ||||||
|     "01": "Январь", |  | ||||||
|     "02": "Февраль", |  | ||||||
|     "03": "Март", |  | ||||||
|     "04": "Апрель", |  | ||||||
|     "05": "Май", |  | ||||||
|     "06": "Июнь", |  | ||||||
|     "07": "Июль", |  | ||||||
|     "08": "Август", |  | ||||||
|     "09": "Сентябрь", |  | ||||||
|     "10": "Октябрь", |  | ||||||
|     "11": "Ноябрь", |  | ||||||
|     "12": "Декабрь", |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const showImages = () => { |  | ||||||
|     const images = []; |  | ||||||
| 
 |  | ||||||
|     for (let i = 0; i < 5; i++) { |  | ||||||
|       if (report.image[i]) { |  | ||||||
|         const image = ( |  | ||||||
|           <img |  | ||||||
|             className={`report-images__exist report-images__item${ |  | ||||||
|               i + 1 |  | ||||||
|             }`}
 |  | ||||||
|             key={i} |  | ||||||
|             src={report.image[i].image} |  | ||||||
|             alt="Report Image" |  | ||||||
|           /> |  | ||||||
|         ); |  | ||||||
|         images.push(image); |  | ||||||
|       } else { |  | ||||||
|         const defImage = ( |  | ||||||
|           <div |  | ||||||
|             className={`report-images__default report-images__item${ |  | ||||||
|               i + 1 |  | ||||||
|             }`}
 |  | ||||||
|             key={i} |  | ||||||
|           > |  | ||||||
|             <Image src={def_image} alt="Default Image" /> |  | ||||||
|           </div> |  | ||||||
|         ); |  | ||||||
|         images.push(defImage); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return images; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   return ( |  | ||||||
|     <div className="report-details page-padding"> |  | ||||||
|       <div className="report-details__container"> |  | ||||||
|         <div className="report-information"> |  | ||||||
|           <RoadType color={ROAD_TYPES_COLORS[report.category]}> |  | ||||||
|             {ROAD_TYPES[report.category]} |  | ||||||
|           </RoadType> |  | ||||||
|           <h2>{report.location[0].address}</h2> |  | ||||||
|           <div className="report-information__date-and-like"> |  | ||||||
|             <div className="report-information__date"> |  | ||||||
|               <Image src={calendar} alt="Calendar Icon" /> |  | ||||||
|               <p> |  | ||||||
|                 {months[report.created_at.slice(5, 7)]}{" "} |  | ||||||
|                 {report.created_at.slice(5, 7).slice(0, 1) === "0" |  | ||||||
|                   ? report.created_at.slice(6, 7) |  | ||||||
|                   : report.created_at.slice(5, 7)} |  | ||||||
|                 , {report.created_at.slice(0, 4)} |  | ||||||
|               </p> |  | ||||||
|             </div> |  | ||||||
|             <ReportLike |  | ||||||
|               count={report.total_likes} |  | ||||||
|               report_id={report.id} |  | ||||||
|             /> |  | ||||||
|           </div> |  | ||||||
| 
 |  | ||||||
|           <p className="report-information__description"> |  | ||||||
|             {report.description} |  | ||||||
|           </p> |  | ||||||
| 
 |  | ||||||
|           <p className="report-information__author"> |  | ||||||
|             Автор обращения:{" "} |  | ||||||
|             <span> |  | ||||||
|               {report.author.first_name}{" "} |  | ||||||
|               {report.author.last_name.slice(0, 1)}. |  | ||||||
|             </span> |  | ||||||
|           </p> |  | ||||||
|           <button className="report-information__show-map"> |  | ||||||
|             <Image src={map_pin} alt="Map Pin Icon" /> |  | ||||||
|             Показать на карте |  | ||||||
|           </button> |  | ||||||
|         </div> |  | ||||||
|         <div className="report-images"> |  | ||||||
|           {showImages().map((image) => image)} |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <ReviewSection endpoint="report" id={+params.id} /> |  | ||||||
|     </div> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export default ReportDetails; |  | ||||||
| @ -3,6 +3,7 @@ | |||||||
| import Image, { StaticImageData } from "next/image"; | import Image, { StaticImageData } from "next/image"; | ||||||
| import "./NewsCard.scss"; | import "./NewsCard.scss"; | ||||||
| import Link from "next/link"; | import Link from "next/link"; | ||||||
|  | import { useTranslations } from "next-intl"; | ||||||
| 
 | 
 | ||||||
| interface INewsCard { | interface INewsCard { | ||||||
|   id: number; |   id: number; | ||||||
| @ -19,6 +20,7 @@ const NewsCard: React.FC<INewsCard> = ({ | |||||||
|   description, |   description, | ||||||
|   date, |   date, | ||||||
| }: INewsCard) => { | }: INewsCard) => { | ||||||
|  |   const t = useTranslations("general"); | ||||||
|   const sliceTitle = (title: string) => { |   const sliceTitle = (title: string) => { | ||||||
|     if (title.length > 35) { |     if (title.length > 35) { | ||||||
|       return `${title.slice(0, 35)}...`; |       return `${title.slice(0, 35)}...`; | ||||||
| @ -57,7 +59,7 @@ const NewsCard: React.FC<INewsCard> = ({ | |||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <Link href={`/news/${id}`} className="news-card__more-btn"> |       <Link href={`/news/${id}`} className="news-card__more-btn"> | ||||||
|         Подробнее |         {t("details")} | ||||||
|       </Link> |       </Link> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
|  | |||||||
							
								
								
									
										5
									
								
								src/features/BreadCrumbs/BreadCrumbs.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,5 @@ | |||||||
|  | .breadcrumbs { | ||||||
|  |   display: flex; | ||||||
|  |   align-items: center; | ||||||
|  |   gap: 8px; | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								src/features/BreadCrumbs/BreadCrumbs.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,9 @@ | |||||||
|  | "use client"; | ||||||
|  | 
 | ||||||
|  | import "./BreadCrumbs.scss"; | ||||||
|  | 
 | ||||||
|  | const BreadCrumbs = () => { | ||||||
|  |   return <div className="breadcrumbs"></div>; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default BreadCrumbs; | ||||||
| @ -1,10 +1,27 @@ | |||||||
|  | "use client"; | ||||||
|  | 
 | ||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
| import "./GoogleButton.scss"; | import "./GoogleButton.scss"; | ||||||
| import google from "./icons/google.svg"; | import google from "./icons/google.svg"; | ||||||
|  | import { useSearchParams } from "next/navigation"; | ||||||
|  | import { signIn, useSession } from "next-auth/react"; | ||||||
| 
 | 
 | ||||||
| const GoogleButton = () => { | const GoogleButton = () => { | ||||||
|  |   const session = useSession(); | ||||||
|  |   const searchParams = useSearchParams(); | ||||||
|  |   const callbackUrl = | ||||||
|  |     searchParams.get("callbackUrl") || "/profile/personal"; | ||||||
|  | 
 | ||||||
|  |   const googleLogin = async () => { | ||||||
|  |     await signIn("google", { callbackUrl }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <button className="google-btn"> |     <button | ||||||
|  |       type="button" | ||||||
|  |       className="google-btn" | ||||||
|  |       onClick={googleLogin} | ||||||
|  |     > | ||||||
|       <Image src={google} alt="Google Icon" /> |       <Image src={google} alt="Google Icon" /> | ||||||
|       Войти через Google |       Войти через Google | ||||||
|     </button> |     </button> | ||||||
|  | |||||||
| @ -3,9 +3,9 @@ | |||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
| import "./ProfileAvatar.scss"; | import "./ProfileAvatar.scss"; | ||||||
| import pen from "./icons/pen.svg"; | import pen from "./icons/pen.svg"; | ||||||
| import { apiInstance } from "@/shared/config/apiConfig"; | import { authInstanse } from "@/shared/config/apiConfig"; | ||||||
| import { useSession } from "next-auth/react"; |  | ||||||
| import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||||
|  | import { useSession } from "next-auth/react"; | ||||||
| 
 | 
 | ||||||
| interface IProfileAvatarProps { | interface IProfileAvatarProps { | ||||||
|   img: string; |   img: string; | ||||||
| @ -19,24 +19,18 @@ const ProfileAvatar: React.FC<IProfileAvatarProps> = ({ | |||||||
|   const changeImage: React.ChangeEventHandler< |   const changeImage: React.ChangeEventHandler< | ||||||
|     HTMLInputElement |     HTMLInputElement | ||||||
|   > = async (e) => { |   > = async (e) => { | ||||||
|     const Authorization = `Bearer ${session.data?.access_token}`; |  | ||||||
|     const config = { |  | ||||||
|       headers: { |  | ||||||
|         Authorization, |  | ||||||
|       }, |  | ||||||
|     }; |  | ||||||
|     const formData = new FormData(); |     const formData = new FormData(); | ||||||
|     if (e.target.files) { |     if (e.target.files) { | ||||||
|       const image = Array.from(e.target.files); |       const image = Array.from(e.target.files); | ||||||
|       formData.append("image", image[0]); |       formData.append("image", image[0]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (session.status === "unauthenticated") return; | ||||||
|  | 
 | ||||||
|     try { |     try { | ||||||
|       const res = await apiInstance.patch( |       const res = await authInstanse( | ||||||
|         "/users/update_image/", |         session.data?.access_token as string | ||||||
|         formData, |       ).patch("/users/update_image/", formData); | ||||||
|         config |  | ||||||
|       ); |  | ||||||
|       router.refresh(); |       router.refresh(); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.log(error); |       console.log(error); | ||||||
|  | |||||||
| @ -4,9 +4,9 @@ import "./ReportLike.scss"; | |||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
| import like from "./icons/like.svg"; | import like from "./icons/like.svg"; | ||||||
| import { apiInstance } from "@/shared/config/apiConfig"; | import { apiInstance } from "@/shared/config/apiConfig"; | ||||||
| import { useRouter } from "next/navigation"; | import { signIn, useSession } from "next-auth/react"; | ||||||
| import { useSession } from "next-auth/react"; |  | ||||||
| import { useEffect } from "react"; | import { useEffect } from "react"; | ||||||
|  | import { usePathname, useRouter } from "@/shared/config/navigation"; | ||||||
| 
 | 
 | ||||||
| interface IReportLikeProps { | interface IReportLikeProps { | ||||||
|   count: number; |   count: number; | ||||||
| @ -20,6 +20,7 @@ const ReportLike: React.FC<IReportLikeProps> = ({ | |||||||
|   const session = useSession(); |   const session = useSession(); | ||||||
| 
 | 
 | ||||||
|   const router = useRouter(); |   const router = useRouter(); | ||||||
|  |   const pathname = usePathname(); | ||||||
| 
 | 
 | ||||||
|   const Authorization = `Bearer ${session?.data?.access_token}`; |   const Authorization = `Bearer ${session?.data?.access_token}`; | ||||||
|   const config = { |   const config = { | ||||||
| @ -29,6 +30,10 @@ const ReportLike: React.FC<IReportLikeProps> = ({ | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const checkLike = async () => { |   const checkLike = async () => { | ||||||
|  |     if (session.status === "unauthenticated") { | ||||||
|  |       signIn(undefined, { callbackUrl: pathname }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     const res = await apiInstance.get<{ detail: string }>( |     const res = await apiInstance.get<{ detail: string }>( | ||||||
|       `/report/${report_id}/like/check/`, |       `/report/${report_id}/like/check/`, | ||||||
|       config |       config | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ | |||||||
| import "./SearchForm.scss"; | import "./SearchForm.scss"; | ||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
| import search from "./icons/search.svg"; | import search from "./icons/search.svg"; | ||||||
|  | import { useTranslations } from "next-intl"; | ||||||
| 
 | 
 | ||||||
| interface ISearchFormProps | interface ISearchFormProps | ||||||
|   extends React.InputHTMLAttributes<HTMLInputElement> { |   extends React.InputHTMLAttributes<HTMLInputElement> { | ||||||
| @ -17,6 +18,7 @@ const SearchForm: React.FC<ISearchFormProps> = ({ | |||||||
|   onChange, |   onChange, | ||||||
|   style, |   style, | ||||||
| }: ISearchFormProps) => { | }: ISearchFormProps) => { | ||||||
|  |   const t = useTranslations("general"); | ||||||
|   return ( |   return ( | ||||||
|     <form |     <form | ||||||
|       style={style} |       style={style} | ||||||
| @ -33,7 +35,7 @@ const SearchForm: React.FC<ISearchFormProps> = ({ | |||||||
|           type="text" |           type="text" | ||||||
|         /> |         /> | ||||||
|       </div> |       </div> | ||||||
|       <button type="submit">Поиск</button> |       <button type="submit">{t("search")}</button> | ||||||
|     </form> |     </form> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  | |||||||
							
								
								
									
										20
									
								
								src/features/ShowMapButton/ShowMapButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,20 @@ | |||||||
|  | "use client"; | ||||||
|  | 
 | ||||||
|  | import Image from "next/image"; | ||||||
|  | import map_pin from "./icons/map-pin.svg"; | ||||||
|  | import { useReportStore } from "@/widgets/report-details/reportStore"; | ||||||
|  | 
 | ||||||
|  | const ShowMapButton = () => { | ||||||
|  |   const { setShowMap, showMap } = useReportStore(); | ||||||
|  |   return ( | ||||||
|  |     <button | ||||||
|  |       onClick={() => setShowMap(!showMap)} | ||||||
|  |       className="report-information__show-map" | ||||||
|  |     > | ||||||
|  |       <Image src={map_pin} alt="Map Pin Icon" /> | ||||||
|  |       Показать на карте | ||||||
|  |     </button> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default ShowMapButton; | ||||||
| Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										11
									
								
								src/i18n.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,11 @@ | |||||||
|  | import { notFound } from "next/navigation"; | ||||||
|  | import { getRequestConfig } from "next-intl/server"; | ||||||
|  | import { locales } from "@/shared/config/navigation"; | ||||||
|  | 
 | ||||||
|  | export default getRequestConfig(async ({ locale }) => { | ||||||
|  |   if (!locales.includes(locale as any)) notFound(); | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     messages: (await import(`../messages/${locale}.json`)).default, | ||||||
|  |   }; | ||||||
|  | }); | ||||||
| @ -1,9 +1,51 @@ | |||||||
| export { default } from "next-auth/middleware"; | import { withAuth } from "next-auth/middleware"; | ||||||
|  | import createIntlMiddleware from "next-intl/middleware"; | ||||||
|  | import { NextRequest } from "next/server"; | ||||||
|  | import { locales, localePrefix } from "./shared/config/navigation"; | ||||||
| 
 | 
 | ||||||
| export const config = { | const privatePages = [ | ||||||
|   matcher: [ |   "/profile", | ||||||
|   "/profile/personal", |   "/profile/personal", | ||||||
|   "/profile/my-reports", |   "/profile/my-reports", | ||||||
|   "/create-report", |   "/create-report", | ||||||
|   ], | ]; | ||||||
|  | 
 | ||||||
|  | const intlMiddleware = createIntlMiddleware({ | ||||||
|  |   locales, | ||||||
|  |   localePrefix, | ||||||
|  |   defaultLocale: "ru", | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const authMiddleware = withAuth( | ||||||
|  |   function onSuccess(req) { | ||||||
|  |     return intlMiddleware(req); | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     callbacks: { | ||||||
|  |       authorized: ({ token }) => token != null, | ||||||
|  |     }, | ||||||
|  |     pages: { | ||||||
|  |       signIn: "/sign-in", | ||||||
|  |     }, | ||||||
|  |   } | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export default function middleware(req: NextRequest) { | ||||||
|  |   const publicPathnameRegex = RegExp( | ||||||
|  |     `^(/(${locales.join("|")}))?(${privatePages | ||||||
|  |       .flatMap((p) => (p === "/" ? ["", "/"] : p)) | ||||||
|  |       .join("|")})/?$`,
 | ||||||
|  |     "i" | ||||||
|  |   ); | ||||||
|  |   const isPublicPage = publicPathnameRegex.test(req.nextUrl.pathname); | ||||||
|  | 
 | ||||||
|  |   if (!isPublicPage) { | ||||||
|  |     return intlMiddleware(req); | ||||||
|  |   } else { | ||||||
|  |     return (authMiddleware as any)(req); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const config = { | ||||||
|  |   matcher: ["/((?!api|_next|.*\\..*).*)"], | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -5,3 +5,13 @@ const API_URL = process.env["NEXT_PUBLIC_BASE_API"]; | |||||||
| export const apiInstance = axios.create({ | export const apiInstance = axios.create({ | ||||||
|   baseURL: API_URL, |   baseURL: API_URL, | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | export const authInstanse = (access_token: string) => { | ||||||
|  |   return axios.create({ | ||||||
|  |     baseURL: API_URL, | ||||||
|  |     headers: { | ||||||
|  |       Authorization: `Bearer ${access_token}`, | ||||||
|  |     }, | ||||||
|  |     method: "", | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | |||||||
| @ -2,24 +2,45 @@ import axios from "axios"; | |||||||
| import { AuthOptions } from "next-auth"; | import { AuthOptions } from "next-auth"; | ||||||
| import { JWT } from "next-auth/jwt"; | import { JWT } from "next-auth/jwt"; | ||||||
| import CredentialsProvider from "next-auth/providers/credentials"; | import CredentialsProvider from "next-auth/providers/credentials"; | ||||||
|  | import { apiInstance } from "./apiConfig"; | ||||||
|  | import { IRefresh, ITokens } from "../types/token-type"; | ||||||
|  | import GoogleProvider from "next-auth/providers/google"; | ||||||
| 
 | 
 | ||||||
| interface IToken { | const verifyToken = async (access_token: string) => { | ||||||
|   access: string; |   const res = await apiInstance.post("/token/verify/", { | ||||||
| } |     token: access_token, | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   if ([200, 201].includes(res.status)) { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return false; | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| const refreshToken = async (token: JWT): Promise<JWT> => { | const refreshToken = async (token: JWT): Promise<JWT> => { | ||||||
|   const data = { |   const data = { | ||||||
|     refresh: token.refresh_token, |     refresh: token.refresh_token, | ||||||
|   }; |   }; | ||||||
|  |   // const date = new Date().toLocaleTimeString();
 | ||||||
|  |   // const expire = new Date(token.expires_in).toLocaleTimeString();
 | ||||||
| 
 | 
 | ||||||
|   const response = await axios.post<IToken>( |   const verify = await verifyToken(token.access_token); | ||||||
|     "https://api.kgroaduat.fishrungames.com/api/v1/token/refresh/", | 
 | ||||||
|  |   if (verify) | ||||||
|  |     return { | ||||||
|  |       ...token, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |   const response = await apiInstance.post<IRefresh>( | ||||||
|  |     "/users/refresh/", | ||||||
|     data |     data | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   return { |   return { | ||||||
|     ...token, |     ...token, | ||||||
|     access_token: response.data.access, |     access_token: response.data.access, | ||||||
|  |     expires_in: response.data.expires_at, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| @ -27,7 +48,6 @@ export const authConfig: AuthOptions = { | |||||||
|   providers: [ |   providers: [ | ||||||
|     CredentialsProvider({ |     CredentialsProvider({ | ||||||
|       name: "Credentials", |       name: "Credentials", | ||||||
| 
 |  | ||||||
|       credentials: { |       credentials: { | ||||||
|         email: { |         email: { | ||||||
|           label: "Email", |           label: "Email", | ||||||
| @ -36,35 +56,30 @@ export const authConfig: AuthOptions = { | |||||||
|         }, |         }, | ||||||
|         password: { label: "Password", type: "password" }, |         password: { label: "Password", type: "password" }, | ||||||
|       }, |       }, | ||||||
| 
 |  | ||||||
|       async authorize(credentials, req) { |       async authorize(credentials, req) { | ||||||
|         if (!credentials?.email || !credentials?.password) |         if (!credentials?.email || !credentials?.password) | ||||||
|           return null; |           return null; | ||||||
| 
 | 
 | ||||||
|         const { email, password } = credentials as any; |         const { email, password } = credentials as any; | ||||||
| 
 |         const data = { | ||||||
|         const res = await fetch( |  | ||||||
|           "https://api.kgroaduat.fishrungames.com/api/v1/users/login/", |  | ||||||
|           { |  | ||||||
|             method: "POST", |  | ||||||
|             headers: { |  | ||||||
|               "Content-Type": "application/json", |  | ||||||
|             }, |  | ||||||
|             body: JSON.stringify({ |  | ||||||
|           email, |           email, | ||||||
|           password, |           password, | ||||||
|             }), |         }; | ||||||
|           } |  | ||||||
|         ); |  | ||||||
| 
 | 
 | ||||||
|         if (res.status.toString()[0] === "4") { |         const res = await apiInstance.post("/users/login/", data); | ||||||
|  | 
 | ||||||
|  |         if (![200, 201].includes(res.status)) { | ||||||
|           return null; |           return null; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const user = await res.json(); |         const user = res.data; | ||||||
|         return user; |         return user; | ||||||
|       }, |       }, | ||||||
|     }), |     }), | ||||||
|  |     GoogleProvider({ | ||||||
|  |       clientId: process.env.CLIENT_ID as string, | ||||||
|  |       clientSecret: process.env.CLIENT_SECRET as string, | ||||||
|  |     }), | ||||||
|   ], |   ], | ||||||
|   pages: { |   pages: { | ||||||
|     signIn: "/sign-in", |     signIn: "/sign-in", | ||||||
| @ -73,9 +88,39 @@ export const authConfig: AuthOptions = { | |||||||
|     strategy: "jwt", |     strategy: "jwt", | ||||||
|   }, |   }, | ||||||
|   callbacks: { |   callbacks: { | ||||||
|     async jwt({ token, user }) { |     async signIn({ account, profile, user }) { | ||||||
|  |       if (!profile?.email) { | ||||||
|  |         throw new Error("No Profile"); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (account?.provider === "google") { | ||||||
|  |         const data = { | ||||||
|  |           auth_token: account?.id_token, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         const res = await apiInstance.post<ITokens>( | ||||||
|  |           "/users/google/", | ||||||
|  |           data | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         if (![200, 201].includes(res.status)) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         user.access_token = res.data.access_token; | ||||||
|  |         user.refresh_token = res.data.refresh_token; | ||||||
|  |         user.expires_in = res.data.expires_in; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return true; | ||||||
|  |     }, | ||||||
|  |     async jwt({ token, user, account }) { | ||||||
|       if (user) return { ...token, ...user }; |       if (user) return { ...token, ...user }; | ||||||
| 
 | 
 | ||||||
|  |       if (token.exp) { | ||||||
|  |         return token; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       return refreshToken(token); |       return refreshToken(token); | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										7
									
								
								src/shared/config/navigation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,7 @@ | |||||||
|  | import { createSharedPathnamesNavigation } from "next-intl/navigation"; | ||||||
|  | 
 | ||||||
|  | export const locales = ["en", "ru", "kg"] as const; | ||||||
|  | export const localePrefix = "always"; // Default
 | ||||||
|  | 
 | ||||||
|  | export const { Link, redirect, usePathname, useRouter } = | ||||||
|  |   createSharedPathnamesNavigation({ locales, localePrefix }); | ||||||
| @ -1,5 +1,5 @@ | |||||||
| export interface ILocation { | export interface ILocation { | ||||||
|   id: 4; |   id: number; | ||||||
|   latitude: string; |   latitude: string; | ||||||
|   longitude: string; |   longitude: string; | ||||||
|   address: string; |   address: string; | ||||||
|  | |||||||
| @ -3,3 +3,8 @@ export interface ITokens { | |||||||
|   access_token: string; |   access_token: string; | ||||||
|   expires_in: string; |   expires_in: string; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export interface IRefresh { | ||||||
|  |   access: string; | ||||||
|  |   expires_at: string; | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,7 +1,23 @@ | |||||||
| export const LINKS = [ | import { useTranslations } from "next-intl"; | ||||||
|   { id: 1, pagename: "Главная", pathname: "/" }, | 
 | ||||||
|   { id: 2, pagename: "О нас", pathname: "/about-us" }, | export const LINKS = () => { | ||||||
|   { id: 3, pagename: "Статистика", pathname: "/statistics" }, |   const t = useTranslations("navigation"); | ||||||
|   { id: 4, pagename: "Новости", pathname: "/news" }, | 
 | ||||||
|   { id: 5, pagename: "Волонтеры", pathname: "/volunteers" }, |   const LINKS = [ | ||||||
| ]; |     { id: 1, pagename: t("home"), pathname: "/" }, | ||||||
|  |     { id: 2, pagename: t("about_us"), pathname: "/about-us" }, | ||||||
|  |     { id: 3, pagename: t("statistics"), pathname: "/statistics" }, | ||||||
|  |     { id: 4, pagename: t("news"), pathname: "/news" }, | ||||||
|  |     { id: 5, pagename: t("volunteers"), pathname: "/volunteers" }, | ||||||
|  |   ]; | ||||||
|  | 
 | ||||||
|  |   return LINKS; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // export const LINKS = [
 | ||||||
|  | //   { id: 1, pagename: "Главная", pathname: "/" },
 | ||||||
|  | //   { id: 2, pagename: "О нас", pathname: "/about-us" },
 | ||||||
|  | //   { id: 3, pagename: "Статистика", pathname: "/statistics" },
 | ||||||
|  | //   { id: 4, pagename: "Новости", pathname: "/news" },
 | ||||||
|  | //   { id: 5, pagename: "Волонтеры", pathname: "/volunteers" },
 | ||||||
|  | // ];
 | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								src/shared/variables/month.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,14 @@ | |||||||
|  | export const MONTHS: Record<string, string> = { | ||||||
|  |   "01": "Январь", | ||||||
|  |   "02": "Февраль", | ||||||
|  |   "03": "Март", | ||||||
|  |   "04": "Апрель", | ||||||
|  |   "05": "Май", | ||||||
|  |   "06": "Июнь", | ||||||
|  |   "07": "Июль", | ||||||
|  |   "08": "Август", | ||||||
|  |   "09": "Сентябрь", | ||||||
|  |   "10": "Октябрь", | ||||||
|  |   "11": "Ноябрь", | ||||||
|  |   "12": "Декабрь", | ||||||
|  | }; | ||||||
| @ -1,20 +1,4 @@ | |||||||
| export const ROAD_TYPES_STATS: Record<number, string> = { | import { getTranslations } from "next-intl/server"; | ||||||
|   1: "Разбитых дорог", |  | ||||||
|   2: "Очагов аварийности", |  | ||||||
|   3: "Локальных дефектов", |  | ||||||
|   4: "В планах ремонта", |  | ||||||
|   5: "Отремонтировано", |  | ||||||
|   6: "Локальных дефектов исправлено", |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const ROAD_TYPES: Record<number, string> = { |  | ||||||
|   1: "Разбитая дорога", |  | ||||||
|   2: "Очаг аварийности", |  | ||||||
|   3: "Локальный дефект", |  | ||||||
|   4: "В планах ремонта", |  | ||||||
|   5: "Отремонтировано", |  | ||||||
|   6: "Локальный дефект исправлен", |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| export const ROAD_TYPES_COLORS: Record<number, string> = { | export const ROAD_TYPES_COLORS: Record<number, string> = { | ||||||
|   1: "rgb(230, 68, 82)", |   1: "rgb(230, 68, 82)", | ||||||
|  | |||||||
| @ -1,6 +1,5 @@ | |||||||
| import "./Footer.scss"; | import "./Footer.scss"; | ||||||
| import logo from "../../shared/assets/logo.svg"; | import logo from "../../shared/assets/logo.svg"; | ||||||
| import Link from "next/link"; |  | ||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
| import { LINKS } from "@/shared/variables/links"; | import { LINKS } from "@/shared/variables/links"; | ||||||
| import youtube from "./icons/youtube.svg"; | import youtube from "./icons/youtube.svg"; | ||||||
| @ -8,17 +7,20 @@ import facebook from "./icons/facebook.svg"; | |||||||
| import instagram from "./icons/instagram.svg"; | import instagram from "./icons/instagram.svg"; | ||||||
| import app_store_btn from "./icons/app-store-btn.svg"; | import app_store_btn from "./icons/app-store-btn.svg"; | ||||||
| import play_market_btn from "./icons/play-market-btn.svg"; | import play_market_btn from "./icons/play-market-btn.svg"; | ||||||
|  | import { Link } from "@/shared/config/navigation"; | ||||||
|  | import { useTranslations } from "next-intl"; | ||||||
| 
 | 
 | ||||||
| const Footer = () => { | const Footer = () => { | ||||||
|  |   const t = useTranslations("general"); | ||||||
|   return ( |   return ( | ||||||
|     <footer className="footer"> |     <footer className="footer"> | ||||||
|       <Link href="/"> |       <Link href="/"> | ||||||
|         <Image src={logo} alt="Logo" /> |         <Image src={logo} alt="Logo" /> | ||||||
|       </Link> |       </Link> | ||||||
|       <div className="footer__links"> |       <div className="footer__links"> | ||||||
|         <h4>Навигация</h4> |         <h4>{t("navigation")}</h4> | ||||||
|         <ul> |         <ul> | ||||||
|           {LINKS.map((link) => ( |           {LINKS().map((link) => ( | ||||||
|             <li key={link.id}> |             <li key={link.id}> | ||||||
|               <Link href={link.pathname} key={link.id}> |               <Link href={link.pathname} key={link.id}> | ||||||
|                 {link.pagename} |                 {link.pagename} | ||||||
| @ -29,7 +31,7 @@ const Footer = () => { | |||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <div className="footer__contacts"> |       <div className="footer__contacts"> | ||||||
|         <h4>Контакты</h4> |         <h4>{t("contacts")}</h4> | ||||||
|         <ul> |         <ul> | ||||||
|           <li>namename@gmail.com</li> |           <li>namename@gmail.com</li> | ||||||
|           <li>+09646895467</li> |           <li>+09646895467</li> | ||||||
| @ -44,7 +46,7 @@ const Footer = () => { | |||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <div className="footer__apps"> |       <div className="footer__apps"> | ||||||
|         <h4>Скачивай наше приложение</h4> |         <h4>{t("download_our_app")}</h4> | ||||||
|         <div className="footer__apps-btns"> |         <div className="footer__apps-btns"> | ||||||
|           {[app_store_btn, play_market_btn].map((app, i) => ( |           {[app_store_btn, play_market_btn].map((app, i) => ( | ||||||
|             <Link key={i} href="#"> |             <Link key={i} href="#"> | ||||||
|  | |||||||
| @ -1,7 +1,8 @@ | |||||||
| import Link from "next/link"; |  | ||||||
| import "./NavAuth.scss"; | import "./NavAuth.scss"; | ||||||
| import { usePathname } from "next/navigation"; | import { usePathname } from "next/navigation"; | ||||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||||
|  | import { Link } from "@/shared/config/navigation"; | ||||||
|  | import { useTranslations } from "next-intl"; | ||||||
| 
 | 
 | ||||||
| interface INavAuthProps { | interface INavAuthProps { | ||||||
|   responsible?: boolean; |   responsible?: boolean; | ||||||
| @ -12,6 +13,7 @@ const NavAuth: React.FC<INavAuthProps> = ({ | |||||||
|   responsible, |   responsible, | ||||||
|   setOpenMenu, |   setOpenMenu, | ||||||
| }: INavAuthProps) => { | }: INavAuthProps) => { | ||||||
|  |   const t = useTranslations("navigation"); | ||||||
|   const session = useSession(); |   const session = useSession(); | ||||||
|   const auth = session.status === "authenticated" ? true : false; |   const auth = session.status === "authenticated" ? true : false; | ||||||
|   const pathname = usePathname(); |   const pathname = usePathname(); | ||||||
| @ -27,7 +29,7 @@ const NavAuth: React.FC<INavAuthProps> = ({ | |||||||
|               : "lg" |               : "lg" | ||||||
|           }`}
 |           }`}
 | ||||||
|         > |         > | ||||||
|           Профиль |           {t("profile")} | ||||||
|         </Link> |         </Link> | ||||||
|       ) : ( |       ) : ( | ||||||
|         <Link |         <Link | ||||||
| @ -39,7 +41,7 @@ const NavAuth: React.FC<INavAuthProps> = ({ | |||||||
|               : "lg" |               : "lg" | ||||||
|           }`}
 |           }`}
 | ||||||
|         > |         > | ||||||
|           Войти |           {t("login")} | ||||||
|         </Link> |         </Link> | ||||||
|       )} |       )} | ||||||
|     </> |     </> | ||||||
|  | |||||||
| @ -1,20 +1,52 @@ | |||||||
|  | "use client"; | ||||||
|  | 
 | ||||||
| import "./NavLanguage.scss"; | import "./NavLanguage.scss"; | ||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
| import { useState } from "react"; | import { useEffect, useRef, useState, useTransition } from "react"; | ||||||
| import globus from "./icons/globus.svg"; | import globus from "./icons/globus.svg"; | ||||||
| import chevron from "./icons/chevron-down.svg"; | import chevron from "./icons/chevron-down.svg"; | ||||||
| import check from "./icons/check.svg"; | import check from "./icons/check.svg"; | ||||||
|  | import { useParams, useRouter } from "next/navigation"; | ||||||
|  | import { LANGUAGES } from "./variables"; | ||||||
|  | import { usePathname } from "@/shared/config/navigation"; | ||||||
| 
 | 
 | ||||||
| const NavLanguage = () => { | const NavLanguage = () => { | ||||||
|   const [language, setLanguage] = useState<string>("ru"); |  | ||||||
|   const [openMenu, setOpenMenu] = useState<boolean>(false); |   const [openMenu, setOpenMenu] = useState<boolean>(false); | ||||||
|   const LANGUAGES = [ |   const menuRef: React.RefObject<HTMLDivElement> = useRef(null); | ||||||
|     { id: 1, language: "Русский", index: "ru" }, | 
 | ||||||
|     { id: 2, language: "Кыргызча", index: "kg" }, |   const router = useRouter(); | ||||||
|     { id: 3, language: "English", index: "en" }, |   const pathname = usePathname(); | ||||||
|   ]; |   const { locale } = useParams(); | ||||||
|  | 
 | ||||||
|  |   const [_, startTransition] = useTransition(); | ||||||
|  |   const [language, setLanguage] = useState<string>(locale as string); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     const params = window.location.search || ""; | ||||||
|  |     startTransition(() => { | ||||||
|  |       router.replace(`/${language}${pathname}${params}`, { | ||||||
|  |         scroll: false, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }, [language]); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     function handleClickOutside(event: any) { | ||||||
|  |       if ( | ||||||
|  |         menuRef.current && | ||||||
|  |         !menuRef.current.contains(event.target) | ||||||
|  |       ) { | ||||||
|  |         setOpenMenu(false); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     document.addEventListener("mousedown", handleClickOutside); | ||||||
|  |     return () => { | ||||||
|  |       document.removeEventListener("mousedown", handleClickOutside); | ||||||
|  |     }; | ||||||
|  |   }, [menuRef]); | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className="nav-language"> |     <div ref={menuRef} className="nav-language"> | ||||||
|       <button |       <button | ||||||
|         onClick={() => setOpenMenu((prev) => !prev)} |         onClick={() => setOpenMenu((prev) => !prev)} | ||||||
|         className="nav-language__btn" |         className="nav-language__btn" | ||||||
| @ -29,7 +61,10 @@ const NavLanguage = () => { | |||||||
|               className={`nav-language__option${ |               className={`nav-language__option${ | ||||||
|                 language === lang.index ? "_active" : "" |                 language === lang.index ? "_active" : "" | ||||||
|               }`}
 |               }`}
 | ||||||
|               onClick={() => setLanguage(lang.index)} |               onClick={() => { | ||||||
|  |                 setLanguage(lang.index); | ||||||
|  |                 setOpenMenu(false); | ||||||
|  |               }} | ||||||
|               key={lang.id} |               key={lang.id} | ||||||
|             > |             > | ||||||
|               {lang.language} |               {lang.language} | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								src/widgets/Navbar/NavLanguage/variables.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,11 @@ | |||||||
|  | interface ILangs { | ||||||
|  |   id: number; | ||||||
|  |   language: string; | ||||||
|  |   index: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const LANGUAGES: ILangs[] = [ | ||||||
|  |   { id: 1, language: "Русский", index: "ru" }, | ||||||
|  |   { id: 2, language: "Кыргызча", index: "kg" }, | ||||||
|  |   { id: 3, language: "English", index: "en" }, | ||||||
|  | ]; | ||||||
| @ -1,8 +1,7 @@ | |||||||
| import { LINKS } from "@/shared/variables/links"; | import { LINKS } from "@/shared/variables/links"; | ||||||
| import "./NavMenu.scss"; | import "./NavMenu.scss"; | ||||||
| import Link from "next/link"; |  | ||||||
| import { usePathname } from "next/navigation"; |  | ||||||
| import NavAuth from "../NavAuth/NavAuth"; | import NavAuth from "../NavAuth/NavAuth"; | ||||||
|  | import { Link, usePathname } from "@/shared/config/navigation"; | ||||||
| 
 | 
 | ||||||
| interface INavMenuProps { | interface INavMenuProps { | ||||||
|   setOpenMenu: (boolean: boolean) => void; |   setOpenMenu: (boolean: boolean) => void; | ||||||
| @ -12,15 +11,16 @@ const NavMenu: React.FC<INavMenuProps> = ({ | |||||||
|   setOpenMenu, |   setOpenMenu, | ||||||
| }: INavMenuProps) => { | }: INavMenuProps) => { | ||||||
|   const pathname = usePathname(); |   const pathname = usePathname(); | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <nav className="nav-menu"> |     <nav className="nav-menu"> | ||||||
|       {LINKS.map((link) => ( |       {LINKS().map((link) => ( | ||||||
|         <Link |         <Link | ||||||
|           onClick={() => setOpenMenu(false)} |           onClick={() => setOpenMenu(false)} | ||||||
|           className={`nav-menu__link${ |           className={`nav-menu__link${ | ||||||
|             pathname === link.pathname ? "_active" : "" |             pathname.slice(4) === link.pathname ? "_active" : "" | ||||||
|           }`}
 |           }`}
 | ||||||
|           href={link.pathname} |           href={`${pathname.slice(1, 3)}/${link.pathname}`} | ||||||
|           key={link.id} |           key={link.id} | ||||||
|         > |         > | ||||||
|           {link.pagename} |           {link.pagename} | ||||||
|  | |||||||
| @ -2,15 +2,14 @@ | |||||||
| 
 | 
 | ||||||
| import "./Navbar.scss"; | import "./Navbar.scss"; | ||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
| import Link from "next/link"; | import { Link } from "@/shared/config/navigation"; | ||||||
| import { usePathname } from "next/navigation"; | import { usePathname } from "@/shared/config/navigation"; | ||||||
| import logo from "@/shared/assets/logo.svg"; | import logo from "@/shared/assets/logo.svg"; | ||||||
| import { LINKS } from "@/shared/variables/links"; | import { LINKS } from "@/shared/variables/links"; | ||||||
| import NavLanguage from "./NavLanguage/NavLanguage"; | import NavLanguage from "./NavLanguage/NavLanguage"; | ||||||
| import NavAuth from "./NavAuth/NavAuth"; | import NavAuth from "./NavAuth/NavAuth"; | ||||||
| import menu from "./icons/menu.svg"; | import menu from "./icons/menu.svg"; | ||||||
| import cross from "./icons/cross.svg"; | import cross from "./icons/cross.svg"; | ||||||
| 
 |  | ||||||
| import NavMenu from "./NavMenu/NavMenu"; | import NavMenu from "./NavMenu/NavMenu"; | ||||||
| import { useState } from "react"; | import { useState } from "react"; | ||||||
| 
 | 
 | ||||||
| @ -25,7 +24,7 @@ const Navbar = () => { | |||||||
|       </Link> |       </Link> | ||||||
| 
 | 
 | ||||||
|       <nav className="navbar__links"> |       <nav className="navbar__links"> | ||||||
|         {LINKS.map((link) => ( |         {LINKS().map((link) => ( | ||||||
|           <Link |           <Link | ||||||
|             className={`navbar__link${ |             className={`navbar__link${ | ||||||
|               pathname === link.pathname ? "_active" : "" |               pathname === link.pathname ? "_active" : "" | ||||||
|  | |||||||
| @ -2,8 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| import "./ProfileNav.scss"; | import "./ProfileNav.scss"; | ||||||
| import LogoutButton from "@/features/LogoutButton/LogoutButton"; | import LogoutButton from "@/features/LogoutButton/LogoutButton"; | ||||||
| import Link from "next/link"; | import { Link, usePathname } from "@/shared/config/navigation"; | ||||||
| import { usePathname } from "next/navigation"; |  | ||||||
| 
 | 
 | ||||||
| interface IProfileNavProps { | interface IProfileNavProps { | ||||||
|   report_count: number; |   report_count: number; | ||||||
|  | |||||||
| @ -20,6 +20,32 @@ | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   &__auth-warning, | ||||||
|  |   &__warning { | ||||||
|  |     height: 150px; | ||||||
|  |     margin-bottom: 70px; | ||||||
|  |     text-align: center; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  |     gap: 4px; | ||||||
|  | 
 | ||||||
|  |     font-size: 18px; | ||||||
|  |     font-weight: 500; | ||||||
|  |     line-height: 22px; | ||||||
|  | 
 | ||||||
|  |     border: 1px solid #c5c6c5; | ||||||
|  |     border-radius: 10px; | ||||||
|  | 
 | ||||||
|  |     button { | ||||||
|  |       text-decoration: underline; | ||||||
|  |       color: #0077b6; | ||||||
|  |       font-size: 18px; | ||||||
|  |       font-weight: 500; | ||||||
|  |       line-height: 22px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   form { |   form { | ||||||
|     margin-bottom: 70px; |     margin-bottom: 70px; | ||||||
|     display: flex; |     display: flex; | ||||||
|  | |||||||
| @ -3,10 +3,15 @@ | |||||||
| import "./ReviewSection.scss"; | import "./ReviewSection.scss"; | ||||||
| import { apiInstance } from "@/shared/config/apiConfig"; | import { apiInstance } from "@/shared/config/apiConfig"; | ||||||
| import { IReviewList } from "@/shared/types/review-type"; | import { IReviewList } from "@/shared/types/review-type"; | ||||||
| import { useSession } from "next-auth/react"; | import { signIn, useSession } from "next-auth/react"; | ||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| import calendar from "./icons/calendar.svg"; | import calendar from "./icons/calendar.svg"; | ||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
|  | import { | ||||||
|  |   Link, | ||||||
|  |   usePathname, | ||||||
|  |   useRouter, | ||||||
|  | } from "@/shared/config/navigation"; | ||||||
| 
 | 
 | ||||||
| interface IReviewsSectionProps { | interface IReviewsSectionProps { | ||||||
|   endpoint: string; |   endpoint: string; | ||||||
| @ -18,6 +23,7 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({ | |||||||
|   id, |   id, | ||||||
| }: IReviewsSectionProps) => { | }: IReviewsSectionProps) => { | ||||||
|   const [reviews, setReviews] = useState<IReviewList>(); |   const [reviews, setReviews] = useState<IReviewList>(); | ||||||
|  |   const pathname = usePathname(); | ||||||
|   const session = useSession(); |   const session = useSession(); | ||||||
|   const handleSubmit: React.MouseEventHandler< |   const handleSubmit: React.MouseEventHandler< | ||||||
|     HTMLFormElement |     HTMLFormElement | ||||||
| @ -33,6 +39,10 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({ | |||||||
|       }, |       }, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  |     if (session.status === "unauthenticated") { | ||||||
|  |       signIn(undefined, { callbackUrl: pathname }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     if (!formData.get("review")) { |     if (!formData.get("review")) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| @ -53,7 +63,7 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({ | |||||||
| 
 | 
 | ||||||
|   const getReviews = async () => { |   const getReviews = async () => { | ||||||
|     const response = await apiInstance.get<IReviewList>( |     const response = await apiInstance.get<IReviewList>( | ||||||
|       `/${endpoint}/${id}/reviews/` |       `/${endpoint}/${id}/reviews/?page_size=8` | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     setReviews(response.data); |     setReviews(response.data); | ||||||
| @ -83,16 +93,30 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({ | |||||||
|         <span id="blue-point" /> Написать комментарий |         <span id="blue-point" /> Написать комментарий | ||||||
|       </h3> |       </h3> | ||||||
| 
 | 
 | ||||||
|  |       {session.status === "authenticated" ? ( | ||||||
|         <form onSubmit={handleSubmit}> |         <form onSubmit={handleSubmit}> | ||||||
|           <textarea name="review" /> |           <textarea name="review" /> | ||||||
|           <button type="submit">Отправить</button> |           <button type="submit">Отправить</button> | ||||||
|         </form> |         </form> | ||||||
|  |       ) : ( | ||||||
|  |         <p className="review-section__auth-warning"> | ||||||
|  |           Перед тем как оставить комментарий, пожалуйста | ||||||
|  |           <button | ||||||
|  |             onClick={() => | ||||||
|  |               signIn(undefined, { callbackUrl: pathname }) | ||||||
|  |             } | ||||||
|  |           > | ||||||
|  |             войдите в аккаунт | ||||||
|  |           </button> | ||||||
|  |         </p> | ||||||
|  |       )} | ||||||
| 
 | 
 | ||||||
|       <div className="review-section__list"> |       <div className="review-section__list"> | ||||||
|         <h3> |         <h3> | ||||||
|           <span id="blue-point" /> Комментарии |           <span id="blue-point" /> Комментарии | ||||||
|         </h3> |         </h3> | ||||||
| 
 | 
 | ||||||
|  |         {reviews?.results.length !== 0 ? ( | ||||||
|           <ul> |           <ul> | ||||||
|             {reviews?.results.map((review) => ( |             {reviews?.results.map((review) => ( | ||||||
|               <li key={review.id} className="review"> |               <li key={review.id} className="review"> | ||||||
| @ -124,6 +148,11 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({ | |||||||
|               </li> |               </li> | ||||||
|             ))} |             ))} | ||||||
|           </ul> |           </ul> | ||||||
|  |         ) : ( | ||||||
|  |           <p className="review-section__warning"> | ||||||
|  |             Оставьте комментарий первым :) | ||||||
|  |           </p> | ||||||
|  |         )} | ||||||
|       </div> |       </div> | ||||||
|     </section> |     </section> | ||||||
|   ); |   ); | ||||||
|  | |||||||
| @ -1,10 +1,10 @@ | |||||||
| "use client"; | "use client"; | ||||||
| 
 | 
 | ||||||
| import { useEffect, useState } from "react"; |  | ||||||
| import "./ConfirmEmailForm.scss"; | import "./ConfirmEmailForm.scss"; | ||||||
|  | import { useEffect, useState } from "react"; | ||||||
| import { apiInstance } from "@/shared/config/apiConfig"; | import { apiInstance } from "@/shared/config/apiConfig"; | ||||||
| import { useRouter } from "next/navigation"; |  | ||||||
| import Loader from "@/shared/ui/components/Loader/Loader"; | import Loader from "@/shared/ui/components/Loader/Loader"; | ||||||
|  | import { useRouter } from "@/shared/config/navigation"; | ||||||
| 
 | 
 | ||||||
| interface IConfirmEmailFormProps { | interface IConfirmEmailFormProps { | ||||||
|   email: string; |   email: string; | ||||||
| @ -5,8 +5,8 @@ import "./confirm-code.scss"; | |||||||
| import { useState } from "react"; | import { useState } from "react"; | ||||||
| import { apiInstance } from "@/shared/config/apiConfig"; | import { apiInstance } from "@/shared/config/apiConfig"; | ||||||
| import Loader from "@/shared/ui/components/Loader/Loader"; | import Loader from "@/shared/ui/components/Loader/Loader"; | ||||||
| import { useRouter } from "next/navigation"; |  | ||||||
| import { ITokens } from "@/shared/types/token-type"; | import { ITokens } from "@/shared/types/token-type"; | ||||||
|  | import { useRouter } from "@/shared/config/navigation"; | ||||||
| 
 | 
 | ||||||
| interface IConfirmCodeProps { | interface IConfirmCodeProps { | ||||||
|   setChangeForm: (boolean: boolean) => void; |   setChangeForm: (boolean: boolean) => void; | ||||||
| Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB | 
| Before Width: | Height: | Size: 739 B After Width: | Height: | Size: 739 B | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 901 B After Width: | Height: | Size: 901 B | 
| @ -3,16 +3,14 @@ | |||||||
| import "./ProfileForm.scss"; | import "./ProfileForm.scss"; | ||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
| import pen from "./icons/pen.svg"; | import pen from "./icons/pen.svg"; | ||||||
| import eye_off from "./icons/eye-off.svg"; |  | ||||||
| import eye_on from "./icons/eye-on.svg"; |  | ||||||
| import { useState } from "react"; | import { useState } from "react"; | ||||||
| import { apiInstance } from "@/shared/config/apiConfig"; | import { apiInstance, authInstanse } from "@/shared/config/apiConfig"; | ||||||
| import { AxiosError } from "axios"; | import { AxiosError } from "axios"; | ||||||
| import Loader from "@/shared/ui/components/Loader/Loader"; | import Loader from "@/shared/ui/components/Loader/Loader"; | ||||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||||
| import { useRouter } from "next/navigation"; |  | ||||||
| import LogoutButton from "@/features/LogoutButton/LogoutButton"; | import LogoutButton from "@/features/LogoutButton/LogoutButton"; | ||||||
| import ChangePassword from "./ChangePassword/ChangePassword"; | import ChangePassword from "./ChangePassword/ChangePassword"; | ||||||
|  | import { useRouter } from "@/shared/config/navigation"; | ||||||
| 
 | 
 | ||||||
| interface IProfileFormProps { | interface IProfileFormProps { | ||||||
|   id: number; |   id: number; | ||||||
| Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB | 
| @ -19,8 +19,8 @@ import axios, { AxiosError } from "axios"; | |||||||
| import { apiInstance } from "@/shared/config/apiConfig"; | import { apiInstance } from "@/shared/config/apiConfig"; | ||||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||||
| import { IDisplayMap } from "@/shared/types/map-type"; | import { IDisplayMap } from "@/shared/types/map-type"; | ||||||
| import { useRouter } from "next/navigation"; |  | ||||||
| import Loader from "@/shared/ui/components/Loader/Loader"; | import Loader from "@/shared/ui/components/Loader/Loader"; | ||||||
|  | import { useRouter } from "@/shared/config/navigation"; | ||||||
| 
 | 
 | ||||||
| interface ILatLng { | interface ILatLng { | ||||||
|   lat: number; |   lat: number; | ||||||
| Before Width: | Height: | Size: 804 B After Width: | Height: | Size: 804 B |