forked from Transparency/kgroad-frontend2
added next-intl, composed next-auth and next-intl in middleware, fixed some bugs, completed details page, added google auth
This commit is contained in:
1 parent
9cd7b2a67d
commit
6637ab4eae
172 files changed
+1772
-502
No files matched your search
Vendored
+8
-2
@@ -4,7 +4,13 @@ declare module "next-auth" {
|
||||
interface Session {
|
||||
refresh_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 {
|
||||
refresh_token: string;
|
||||
access_token: string;
|
||||
expires_in: string;
|
||||
expires_in?: string;
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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": "Четке калган иштеш коду."
|
||||
}
|
||||
}
|
||||
@@ -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": "Неверный код."
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -1,3 +1,6 @@
|
||||
import createNextIntlPlugin from "next-intl/plugin";
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
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",
|
||||
"next": "14.1.0",
|
||||
"next-auth": "^4.24.5",
|
||||
"next-intl": "^3.9.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-leaflet": "^4.2.1",
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -10,7 +10,7 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
const DynamicForm = dynamic(
|
||||
() => import("@/widgets/ReportForm/ReportForm"),
|
||||
() => import("@/widgets/forms/ReportForm/ReportForm"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
File renamed without changes.
@@ -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>
|
||||
);
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,7 +1,5 @@
|
||||
import "./News.scss";
|
||||
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 { Metadata } from "next";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Header from "@/widgets/Header/Header";
|
||||
import StatisticsSection from "@/widgets/StatisticsSection/StatisticsSection";
|
||||
import RatingSection from "@/widgets/RatingSection/RatingSection";
|
||||
import NewsSection from "@/widgets/NewsSection/NewsSection";
|
||||
import MapSection from "@/widgets/MapSection/MapSection";
|
||||
import Header from "@/widgets/home/Header/Header";
|
||||
import StatisticsSection from "@/widgets/home/StatisticsSection/StatisticsSection";
|
||||
import RatingSection from "@/widgets/home/RatingSection/RatingSection";
|
||||
import NewsSection from "@/widgets/home/NewsSection/NewsSection";
|
||||
import MapSection from "@/widgets/home/MapSection/MapSection";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -14,13 +14,9 @@ const AuthGuard = ({ children }: { children: React.ReactNode }) => {
|
||||
};
|
||||
await apiInstance.post("/token/verify/", data);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response?.data.code === "token_not_valid") {
|
||||
signOut({
|
||||
callbackUrl: "/",
|
||||
});
|
||||
}
|
||||
}
|
||||
signOut({
|
||||
callbackUrl: "/",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
File renamed without changes.
@@ -6,7 +6,6 @@ import { AxiosError } from "axios";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authConfig } from "@/shared/config/authConfig";
|
||||
import { IProfile } from "@/shared/types/profile-type";
|
||||
import { Metadata } from "next";
|
||||
|
||||
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";
|
||||
|
||||
const MyReports = async ({
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <div>page</div>;
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import ProfileAvatar from "@/features/ProfileAvatar/ProfileAvatar";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { authConfig } from "@/shared/config/authConfig";
|
||||
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 { getServerSession } from "next-auth";
|
||||
import React from "react";
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -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
-1
@@ -1,5 +1,5 @@
|
||||
import "@/shared/ui/auth-classes.scss";
|
||||
import ForgotPasswordForm from "@/widgets/ForgotPasswordForm/ForgotPasswordForm";
|
||||
import ForgotPasswordForm from "@/widgets/forms/ForgotPasswordForm/ForgotPasswordForm";
|
||||
|
||||
const ForgotPassword = () => {
|
||||
return (
|
||||
File renamed without changes.
@@ -1,9 +1,9 @@
|
||||
import "@/shared/ui/auth-classes.scss";
|
||||
import Image from "next/image";
|
||||
import sign_in_icon from "./icons/sign-in_icon.svg";
|
||||
import SignInForm from "@/widgets/SignInForm/SignInForm";
|
||||
import Link from "next/link";
|
||||
import SignInForm from "@/widgets/forms/SignInForm/SignInForm";
|
||||
import { Metadata } from "next";
|
||||
import { Link } from "@/shared/config/navigation";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "KG ROAD | Вход",
|
||||
File renamed without changes.
@@ -1,7 +1,7 @@
|
||||
import "@/shared/ui/auth-classes.scss";
|
||||
import Image from "next/image";
|
||||
import key from "./icons/key.svg";
|
||||
import ResetCodeForm from "@/widgets/ResetCodeForm/ResetCodeForm";
|
||||
import ResetCodeForm from "@/widgets/forms/ResetCodeForm/ResetCodeForm";
|
||||
|
||||
const ResetCode = () => {
|
||||
return (
|
||||
File renamed without changes.
+1
-1
@@ -1,7 +1,7 @@
|
||||
import "@/shared/ui/auth-classes.scss";
|
||||
import Image from "next/image";
|
||||
import mail from "./icons/mail.svg";
|
||||
import ConfirmEmailForm from "@/widgets/ConfirmEmailForm/ConfirmEmailForm";
|
||||
import ConfirmEmailForm from "@/widgets/forms/ConfirmEmailForm/ConfirmEmailForm";
|
||||
|
||||
const ConfirmEmail = ({
|
||||
searchParams,
|
||||
File renamed without changes.
@@ -1,9 +1,9 @@
|
||||
import "@/shared/ui/auth-classes.scss";
|
||||
import Image from "next/image";
|
||||
import flag from "./icons/flag.svg";
|
||||
import Link from "next/link";
|
||||
import SignUpForm from "@/widgets/SignUpForm/SignUpForm";
|
||||
import SignUpForm from "@/widgets/forms/SignUpForm/SignUpForm";
|
||||
import { Metadata } from "next";
|
||||
import { Link } from "@/shared/config/navigation";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "KG ROAD | Регистрация",
|
||||
File renamed without changes.
@@ -1,9 +1,6 @@
|
||||
import Typography from "@/shared/ui/components/Typography/Typography";
|
||||
import "./Statistics.scss";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { IStatistics } from "@/shared/types/statistics-type";
|
||||
import { AxiosError } from "axios";
|
||||
import StatisticsTable from "@/widgets/StatisticsTable/StatisticsTable";
|
||||
import StatisticsTable from "@/widgets/tables/StatisticsTable/StatisticsTable";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
File renamed without changes.
@@ -1,6 +1,6 @@
|
||||
import Typography from "@/shared/ui/components/Typography/Typography";
|
||||
import "./Volunteers.scss";
|
||||
import VolunteersTable from "@/widgets/VolunteersTable/VolunteersTable";
|
||||
import VolunteersTable from "@/widgets/tables/VolunteersTable/VolunteersTable";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
+7
-23
@@ -1,25 +1,9 @@
|
||||
import type { Metadata } from "next";
|
||||
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";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<Providers>
|
||||
<Navbar />
|
||||
<div className="app">{children}</div>
|
||||
<Footer />
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: Props) {
|
||||
return children;
|
||||
}
|
||||
@@ -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 "./NewsCard.scss";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface INewsCard {
|
||||
id: number;
|
||||
@@ -19,6 +20,7 @@ const NewsCard: React.FC<INewsCard> = ({
|
||||
description,
|
||||
date,
|
||||
}: INewsCard) => {
|
||||
const t = useTranslations("general");
|
||||
const sliceTitle = (title: string) => {
|
||||
if (title.length > 35) {
|
||||
return `${title.slice(0, 35)}...`;
|
||||
@@ -57,7 +59,7 @@ const NewsCard: React.FC<INewsCard> = ({
|
||||
</div>
|
||||
|
||||
<Link href={`/news/${id}`} className="news-card__more-btn">
|
||||
Подробнее
|
||||
{t("details")}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -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 "./GoogleButton.scss";
|
||||
import google from "./icons/google.svg";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
|
||||
const GoogleButton = () => {
|
||||
const session = useSession();
|
||||
const searchParams = useSearchParams();
|
||||
const callbackUrl =
|
||||
searchParams.get("callbackUrl") || "/profile/personal";
|
||||
|
||||
const googleLogin = async () => {
|
||||
await signIn("google", { callbackUrl });
|
||||
};
|
||||
|
||||
return (
|
||||
<button className="google-btn">
|
||||
<button
|
||||
type="button"
|
||||
className="google-btn"
|
||||
onClick={googleLogin}
|
||||
>
|
||||
<Image src={google} alt="Google Icon" />
|
||||
Войти через Google
|
||||
</button>
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import Image from "next/image";
|
||||
import "./ProfileAvatar.scss";
|
||||
import pen from "./icons/pen.svg";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { authInstanse } from "@/shared/config/apiConfig";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
interface IProfileAvatarProps {
|
||||
img: string;
|
||||
@@ -19,24 +19,18 @@ const ProfileAvatar: React.FC<IProfileAvatarProps> = ({
|
||||
const changeImage: React.ChangeEventHandler<
|
||||
HTMLInputElement
|
||||
> = async (e) => {
|
||||
const Authorization = `Bearer ${session.data?.access_token}`;
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization,
|
||||
},
|
||||
};
|
||||
const formData = new FormData();
|
||||
if (e.target.files) {
|
||||
const image = Array.from(e.target.files);
|
||||
formData.append("image", image[0]);
|
||||
}
|
||||
|
||||
if (session.status === "unauthenticated") return;
|
||||
|
||||
try {
|
||||
const res = await apiInstance.patch(
|
||||
"/users/update_image/",
|
||||
formData,
|
||||
config
|
||||
);
|
||||
const res = await authInstanse(
|
||||
session.data?.access_token as string
|
||||
).patch("/users/update_image/", formData);
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
@@ -4,9 +4,9 @@ import "./ReportLike.scss";
|
||||
import Image from "next/image";
|
||||
import like from "./icons/like.svg";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import { useEffect } from "react";
|
||||
import { usePathname, useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface IReportLikeProps {
|
||||
count: number;
|
||||
@@ -20,6 +20,7 @@ const ReportLike: React.FC<IReportLikeProps> = ({
|
||||
const session = useSession();
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const Authorization = `Bearer ${session?.data?.access_token}`;
|
||||
const config = {
|
||||
@@ -29,6 +30,10 @@ const ReportLike: React.FC<IReportLikeProps> = ({
|
||||
};
|
||||
|
||||
const checkLike = async () => {
|
||||
if (session.status === "unauthenticated") {
|
||||
signIn(undefined, { callbackUrl: pathname });
|
||||
}
|
||||
|
||||
const res = await apiInstance.get<{ detail: string }>(
|
||||
`/report/${report_id}/like/check/`,
|
||||
config
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import "./SearchForm.scss";
|
||||
import Image from "next/image";
|
||||
import search from "./icons/search.svg";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface ISearchFormProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
@@ -17,6 +18,7 @@ const SearchForm: React.FC<ISearchFormProps> = ({
|
||||
onChange,
|
||||
style,
|
||||
}: ISearchFormProps) => {
|
||||
const t = useTranslations("general");
|
||||
return (
|
||||
<form
|
||||
style={style}
|
||||
@@ -33,7 +35,7 @@ const SearchForm: React.FC<ISearchFormProps> = ({
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit">Поиск</button>
|
||||
<button type="submit">{t("search")}</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
File renamed without changes.
@@ -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;
|
||||
File renamed without changes.
+11
@@ -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,
|
||||
};
|
||||
});
|
||||
+48
-6
@@ -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";
|
||||
|
||||
const privatePages = [
|
||||
"/profile",
|
||||
"/profile/personal",
|
||||
"/profile/my-reports",
|
||||
"/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: [
|
||||
"/profile/personal",
|
||||
"/profile/my-reports",
|
||||
"/create-report",
|
||||
],
|
||||
matcher: ["/((?!api|_next|.*\\..*).*)"],
|
||||
};
|
||||
@@ -5,3 +5,13 @@ const API_URL = process.env["NEXT_PUBLIC_BASE_API"];
|
||||
export const apiInstance = axios.create({
|
||||
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 { JWT } from "next-auth/jwt";
|
||||
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 {
|
||||
access: string;
|
||||
}
|
||||
const verifyToken = async (access_token: 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 data = {
|
||||
refresh: token.refresh_token,
|
||||
};
|
||||
// const date = new Date().toLocaleTimeString();
|
||||
// const expire = new Date(token.expires_in).toLocaleTimeString();
|
||||
|
||||
const response = await axios.post<IToken>(
|
||||
"https://api.kgroaduat.fishrungames.com/api/v1/token/refresh/",
|
||||
const verify = await verifyToken(token.access_token);
|
||||
|
||||
if (verify)
|
||||
return {
|
||||
...token,
|
||||
};
|
||||
|
||||
const response = await apiInstance.post<IRefresh>(
|
||||
"/users/refresh/",
|
||||
data
|
||||
);
|
||||
|
||||
return {
|
||||
...token,
|
||||
access_token: response.data.access,
|
||||
expires_in: response.data.expires_at,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -27,7 +48,6 @@ export const authConfig: AuthOptions = {
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
name: "Credentials",
|
||||
|
||||
credentials: {
|
||||
email: {
|
||||
label: "Email",
|
||||
@@ -36,35 +56,30 @@ export const authConfig: AuthOptions = {
|
||||
},
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
|
||||
async authorize(credentials, req) {
|
||||
if (!credentials?.email || !credentials?.password)
|
||||
return null;
|
||||
|
||||
const { email, password } = credentials as any;
|
||||
const data = {
|
||||
email,
|
||||
password,
|
||||
};
|
||||
|
||||
const res = await fetch(
|
||||
"https://api.kgroaduat.fishrungames.com/api/v1/users/login/",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
}
|
||||
);
|
||||
const res = await apiInstance.post("/users/login/", data);
|
||||
|
||||
if (res.status.toString()[0] === "4") {
|
||||
if (![200, 201].includes(res.status)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await res.json();
|
||||
const user = res.data;
|
||||
return user;
|
||||
},
|
||||
}),
|
||||
GoogleProvider({
|
||||
clientId: process.env.CLIENT_ID as string,
|
||||
clientSecret: process.env.CLIENT_SECRET as string,
|
||||
}),
|
||||
],
|
||||
pages: {
|
||||
signIn: "/sign-in",
|
||||
@@ -73,9 +88,39 @@ export const authConfig: AuthOptions = {
|
||||
strategy: "jwt",
|
||||
},
|
||||
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 (token.exp) {
|
||||
return token;
|
||||
}
|
||||
|
||||
return refreshToken(token);
|
||||
},
|
||||
|
||||
|
||||
@@ -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 {
|
||||
id: 4;
|
||||
id: number;
|
||||
latitude: string;
|
||||
longitude: string;
|
||||
address: string;
|
||||
|
||||
@@ -3,3 +3,8 @@ export interface ITokens {
|
||||
access_token: string;
|
||||
expires_in: string;
|
||||
}
|
||||
|
||||
export interface IRefresh {
|
||||
access: string;
|
||||
expires_at: string;
|
||||
}
|
||||
@@ -1,7 +1,23 @@
|
||||
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" },
|
||||
];
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export const LINKS = () => {
|
||||
const t = useTranslations("navigation");
|
||||
|
||||
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" },
|
||||
// ];
|
||||
@@ -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> = {
|
||||
1: "Разбитых дорог",
|
||||
2: "Очагов аварийности",
|
||||
3: "Локальных дефектов",
|
||||
4: "В планах ремонта",
|
||||
5: "Отремонтировано",
|
||||
6: "Локальных дефектов исправлено",
|
||||
};
|
||||
|
||||
export const ROAD_TYPES: Record<number, string> = {
|
||||
1: "Разбитая дорога",
|
||||
2: "Очаг аварийности",
|
||||
3: "Локальный дефект",
|
||||
4: "В планах ремонта",
|
||||
5: "Отремонтировано",
|
||||
6: "Локальный дефект исправлен",
|
||||
};
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
export const ROAD_TYPES_COLORS: Record<number, string> = {
|
||||
1: "rgb(230, 68, 82)",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import "./Footer.scss";
|
||||
import logo from "../../shared/assets/logo.svg";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { LINKS } from "@/shared/variables/links";
|
||||
import youtube from "./icons/youtube.svg";
|
||||
@@ -8,17 +7,20 @@ import facebook from "./icons/facebook.svg";
|
||||
import instagram from "./icons/instagram.svg";
|
||||
import app_store_btn from "./icons/app-store-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 t = useTranslations("general");
|
||||
return (
|
||||
<footer className="footer">
|
||||
<Link href="/">
|
||||
<Image src={logo} alt="Logo" />
|
||||
</Link>
|
||||
<div className="footer__links">
|
||||
<h4>Навигация</h4>
|
||||
<h4>{t("navigation")}</h4>
|
||||
<ul>
|
||||
{LINKS.map((link) => (
|
||||
{LINKS().map((link) => (
|
||||
<li key={link.id}>
|
||||
<Link href={link.pathname} key={link.id}>
|
||||
{link.pagename}
|
||||
@@ -29,7 +31,7 @@ const Footer = () => {
|
||||
</div>
|
||||
|
||||
<div className="footer__contacts">
|
||||
<h4>Контакты</h4>
|
||||
<h4>{t("contacts")}</h4>
|
||||
<ul>
|
||||
<li>namename@gmail.com</li>
|
||||
<li>+09646895467</li>
|
||||
@@ -44,7 +46,7 @@ const Footer = () => {
|
||||
</div>
|
||||
|
||||
<div className="footer__apps">
|
||||
<h4>Скачивай наше приложение</h4>
|
||||
<h4>{t("download_our_app")}</h4>
|
||||
<div className="footer__apps-btns">
|
||||
{[app_store_btn, play_market_btn].map((app, i) => (
|
||||
<Link key={i} href="#">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import Link from "next/link";
|
||||
import "./NavAuth.scss";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { Link } from "@/shared/config/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface INavAuthProps {
|
||||
responsible?: boolean;
|
||||
@@ -12,6 +13,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
|
||||
responsible,
|
||||
setOpenMenu,
|
||||
}: INavAuthProps) => {
|
||||
const t = useTranslations("navigation");
|
||||
const session = useSession();
|
||||
const auth = session.status === "authenticated" ? true : false;
|
||||
const pathname = usePathname();
|
||||
@@ -27,7 +29,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
|
||||
: "lg"
|
||||
}`}
|
||||
>
|
||||
Профиль
|
||||
{t("profile")}
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
@@ -39,7 +41,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
|
||||
: "lg"
|
||||
}`}
|
||||
>
|
||||
Войти
|
||||
{t("login")}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,20 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import "./NavLanguage.scss";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState, useTransition } from "react";
|
||||
import globus from "./icons/globus.svg";
|
||||
import chevron from "./icons/chevron-down.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 [language, setLanguage] = useState<string>("ru");
|
||||
const [openMenu, setOpenMenu] = useState<boolean>(false);
|
||||
const LANGUAGES = [
|
||||
{ id: 1, language: "Русский", index: "ru" },
|
||||
{ id: 2, language: "Кыргызча", index: "kg" },
|
||||
{ id: 3, language: "English", index: "en" },
|
||||
];
|
||||
const menuRef: React.RefObject<HTMLDivElement> = useRef(null);
|
||||
|
||||
const router = useRouter();
|
||||
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 (
|
||||
<div className="nav-language">
|
||||
<div ref={menuRef} className="nav-language">
|
||||
<button
|
||||
onClick={() => setOpenMenu((prev) => !prev)}
|
||||
className="nav-language__btn"
|
||||
@@ -29,7 +61,10 @@ const NavLanguage = () => {
|
||||
className={`nav-language__option${
|
||||
language === lang.index ? "_active" : ""
|
||||
}`}
|
||||
onClick={() => setLanguage(lang.index)}
|
||||
onClick={() => {
|
||||
setLanguage(lang.index);
|
||||
setOpenMenu(false);
|
||||
}}
|
||||
key={lang.id}
|
||||
>
|
||||
{lang.language}
|
||||
|
||||
@@ -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 "./NavMenu.scss";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import NavAuth from "../NavAuth/NavAuth";
|
||||
import { Link, usePathname } from "@/shared/config/navigation";
|
||||
|
||||
interface INavMenuProps {
|
||||
setOpenMenu: (boolean: boolean) => void;
|
||||
@@ -12,15 +11,16 @@ const NavMenu: React.FC<INavMenuProps> = ({
|
||||
setOpenMenu,
|
||||
}: INavMenuProps) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<nav className="nav-menu">
|
||||
{LINKS.map((link) => (
|
||||
{LINKS().map((link) => (
|
||||
<Link
|
||||
onClick={() => setOpenMenu(false)}
|
||||
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}
|
||||
>
|
||||
{link.pagename}
|
||||
|
||||
@@ -2,15 +2,14 @@
|
||||
|
||||
import "./Navbar.scss";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Link } from "@/shared/config/navigation";
|
||||
import { usePathname } from "@/shared/config/navigation";
|
||||
import logo from "@/shared/assets/logo.svg";
|
||||
import { LINKS } from "@/shared/variables/links";
|
||||
import NavLanguage from "./NavLanguage/NavLanguage";
|
||||
import NavAuth from "./NavAuth/NavAuth";
|
||||
import menu from "./icons/menu.svg";
|
||||
import cross from "./icons/cross.svg";
|
||||
|
||||
import NavMenu from "./NavMenu/NavMenu";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -25,7 +24,7 @@ const Navbar = () => {
|
||||
</Link>
|
||||
|
||||
<nav className="navbar__links">
|
||||
{LINKS.map((link) => (
|
||||
{LINKS().map((link) => (
|
||||
<Link
|
||||
className={`navbar__link${
|
||||
pathname === link.pathname ? "_active" : ""
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import "./ProfileNav.scss";
|
||||
import LogoutButton from "@/features/LogoutButton/LogoutButton";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Link, usePathname } from "@/shared/config/navigation";
|
||||
|
||||
interface IProfileNavProps {
|
||||
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 {
|
||||
margin-bottom: 70px;
|
||||
display: flex;
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
import "./ReviewSection.scss";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
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 calendar from "./icons/calendar.svg";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
Link,
|
||||
usePathname,
|
||||
useRouter,
|
||||
} from "@/shared/config/navigation";
|
||||
|
||||
interface IReviewsSectionProps {
|
||||
endpoint: string;
|
||||
@@ -18,6 +23,7 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
|
||||
id,
|
||||
}: IReviewsSectionProps) => {
|
||||
const [reviews, setReviews] = useState<IReviewList>();
|
||||
const pathname = usePathname();
|
||||
const session = useSession();
|
||||
const handleSubmit: React.MouseEventHandler<
|
||||
HTMLFormElement
|
||||
@@ -33,6 +39,10 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
|
||||
},
|
||||
};
|
||||
|
||||
if (session.status === "unauthenticated") {
|
||||
signIn(undefined, { callbackUrl: pathname });
|
||||
}
|
||||
|
||||
if (!formData.get("review")) {
|
||||
return;
|
||||
}
|
||||
@@ -53,7 +63,7 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
|
||||
|
||||
const getReviews = async () => {
|
||||
const response = await apiInstance.get<IReviewList>(
|
||||
`/${endpoint}/${id}/reviews/`
|
||||
`/${endpoint}/${id}/reviews/?page_size=8`
|
||||
);
|
||||
|
||||
setReviews(response.data);
|
||||
@@ -83,47 +93,66 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
|
||||
<span id="blue-point" /> Написать комментарий
|
||||
</h3>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<textarea name="review" />
|
||||
<button type="submit">Отправить</button>
|
||||
</form>
|
||||
{session.status === "authenticated" ? (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<textarea name="review" />
|
||||
<button type="submit">Отправить</button>
|
||||
</form>
|
||||
) : (
|
||||
<p className="review-section__auth-warning">
|
||||
Перед тем как оставить комментарий, пожалуйста
|
||||
<button
|
||||
onClick={() =>
|
||||
signIn(undefined, { callbackUrl: pathname })
|
||||
}
|
||||
>
|
||||
войдите в аккаунт
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="review-section__list">
|
||||
<h3>
|
||||
<span id="blue-point" /> Комментарии
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
{reviews?.results.map((review) => (
|
||||
<li key={review.id} className="review">
|
||||
<div className="review__author">
|
||||
<img
|
||||
id="author-img"
|
||||
src={review.author.image}
|
||||
alt="Author Image"
|
||||
/>
|
||||
<div className="review__header">
|
||||
<h5 className="review__author-name">
|
||||
{review.author.first_name}{" "}
|
||||
{review.author.last_name}
|
||||
</h5>
|
||||
<div className="review__date">
|
||||
<Image src={calendar} alt="Calendar Icon" />
|
||||
<p>
|
||||
{months[review.created_at.slice(5, 7)]}{" "}
|
||||
{review.created_at.slice(5, 7).slice(0, 1) ===
|
||||
"0"
|
||||
? review.created_at.slice(6, 7)
|
||||
: review.created_at.slice(5, 7)}
|
||||
, {review.created_at.slice(0, 4)}
|
||||
</p>
|
||||
{reviews?.results.length !== 0 ? (
|
||||
<ul>
|
||||
{reviews?.results.map((review) => (
|
||||
<li key={review.id} className="review">
|
||||
<div className="review__author">
|
||||
<img
|
||||
id="author-img"
|
||||
src={review.author.image}
|
||||
alt="Author Image"
|
||||
/>
|
||||
<div className="review__header">
|
||||
<h5 className="review__author-name">
|
||||
{review.author.first_name}{" "}
|
||||
{review.author.last_name}
|
||||
</h5>
|
||||
<div className="review__date">
|
||||
<Image src={calendar} alt="Calendar Icon" />
|
||||
<p>
|
||||
{months[review.created_at.slice(5, 7)]}{" "}
|
||||
{review.created_at.slice(5, 7).slice(0, 1) ===
|
||||
"0"
|
||||
? review.created_at.slice(6, 7)
|
||||
: review.created_at.slice(5, 7)}
|
||||
, {review.created_at.slice(0, 4)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="review__description">{review.review}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="review__description">{review.review}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="review-section__warning">
|
||||
Оставьте комментарий первым :)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
File renamed without changes.
+2
-2
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import "./ConfirmEmailForm.scss";
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface IConfirmEmailFormProps {
|
||||
email: string;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -5,8 +5,8 @@ import "./confirm-code.scss";
|
||||
import { useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ITokens } from "@/shared/types/token-type";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface IConfirmCodeProps {
|
||||
setChangeForm: (boolean: boolean) => void;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-4
@@ -3,16 +3,14 @@
|
||||
import "./ProfileForm.scss";
|
||||
import Image from "next/image";
|
||||
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 { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { apiInstance, authInstanse } from "@/shared/config/apiConfig";
|
||||
import { AxiosError } from "axios";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import LogoutButton from "@/features/LogoutButton/LogoutButton";
|
||||
import ChangePassword from "./ChangePassword/ChangePassword";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface IProfileFormProps {
|
||||
id: number;
|
||||
File renamed without changes.
Whitespace-only changes.
File renamed without changes.
File renamed without changes.
@@ -19,8 +19,8 @@ import axios, { AxiosError } from "axios";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { IDisplayMap } from "@/shared/types/map-type";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface ILatLng {
|
||||
lat: number;
|
||||
File renamed without changes.
Loaded 100 of 172 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user