release004
10
lib/next-auth.d.ts
vendored
@ -4,7 +4,13 @@ declare module "next-auth" {
|
||||
interface Session {
|
||||
refresh_token: string;
|
||||
access_token: string;
|
||||
expires_in: string;
|
||||
expires_in: Date;
|
||||
}
|
||||
|
||||
interface User {
|
||||
refresh_token: string;
|
||||
access_token: string;
|
||||
expires_in: Date;
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +20,6 @@ declare module "next-auth/jwt" {
|
||||
interface JWT {
|
||||
refresh_token: string;
|
||||
access_token: string;
|
||||
expires_in: string;
|
||||
expires_in: Date;
|
||||
}
|
||||
}
|
||||
|
192
messages/en.json
Normal file
@ -0,0 +1,192 @@
|
||||
{
|
||||
"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."
|
||||
},
|
||||
"disclaimer": {
|
||||
"text": "This website is funded by the European Union. Its contents are the sole responsibility of Transparency International Kyrgyzstan and do not necessarily reflect the views of the European Union."
|
||||
},
|
||||
"rights": {
|
||||
"text": "All rights reserved"
|
||||
}
|
||||
}
|
192
messages/kg.json
Normal file
@ -0,0 +1,192 @@
|
||||
{
|
||||
"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": "Четке калган иштеш коду."
|
||||
},
|
||||
"disclaimer": {
|
||||
"text": "Бул веб-сайт Европа Биримдиги тарабынан каржыланат. Анын мазмуну үчүн Трансперенси Интернешнл Кыргызстан гана жоопкерчиликтүү жана ал Европа Биримдигинин көз карашын сөзсүз түрдө чагылдырбайт."
|
||||
},
|
||||
"rights": {
|
||||
"text": "Бардык укуктар корголгон"
|
||||
}
|
||||
}
|
192
messages/ru.json
Normal file
@ -0,0 +1,192 @@
|
||||
{
|
||||
"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": "Неверный код."
|
||||
},
|
||||
"disclaimer": {
|
||||
"text": "Этот веб-сайт финансируется Европейским Союзом. Ответственность за его содержание лежит исключительно на Трансперенси Интернешнл Кыргызстан и не обязательно отражает точку зрения Европейского Союза."
|
||||
},
|
||||
"rights": {
|
||||
"text": "Все права защищены"
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -15,6 +15,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",
|
||||
|
@ -7,7 +7,7 @@
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
img {
|
||||
&__image {
|
||||
align-self: center;
|
||||
margin-bottom: 65px;
|
||||
border-radius: 12px;
|
||||
@ -36,6 +36,22 @@
|
||||
line-height: 34px;
|
||||
color: rgb(62, 50, 50);
|
||||
}
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
li {
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
line-height: 34px;
|
||||
color: rgb(62, 50, 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__author {
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 332 KiB After Width: | Height: | Size: 332 KiB |
109
src/app/[locale]/about-us/page.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import Typography from "@/shared/ui/components/Typography/Typography";
|
||||
import "./AboutUs.scss";
|
||||
import Image from "next/image";
|
||||
import header from "./assets/header.svg";
|
||||
import { Metadata } from "next";
|
||||
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { IMetatag } from "@/shared/types/metatag-type";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const data = await apiInstance
|
||||
.get<IMetatag[]>("/metatags/")
|
||||
.then((res) => res.data)
|
||||
.catch((e) => console.log(e));
|
||||
|
||||
if (!data)
|
||||
return {
|
||||
title: "KG ROAD | О нас",
|
||||
description:
|
||||
"Transparency International - Кыргызстан - филиал международной организации Transparency International в Кыргызской Республике.",
|
||||
openGraph: {
|
||||
title: "KG ROAD | О нас",
|
||||
description:
|
||||
"Transparency International - Кыргызстан - филиал международной организации Transparency International в Кыргызской Республике.",
|
||||
},
|
||||
};
|
||||
|
||||
const metadata = data.filter((tag) => tag.page === "about-us")[0];
|
||||
if (!metadata) {
|
||||
return {
|
||||
title: "KG ROAD | О нас",
|
||||
description:
|
||||
"Transparency International - Кыргызстан - филиал международной организации Transparency International в Кыргызской Республике.",
|
||||
openGraph: {
|
||||
title: "KG ROAD | О нас",
|
||||
description:
|
||||
"Transparency International - Кыргызстан - филиал международной организации Transparency International в Кыргызской Республике.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: `KG ROAD | ${metadata.title}`,
|
||||
description: metadata.description,
|
||||
keywords: metadata.keywords.split(","),
|
||||
openGraph: {
|
||||
title: `KG ROAD | ${metadata.title}`,
|
||||
description: metadata.description,
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const AboutUs = () => {
|
||||
return (
|
||||
<div className="about-us page-padding">
|
||||
<BreadCrumbs homeRequired />
|
||||
<Typography element="h2">О нас</Typography>
|
||||
|
||||
<Image
|
||||
className="about-us__image"
|
||||
src={header}
|
||||
alt="Header Image"
|
||||
/>
|
||||
<div className="about-us__descriptions">
|
||||
<h3>
|
||||
Transparency International-Кыргызстан - филиал международной
|
||||
организации Transparency International в Кыргызской
|
||||
Республике.
|
||||
</h3>
|
||||
<h3>Миссия ТИ-Кыргызстан</h3>
|
||||
<p>
|
||||
Продвижение эффективной общественной политики и надлежащего
|
||||
управления в целях предотвращения коррупции и усиления
|
||||
демократии в стране.{" "}
|
||||
</p>
|
||||
<h3>Цели и приоритеты ТИ-Кыргызстан:</h3>
|
||||
<ul>
|
||||
<li>
|
||||
- антикоррупционное просвещение населения, повышение
|
||||
общественного осознания значимости и важности борьбы с
|
||||
коррупцией в Кыргызстане;
|
||||
</li>
|
||||
<li>
|
||||
- организация изучения практики и теории борьбы с
|
||||
коррупцией и участия в ней структур гражданского общества
|
||||
в Кыргызстане и других странах;
|
||||
</li>
|
||||
<li>
|
||||
- содействие гражданам и организациям в реализации их
|
||||
конституционных прав и свобод;
|
||||
</li>
|
||||
<li>
|
||||
- преимущественная ориентация на международный опыт
|
||||
уменьшения коррупции, освоение его технологий, ресурсов, а
|
||||
также включение структур гражданского общества в
|
||||
международный диалог борьбы с коррупцией.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p className="about-us__author">
|
||||
Photo By Mzximvs VdB from Brussels, belgium - Road to
|
||||
Issyk-Kul (south shore), CC BY-SA 2.0
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutUs;
|
@ -2,15 +2,15 @@ import Typography from "@/shared/ui/components/Typography/Typography";
|
||||
import "./CreateReport.scss";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Metadata } from "next";
|
||||
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "KG ROAD | Написать обращение",
|
||||
description:
|
||||
"Написать обращение KG ROAD",
|
||||
description: "Написать обращение KG ROAD",
|
||||
};
|
||||
|
||||
const DynamicForm = dynamic(
|
||||
() => import("@/widgets/ReportForm/ReportForm"),
|
||||
() => import("@/widgets/forms/ReportForm/ReportForm"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
@ -19,6 +19,7 @@ const DynamicForm = dynamic(
|
||||
const CreateReport = () => {
|
||||
return (
|
||||
<div className="create-report page-padding">
|
||||
<BreadCrumbs homeRequired />
|
||||
<Typography element="h2">Написать обращение</Typography>
|
||||
|
||||
<DynamicForm />
|
33
src/app/[locale]/layout.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
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>
|
||||
);
|
||||
}
|
@ -1,21 +1,25 @@
|
||||
.news {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
|
||||
h2 {
|
||||
width: fit-content;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.news {
|
||||
gap: 30px;
|
||||
h2 {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.news {
|
||||
gap: 20px;
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
9
src/app/[locale]/news/[id]/error.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import NotFound from "@/widgets/NotFound/NotFound";
|
||||
|
||||
const error = () => {
|
||||
return <NotFound />;
|
||||
};
|
||||
|
||||
export default error;
|
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 |
@ -6,11 +6,33 @@ import Image from "next/image";
|
||||
import message from "./icons/message.svg";
|
||||
import calendar from "./icons/calendar.svg";
|
||||
import ReviewSection from "@/widgets/ReviewSection/ReviewSection";
|
||||
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string };
|
||||
}): Promise<Metadata> {
|
||||
const response = await apiInstance.get<INews>(
|
||||
`/news/${params.id}/`
|
||||
);
|
||||
|
||||
return {
|
||||
title: response.data.title,
|
||||
description: response.data.description,
|
||||
openGraph: {
|
||||
images: [response.data.image],
|
||||
type: "article",
|
||||
publishedTime: response.data.created_at,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const NewsDetails = async ({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string };
|
||||
params: { id: string; новость: string };
|
||||
}) => {
|
||||
const getNewsById = async () => {
|
||||
const response = await apiInstance.get<INews>(
|
||||
@ -39,6 +61,7 @@ const NewsDetails = async ({
|
||||
|
||||
return (
|
||||
<div className="news-details page-padding">
|
||||
<BreadCrumbs />
|
||||
<Typography element="h2">{data.title}</Typography>
|
||||
|
||||
<div className="news-details__img">
|
59
src/app/[locale]/news/page.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
|
||||
import "./News.scss";
|
||||
import Typography from "@/shared/ui/components/Typography/Typography";
|
||||
import NewsList from "@/widgets/NewsList/NewsList";
|
||||
import { Metadata } from "next";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { IMetatag } from "@/shared/types/metatag-type";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const data = await apiInstance
|
||||
.get<IMetatag[]>("/metatags/")
|
||||
.then((res) => res.data)
|
||||
.catch((e) => console.log(e));
|
||||
|
||||
if (!data)
|
||||
return {
|
||||
title: "KG ROAD | Новости",
|
||||
description: "Страница новостей KG ROAD",
|
||||
};
|
||||
|
||||
const metadata = data.filter((tag) => tag.page === "news")[0];
|
||||
if (!metadata) {
|
||||
return {
|
||||
title: "KG ROAD | Новости",
|
||||
description: "Страница новостей KG ROAD",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: `KG ROAD | ${metadata.title}`,
|
||||
description: metadata.description,
|
||||
keywords: metadata.keywords.split(","),
|
||||
openGraph: {
|
||||
title: `KG ROAD | ${metadata.title}`,
|
||||
description: metadata.description,
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const News = ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: {
|
||||
["страница-новостей"]: string;
|
||||
};
|
||||
}) => {
|
||||
return (
|
||||
<div className="news page-padding">
|
||||
<BreadCrumbs homeRequired />
|
||||
|
||||
<Typography element="h2">Новости</Typography>
|
||||
|
||||
<NewsList searchParams={searchParams} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default News;
|
9
src/app/[locale]/not-found.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import NotFound from "@/widgets/NotFound/NotFound";
|
||||
import "../globals.scss";
|
||||
import "@/shared/fonts/fonts.scss";
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return <NotFound />;
|
||||
}
|
78
src/app/[locale]/page.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
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";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { IMetatag } from "@/shared/types/metatag-type";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const data = await apiInstance
|
||||
.get<IMetatag[]>("/metatags/")
|
||||
.then((res) => res.data)
|
||||
.catch((e) => console.log(e));
|
||||
|
||||
if (!data)
|
||||
return {
|
||||
title: "KG ROAD | Главная",
|
||||
description:
|
||||
"Главная страница KG ROAD | Сделаем дороги безопасными!",
|
||||
openGraph: {
|
||||
title: "KG ROAD | Главная",
|
||||
description:
|
||||
"Главная страница KG ROAD | Сделаем дороги безопасными!",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
|
||||
const metadata = data.filter((tag) => tag.page === "home")[0];
|
||||
|
||||
if (!metadata) {
|
||||
return {
|
||||
title: "KG ROAD | Главная",
|
||||
description:
|
||||
"Главная страница KG ROAD | Сделаем дороги безопасными!",
|
||||
openGraph: {
|
||||
title: "KG ROAD | Главная",
|
||||
description:
|
||||
"Главная страница KG ROAD | Сделаем дороги безопасными!",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: `KG ROAD | ${metadata.title}`,
|
||||
description: metadata.description,
|
||||
keywords: metadata.keywords.split(","),
|
||||
openGraph: {
|
||||
title: `KG ROAD | ${metadata.title}`,
|
||||
description: metadata.description,
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const Home = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: {
|
||||
["тип-дороги"]: string;
|
||||
["поиск-на-карте"]: string;
|
||||
["поиск-рейтинг"]: string;
|
||||
["страница-рейтинга"]: string;
|
||||
};
|
||||
}) => {
|
||||
return (
|
||||
<div className="home">
|
||||
<Header />
|
||||
<StatisticsSection />
|
||||
<MapSection searchParams={searchParams} />
|
||||
<RatingSection searchParams={searchParams} />
|
||||
<NewsSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
@ -14,14 +14,10 @@ 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: "/",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
@ -6,13 +6,12 @@ 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";
|
||||
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "KG ROAD | Профиль",
|
||||
description:
|
||||
"Страница профиля KG ROAD",
|
||||
description: "Страница профиля KG ROAD",
|
||||
};
|
||||
|
||||
const Profile = async ({
|
||||
@ -44,6 +43,7 @@ const Profile = async ({
|
||||
|
||||
return (
|
||||
<div className="profile page-padding">
|
||||
<BreadCrumbs />
|
||||
<Typography element="h2">Личный кабинет</Typography>
|
||||
<ProfileNav report_count={data?.report_count as number} />
|
||||
|
@ -1,4 +1,4 @@
|
||||
import ProfileTable from "@/widgets/ProfileTable/ProfileTable";
|
||||
import ProfileTable from "@/widgets/tables/ProfileTable/ProfileTable";
|
||||
import React from "react";
|
||||
|
||||
const MyReports = async ({
|
13
src/app/[locale]/profile/page.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const Profile = () => {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
router.push("/profile/personal");
|
||||
}, []);
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default Profile;
|
@ -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";
|
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;
|
||||
}
|
||||
}
|
||||
}
|
9
src/app/[locale]/report/[id]/error.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import NotFound from "@/widgets/NotFound/NotFound";
|
||||
|
||||
const error = () => {
|
||||
return <NotFound />;
|
||||
};
|
||||
|
||||
export default error;
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
86
src/app/[locale]/report/[id]/page.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
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";
|
||||
import NotFound from "@/widgets/NotFound/NotFound";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
|
||||
const DynamicMap = dynamic(
|
||||
() => import("@/widgets/report-details/ReportMap/ReportMap"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string };
|
||||
}): Promise<Metadata> {
|
||||
const response = await apiInstance.get<IReport>(
|
||||
`/report/${params.id}/`
|
||||
);
|
||||
|
||||
return {
|
||||
title: `KG ROAD | ${response.data.location[0].address}`,
|
||||
description: response.data.description,
|
||||
openGraph: {
|
||||
title: `KG ROAD | ${response.data.location[0].address}`,
|
||||
description: response.data.description,
|
||||
images: [response.data.image[0].image],
|
||||
type: "article",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ReportDetails = async ({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string };
|
||||
}) => {
|
||||
const getReportDetails = async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_BASE_API}/report/${params.id}/`,
|
||||
{ cache: "no-store" }
|
||||
);
|
||||
|
||||
return res.json();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
const report: IReport = await getReportDetails();
|
||||
|
||||
return (
|
||||
<div className="report-details page-padding">
|
||||
<BreadCrumbs homeRequired />
|
||||
<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;
|
16
src/app/[locale]/sign-in/forgot-password/page.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
|
||||
import "@/shared/ui/auth-classes.scss";
|
||||
import ForgotPasswordForm from "@/widgets/forms/ForgotPasswordForm/ForgotPasswordForm";
|
||||
|
||||
const ForgotPassword = () => {
|
||||
return (
|
||||
<div className="page-padding">
|
||||
<BreadCrumbs />
|
||||
<div className="auth-page">
|
||||
<ForgotPasswordForm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPassword;
|
Before Width: | Height: | Size: 768 B After Width: | Height: | Size: 768 B |
@ -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 | Вход",
|
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 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 (
|
Before Width: | Height: | Size: 739 B After Width: | Height: | Size: 739 B |
38
src/app/[locale]/sign-up/confirm-email/page.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import "@/shared/ui/auth-classes.scss";
|
||||
import Image from "next/image";
|
||||
import mail from "./icons/mail.svg";
|
||||
import ConfirmEmailForm from "@/widgets/forms/ConfirmEmailForm/ConfirmEmailForm";
|
||||
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
|
||||
|
||||
const ConfirmEmail = ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: {
|
||||
email: string;
|
||||
};
|
||||
}) => {
|
||||
return (
|
||||
<div className="page-padding">
|
||||
<BreadCrumbs />
|
||||
<div className="auth-page">
|
||||
<div className="auth-wrapper">
|
||||
<div className="auth-icon2">
|
||||
<Image
|
||||
src={mail}
|
||||
alt="Mail icon"
|
||||
width={56}
|
||||
height={56}
|
||||
/>
|
||||
</div>
|
||||
<div className="auth-header">
|
||||
<h2>Проверьте свою почту</h2>
|
||||
<p>Мы отправили код на почту {searchParams.email}</p>
|
||||
</div>
|
||||
<ConfirmEmailForm email={searchParams.email} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmEmail;
|
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 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 | Регистрация",
|
@ -1,21 +1,25 @@
|
||||
.statistics {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
|
||||
h2 {
|
||||
width: fit-content;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.statistics {
|
||||
gap: 30px;
|
||||
h2 {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.statistics {
|
||||
gap: 20px;
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
56
src/app/[locale]/statistics/page.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import Typography from "@/shared/ui/components/Typography/Typography";
|
||||
import "./Statistics.scss";
|
||||
import StatisticsTable from "@/widgets/tables/StatisticsTable/StatisticsTable";
|
||||
import { Metadata } from "next";
|
||||
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
|
||||
import { IMetatag } from "@/shared/types/metatag-type";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const data = await apiInstance
|
||||
.get<IMetatag[]>("/metatags/")
|
||||
.then((res) => res.data)
|
||||
.catch((e) => console.log(e));
|
||||
|
||||
if (!data)
|
||||
return {
|
||||
title: "KG ROAD | Статистика",
|
||||
description: `Статистика по населенным пунктам Кыргызстана`,
|
||||
keywords: ["Бишкек", "Чуй", "Кыргызстан", "Дороги"],
|
||||
};
|
||||
|
||||
const metadata = data.filter((tag) => tag.page === "statistics")[0];
|
||||
if (!metadata) {
|
||||
return {
|
||||
title: "KG ROAD | Статистика",
|
||||
description: `Статистика по населенным пунктам Кыргызстана`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: `KG ROAD | ${metadata.title}`,
|
||||
description: metadata.description,
|
||||
keywords: metadata.keywords.split(","),
|
||||
openGraph: {
|
||||
title: `KG ROAD | ${metadata.title}`,
|
||||
description: metadata.description,
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const Statistics = ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { ["поиск-населенного-пункта"]: string };
|
||||
}) => {
|
||||
return (
|
||||
<div className="statistics page-padding">
|
||||
<BreadCrumbs homeRequired />
|
||||
<Typography element="h2">Статистика</Typography>
|
||||
<StatisticsTable searchParams={searchParams} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Statistics;
|
@ -1,21 +1,25 @@
|
||||
.volunteers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
|
||||
h2 {
|
||||
width: fit-content;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.volunteers {
|
||||
gap: 30px;
|
||||
h2 {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.volunteers {
|
||||
gap: 20px;
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
54
src/app/[locale]/volunteers/page.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import Typography from "@/shared/ui/components/Typography/Typography";
|
||||
import "./Volunteers.scss";
|
||||
import VolunteersTable from "@/widgets/tables/VolunteersTable/VolunteersTable";
|
||||
import { Metadata } from "next";
|
||||
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { IMetatag } from "@/shared/types/metatag-type";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const data = await apiInstance
|
||||
.get<IMetatag[]>("/metatags/")
|
||||
.then((res) => res.data)
|
||||
.catch((e) => console.log(e));
|
||||
|
||||
if (!data)
|
||||
return {
|
||||
title: "KG ROAD | Волонтеры",
|
||||
description:
|
||||
"Страница лучших волонтеров Кыргызской Республики!",
|
||||
};
|
||||
|
||||
const metadata = data.filter((tag) => tag.page === "volunteers")[0];
|
||||
if (!metadata) {
|
||||
return {
|
||||
title: "KG ROAD | Волонтеры",
|
||||
description:
|
||||
"Страница лучших волонтеров Кыргызской Республики!",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: `KG ROAD | ${metadata.title}`,
|
||||
description: metadata.description,
|
||||
keywords: metadata.keywords.split(","),
|
||||
openGraph: {
|
||||
title: `KG ROAD | ${metadata.title}`,
|
||||
description: metadata.description,
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const Volunteers = () => {
|
||||
return (
|
||||
<div className="volunteers page-padding">
|
||||
<BreadCrumbs homeRequired />
|
||||
|
||||
<Typography element="h2">Волонтеры</Typography>
|
||||
<VolunteersTable />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Volunteers;
|
@ -1,36 +0,0 @@
|
||||
import Typography from "@/shared/ui/components/Typography/Typography";
|
||||
import "./AboutUs.scss";
|
||||
import Image from "next/image";
|
||||
import header from "./assets/header.svg";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "KG ROAD | О нас",
|
||||
description:
|
||||
'Страница "О Нас" KG ROAD',
|
||||
};
|
||||
|
||||
const AboutUs = () => {
|
||||
return (
|
||||
<div className="about-us page-padding">
|
||||
<Typography element="h2">О нас</Typography>
|
||||
|
||||
<Image src={header} alt="Header Image" />
|
||||
<div className="about-us__descriptions">
|
||||
<p>Transparency International-Кыргызстан - филиал международной организации Transparency International в Кыргызской Республике.</p>
|
||||
<h4>Миссия ТИ-Кыргызстан</h4>
|
||||
<p>Продвижение эффективной общественной политики и надлежащего управления в целях предотвращения коррупции и усиления демократии в стране. </p>
|
||||
<h4>Цели и приоритеты ТИ-Кыргызстан:</h4>
|
||||
<ul>
|
||||
<li> - антикоррупционное просвещение населения, повышение общественного осознания значимости и важности борьбы с коррупцией в Кыргызстане;</li>
|
||||
<li> - организация изучения практики и теории борьбы с коррупцией и участия в ней структур гражданского общества в Кыргызстане и других странах;</li>
|
||||
<li> - содействие гражданам и организациям в реализации их конституционных прав и свобод;</li>
|
||||
<li> - преимущественная ориентация на международный опыт уменьшения коррупции, освоение его технологий, ресурсов, а также включение структур гражданского общества в международный диалог борьбы с коррупцией.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Photo By Mzximvs VdB from Brussels, belgium - Road to Issyk-Kul (south shore), CC BY-SA 2.0</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutUs;
|
@ -4,10 +4,7 @@
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans",
|
||||
"Helvetica Neue", sans-serif;
|
||||
// font-family: "Tilda Sans";
|
||||
font-family: "Tilda Sans";
|
||||
}
|
||||
|
||||
button,
|
||||
|
@ -1,13 +1,10 @@
|
||||
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 { GoogleAnalytics } from '@next/third-parties/google'
|
||||
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
/*
|
||||
<<<<<<< HEAD
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
@ -28,4 +25,8 @@ export default function RootLayout({
|
||||
{<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS || ""} />}
|
||||
</html>
|
||||
);
|
||||
=======*/
|
||||
export default function RootLayout({ children }: Props) {
|
||||
return children;
|
||||
//>>>>>>> ali
|
||||
}
|
||||
|
@ -1,30 +0,0 @@
|
||||
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";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "KG ROAD | Новости",
|
||||
description:
|
||||
"Страница новостей KG ROAD",
|
||||
};
|
||||
|
||||
const News = ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: {
|
||||
["страница-новостей"]: string;
|
||||
};
|
||||
}) => {
|
||||
return (
|
||||
<div className="news page-padding">
|
||||
<Typography element="h2">Новости</Typography>
|
||||
|
||||
<NewsList searchParams={searchParams} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default News;
|
15
src/app/not-found.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import NotFound from "@/widgets/NotFound/NotFound";
|
||||
import "./globals.scss";
|
||||
import "@/shared/fonts/fonts.scss";
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<NotFound />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
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 { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "KG ROAD | Главная",
|
||||
description:
|
||||
"Главная страница KG ROAD",
|
||||
};
|
||||
|
||||
const Home = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: {
|
||||
["тип-дороги"]: string;
|
||||
["поиск-на-карте"]: string;
|
||||
["поиск-рейтинг"]: string;
|
||||
["страница-рейтинга"]: string;
|
||||
};
|
||||
}) => {
|
||||
return (
|
||||
<div className="home">
|
||||
<Header />
|
||||
<StatisticsSection />
|
||||
<MapSection searchParams={searchParams} />
|
||||
<RatingSection searchParams={searchParams} />
|
||||
<NewsSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <div>page</div>;
|
||||
};
|
||||
|
||||
export default page;
|
@ -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:
|
||||
"Страница обращения KG ROAD",
|
||||
};
|
||||
|
||||
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;
|
@ -1,12 +0,0 @@
|
||||
import "@/shared/ui/auth-classes.scss";
|
||||
import ForgotPasswordForm from "@/widgets/ForgotPasswordForm/ForgotPasswordForm";
|
||||
|
||||
const ForgotPassword = () => {
|
||||
return (
|
||||
<div className="auth-page">
|
||||
<ForgotPasswordForm />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPassword;
|
@ -1,29 +0,0 @@
|
||||
import "@/shared/ui/auth-classes.scss";
|
||||
import Image from "next/image";
|
||||
import mail from "./icons/mail.svg";
|
||||
import ConfirmEmailForm from "@/widgets/ConfirmEmailForm/ConfirmEmailForm";
|
||||
|
||||
const ConfirmEmail = ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: {
|
||||
email: string;
|
||||
};
|
||||
}) => {
|
||||
return (
|
||||
<div className="auth-page">
|
||||
<div className="auth-wrapper">
|
||||
<div className="auth-icon2">
|
||||
<Image src={mail} alt="Mail icon" width={56} height={56} />
|
||||
</div>
|
||||
<div className="auth-header">
|
||||
<h2>Проверьте свою почту</h2>
|
||||
<p>Мы отправили код на почту {searchParams.email}</p>
|
||||
</div>
|
||||
<ConfirmEmailForm email={searchParams.email} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmEmail;
|
@ -1,28 +0,0 @@
|
||||
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 { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "KG ROAD | Статистика",
|
||||
description:
|
||||
"Страница статистики KG ROAD",
|
||||
};
|
||||
|
||||
const Statistics = ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { ["поиск-населенного-пункта"]: string };
|
||||
}) => {
|
||||
return (
|
||||
<div className="statistics page-padding">
|
||||
<Typography element="h2">Статистика</Typography>
|
||||
<StatisticsTable searchParams={searchParams} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Statistics;
|
@ -1,21 +0,0 @@
|
||||
import Typography from "@/shared/ui/components/Typography/Typography";
|
||||
import "./Volunteers.scss";
|
||||
import VolunteersTable from "@/widgets/VolunteersTable/VolunteersTable";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "KG ROAD | Волонтеры",
|
||||
description:
|
||||
"Страница волонтеров KG ROAD",
|
||||
};
|
||||
|
||||
const Volunteers = () => {
|
||||
return (
|
||||
<div className="volunteers page-padding">
|
||||
<Typography element="h2">Волонтеры</Typography>
|
||||
<VolunteersTable />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Volunteers;
|
@ -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)}...`;
|
||||
@ -56,8 +58,14 @@ const NewsCard: React.FC<INewsCard> = ({
|
||||
<p>{sliceDescription(description)}</p>
|
||||
</div>
|
||||
|
||||
<Link href={`/news/${id}`} className="news-card__more-btn">
|
||||
Подробнее
|
||||
<Link
|
||||
href={{
|
||||
pathname: `/news/${id}`,
|
||||
query: { новость: title },
|
||||
}}
|
||||
className="news-card__more-btn"
|
||||
>
|
||||
{t("details")}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
29
src/features/BreadCrumbs/BreadCrumbs.scss
Normal file
@ -0,0 +1,29 @@
|
||||
.breadcrumbs {
|
||||
margin-bottom: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
span,
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
a {
|
||||
gap: 8px;
|
||||
color: rgb(72, 159, 225);
|
||||
|
||||
img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: rgb(50, 48, 58);
|
||||
}
|
||||
}
|
67
src/features/BreadCrumbs/BreadCrumbs.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { Link, usePathname } from "@/shared/config/navigation";
|
||||
import "./BreadCrumbs.scss";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Image from "next/image";
|
||||
import chevron from "./icons/chevron-right.svg";
|
||||
|
||||
interface IBreadcrumbsProps {
|
||||
homeRequired?: boolean;
|
||||
}
|
||||
|
||||
const BreadCrumbs: React.FC<IBreadcrumbsProps> = ({
|
||||
homeRequired,
|
||||
}: IBreadcrumbsProps) => {
|
||||
const pathname = usePathname();
|
||||
const query = useSearchParams().get("новость");
|
||||
const routes = pathname.split("/").filter((route) => route !== "");
|
||||
const tRoutes: Record<string, string> = {
|
||||
"about-us": "О нас",
|
||||
"create-report": "Написать обращение",
|
||||
news: "Новости",
|
||||
profile: "Профиль",
|
||||
"my-reports": "Мои обращения",
|
||||
personal: "Личные данные",
|
||||
report: "Обращение",
|
||||
"sign-in": "Войти в аккаунт",
|
||||
"forgot-password": "Восстановление пароля",
|
||||
"reset-code": "Сброс пароля",
|
||||
"sign-up": "Регистрация",
|
||||
"confirm-email": "Потверждение почты",
|
||||
statistics: "Статистика",
|
||||
volunteers: "Волонтеры",
|
||||
};
|
||||
return (
|
||||
<div className="breadcrumbs">
|
||||
{homeRequired && (
|
||||
<Link href="/">
|
||||
Главная <Image src={chevron} alt="Chevron Right Icon" />
|
||||
</Link>
|
||||
)}
|
||||
{routes.map((route, i, array) => {
|
||||
if (routes.length === 1 && routes[0] === "profile")
|
||||
return null;
|
||||
if (parseInt(route)) {
|
||||
if (routes[0] === "news") {
|
||||
return <span key={query}>{query}</span>;
|
||||
}
|
||||
}
|
||||
if (i === array.length - 1) {
|
||||
return <span key={route}>{tRoutes[route]}</span>;
|
||||
} else {
|
||||
return route === "report" ? (
|
||||
<span key={route}>{tRoutes[route]}</span>
|
||||
) : (
|
||||
<Link key={route} href={`/${route}`}>
|
||||
{tRoutes[route]}
|
||||
<Image src={chevron} alt="Chevron Right Icon" />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BreadCrumbs;
|
14
src/features/BreadCrumbs/icons/chevron-right.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<svg width="16.000000" height="16.000000" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<desc>
|
||||
Created with Pixso.
|
||||
</desc>
|
||||
<defs>
|
||||
<clipPath id="clip7_52716">
|
||||
<rect id="chevron-right" width="16.000000" height="16.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="chevron-right" width="16.000000" height="16.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<g clip-path="url(#clip7_52716)">
|
||||
<path id="Vector" d="M6 4L10 8L6 12" stroke="#667085" stroke-opacity="1.000000" stroke-width="1.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 651 B |
@ -1,10 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import "./GoogleButton.scss";
|
||||
import google from "./icons/google.svg";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { signIn } from "next-auth/react";
|
||||
|
||||
const GoogleButton = () => {
|
||||
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>
|
||||
|
@ -11,11 +11,7 @@
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
&__change-btn {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
@ -34,4 +30,126 @@
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
&__modal {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
position: relative;
|
||||
max-width: 400px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0px 16px 24px 0px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&__text {
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
color: rgb(51, 65, 85);
|
||||
}
|
||||
|
||||
button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgb(226, 232, 240);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
width: 90%;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: rgb(100, 116, 139);
|
||||
}
|
||||
}
|
||||
|
||||
&__user-img {
|
||||
margin: 24px 0;
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
button,
|
||||
label {
|
||||
min-width: 110px;
|
||||
padding: 8px 16px;
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__blue-btn {
|
||||
color: white;
|
||||
background-color: rgb(72, 159, 225);
|
||||
}
|
||||
|
||||
&__gray-btn {
|
||||
color: rgb(51, 65, 85);
|
||||
border: 1px solid rgb(226, 232, 240);
|
||||
background: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
&__message {
|
||||
position: absolute;
|
||||
bottom: 32px;
|
||||
padding: 8px;
|
||||
width: 302px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: 3px;
|
||||
background-color: rgb(0, 0, 0);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
color: rgb(255, 255, 255);
|
||||
|
||||
button {
|
||||
font-size: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,14 @@
|
||||
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 close from "./icons/close.svg";
|
||||
import close_white from "./icons/close-white.svg";
|
||||
import { authInstanse } from "@/shared/config/apiConfig";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useState } from "react";
|
||||
import { AxiosError } from "axios";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
|
||||
interface IProfileAvatarProps {
|
||||
img: string;
|
||||
@ -14,35 +19,109 @@ interface IProfileAvatarProps {
|
||||
const ProfileAvatar: React.FC<IProfileAvatarProps> = ({
|
||||
img,
|
||||
}: IProfileAvatarProps) => {
|
||||
const [modal, setModal] = useState<boolean>(false);
|
||||
const [display_image, setDisplayImage] = useState<File | string>(
|
||||
img
|
||||
);
|
||||
const [isDeleting, setIsDeleting] = useState<boolean>(false);
|
||||
const [message, setMessage] = useState<boolean>(false);
|
||||
const def =
|
||||
"https://api.kgroaduat.fishrungames.com/media/user_photo/default.webp";
|
||||
|
||||
const [success, setSuccess] = useState<string>("");
|
||||
const [loader, setLoader] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
const changeImage: React.ChangeEventHandler<
|
||||
HTMLInputElement
|
||||
> = async (e) => {
|
||||
const Authorization = `Bearer ${session.data?.access_token}`;
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization,
|
||||
},
|
||||
};
|
||||
const formData = new FormData();
|
||||
|
||||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (
|
||||
e
|
||||
) => {
|
||||
if (e.target.files) {
|
||||
const image = Array.from(e.target.files);
|
||||
formData.append("image", image[0]);
|
||||
setDisplayImage(e.target.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const changeImage = async () => {
|
||||
const formData = new FormData();
|
||||
|
||||
if (session.status === "unauthenticated") return;
|
||||
|
||||
if (
|
||||
typeof display_image === typeof "string" ||
|
||||
display_image === img
|
||||
)
|
||||
return;
|
||||
|
||||
formData.append("image", display_image);
|
||||
|
||||
try {
|
||||
const res = await apiInstance.patch(
|
||||
"/users/update_image/",
|
||||
formData,
|
||||
config
|
||||
);
|
||||
setLoader(true);
|
||||
const res = await authInstanse(
|
||||
session.data?.access_token as string
|
||||
).patch("/users/update_image/", formData);
|
||||
|
||||
setError("");
|
||||
setSuccess("Фото профиля обновлено");
|
||||
setMessage(true);
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
setTimeout(() => {
|
||||
setMessage(false);
|
||||
}, 3000);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
setSuccess("");
|
||||
setError(error.message);
|
||||
setMessage(true);
|
||||
setTimeout(() => {
|
||||
setMessage(false);
|
||||
}, 3000);
|
||||
}
|
||||
} finally {
|
||||
setLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
const returnDefaultImage = async () => {
|
||||
if (session.status === "unauthenticated") return;
|
||||
|
||||
try {
|
||||
setLoader(true);
|
||||
const res = await authInstanse(
|
||||
session.data?.access_token as string
|
||||
).patch("/users/delete_image/", {});
|
||||
|
||||
setError("");
|
||||
setSuccess("Фото профиля удалено");
|
||||
setMessage(true);
|
||||
setDisplayImage(def);
|
||||
router.refresh();
|
||||
|
||||
setTimeout(() => {
|
||||
setMessage(false);
|
||||
}, 3000);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
setSuccess("");
|
||||
setError(error.message);
|
||||
setMessage(true);
|
||||
setTimeout(() => {
|
||||
setMessage(false);
|
||||
}, 3000);
|
||||
}
|
||||
} finally {
|
||||
setLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
const imageIsString =
|
||||
typeof display_image === "string"
|
||||
? display_image
|
||||
: typeof display_image === "undefined"
|
||||
? ""
|
||||
: URL.createObjectURL(display_image as File);
|
||||
return (
|
||||
<div className="profile-avatar">
|
||||
<img
|
||||
@ -50,10 +129,123 @@ const ProfileAvatar: React.FC<IProfileAvatarProps> = ({
|
||||
src={img}
|
||||
alt="User Image"
|
||||
/>
|
||||
<label htmlFor="profile-image">
|
||||
<button
|
||||
onClick={() => setModal(true)}
|
||||
className="profile-avatar__change-btn"
|
||||
>
|
||||
<Image src={pen} alt="Pen Icon" />
|
||||
</button>
|
||||
|
||||
{modal && (
|
||||
<div
|
||||
onClick={() => setModal(false)}
|
||||
className="profile-avatar__modal"
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="profile-avatar__wrapper"
|
||||
>
|
||||
<div className="profile-avatar__text">
|
||||
<div>
|
||||
<h4>Фото профиля</h4>
|
||||
<button onClick={() => setModal(false)}>
|
||||
<Image src={close} alt="Close Icon" />
|
||||
</button>
|
||||
</div>
|
||||
<p>
|
||||
По фото профиля другие люди смогут вас узнавать, а вам
|
||||
будет проще определять, в какой аккаунт вы вошли.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<img
|
||||
className="profile-avatar__user-img"
|
||||
src={imageIsString}
|
||||
alt="User image"
|
||||
/>
|
||||
<div className="profile-avatar__btns">
|
||||
{img === def && display_image === def ? (
|
||||
<>
|
||||
<label
|
||||
className="profile-avatar__blue-btn"
|
||||
htmlFor="change-image"
|
||||
>
|
||||
Добавить фото профиля
|
||||
</label>
|
||||
<input onChange={changeImage} id="profile-image" type="file" />
|
||||
<input
|
||||
onChange={handleChange}
|
||||
id="change-image"
|
||||
type="file"
|
||||
/>
|
||||
</>
|
||||
) : isDeleting ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setIsDeleting(false)}
|
||||
className="profile-avatar__gray-btn"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
onClick={returnDefaultImage}
|
||||
disabled={loader}
|
||||
className="profile-avatar__blue-btn"
|
||||
>
|
||||
{loader ? <Loader /> : "Удалить"}
|
||||
</button>
|
||||
</>
|
||||
) : img === display_image || success !== "" ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setIsDeleting(true)}
|
||||
className="profile-avatar__gray-btn"
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
<label
|
||||
className="profile-avatar__blue-btn"
|
||||
htmlFor="change-image"
|
||||
>
|
||||
Сменить
|
||||
</label>
|
||||
<input
|
||||
onChange={handleChange}
|
||||
id="change-image"
|
||||
type="file"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setDisplayImage(img)}
|
||||
className="profile-avatar__gray-btn"
|
||||
>
|
||||
Назад
|
||||
</button>
|
||||
<button
|
||||
disabled={loader}
|
||||
onClick={changeImage}
|
||||
className="profile-avatar__blue-btn"
|
||||
>
|
||||
{loader ? <Loader /> : "Сохранить"}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className="profile-avatar__message">
|
||||
<p>
|
||||
{success} {error}
|
||||
</p>
|
||||
<button onClick={() => setMessage(false)}>
|
||||
<Image src={close_white} alt="Close Icon White" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
14
src/features/ProfileAvatar/icons/close-white.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<svg width="20.000000" height="20.000000" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<desc>
|
||||
Created with Pixso.
|
||||
</desc>
|
||||
<defs>
|
||||
<clipPath id="clip2704_56355">
|
||||
<rect id="icon" width="20.000000" height="20.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#clip2704_56355)">
|
||||
<path id="Vector" d="M14.375 5.625L5.625 14.375" stroke="#FFFFFF" stroke-opacity="1.000000" stroke-width="1.500000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path id="Vector" d="M5.625 5.625L14.375 14.375" stroke="#FFFFFF" stroke-opacity="1.000000" stroke-width="1.500000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 730 B |
14
src/features/ProfileAvatar/icons/close.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<svg width="12.000000" height="12.000000" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<desc>
|
||||
Created with Pixso.
|
||||
</desc>
|
||||
<defs>
|
||||
<clipPath id="clip4_25648">
|
||||
<rect id="close" width="12.000000" height="12.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#clip4_25648)">
|
||||
<path id="Vector" d="M8.625 3.375L3.375 8.625" stroke="#334155" stroke-opacity="1.000000" stroke-width="1.500000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path id="Vector" d="M3.375 3.375L8.625 8.625" stroke="#334155" stroke-opacity="1.000000" stroke-width="1.500000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 721 B |
@ -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>
|
||||
);
|
||||
};
|
||||
|
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 = {
|
||||
matcher: [
|
||||
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: ["/((?!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,32 @@ import axios from "axios";
|
||||
import { AuthOptions } from "next-auth";
|
||||
import { JWT } from "next-auth/jwt";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
|
||||
interface IToken {
|
||||
access: string;
|
||||
}
|
||||
import { apiInstance } from "./apiConfig";
|
||||
import { IRefresh, ITokens } from "../types/token-type";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
|
||||
const refreshToken = async (token: JWT): Promise<JWT> => {
|
||||
const UTC = new Date();
|
||||
|
||||
const data = {
|
||||
refresh: token.refresh_token,
|
||||
};
|
||||
|
||||
const response = await axios.post<IToken>(
|
||||
"https://api.kgroad.org/api/v1/token/refresh/",
|
||||
const response = await apiInstance.post<IRefresh>(
|
||||
"/users/refresh/",
|
||||
data
|
||||
);
|
||||
|
||||
const expirationTime = new Date(UTC.getTime() + 14 * 60000);
|
||||
expirationTime.setTime(
|
||||
expirationTime.getTime() +
|
||||
expirationTime.getTimezoneOffset() * 60 * 1000 * -1
|
||||
);
|
||||
|
||||
return {
|
||||
...token,
|
||||
access_token: response.data.access,
|
||||
expires_in: expirationTime,
|
||||
};
|
||||
};
|
||||
|
||||
@ -27,7 +35,6 @@ export const authConfig: AuthOptions = {
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
name: "Credentials",
|
||||
|
||||
credentials: {
|
||||
email: {
|
||||
label: "Email",
|
||||
@ -36,35 +43,47 @@ export const authConfig: AuthOptions = {
|
||||
},
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
|
||||
async authorize(credentials, req) {
|
||||
async authorize(credentials, req): Promise<any> {
|
||||
if (!credentials?.email || !credentials?.password)
|
||||
return null;
|
||||
|
||||
const { email, password } = credentials as any;
|
||||
|
||||
const res = await fetch(
|
||||
"https://api.kgroad.org/api/v1/users/login/",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
const { email, password } = credentials;
|
||||
const data = {
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
const res = await apiInstance.post<ITokens>(
|
||||
"/users/login/",
|
||||
data
|
||||
);
|
||||
|
||||
if (res.status.toString()[0] === "4") {
|
||||
return null;
|
||||
if ([200, 201].includes(res.status)) {
|
||||
const currentTime = new Date();
|
||||
const expirationTime = new Date(
|
||||
currentTime.getTime() + 14 * 60000
|
||||
);
|
||||
expirationTime.setTime(
|
||||
expirationTime.getTime() +
|
||||
expirationTime.getTimezoneOffset() * 60 * 1000 * -1
|
||||
);
|
||||
|
||||
const user = {
|
||||
refresh_token: res.data.refresh_token,
|
||||
access_token: res.data.access_token,
|
||||
expires_in: expirationTime,
|
||||
};
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
const user = await res.json();
|
||||
return user;
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
GoogleProvider({
|
||||
clientId: process.env.CLIENT_ID as string,
|
||||
clientSecret: process.env.CLIENT_SECRET as string,
|
||||
}),
|
||||
],
|
||||
pages: {
|
||||
signIn: "/sign-in",
|
||||
@ -73,15 +92,57 @@ export const authConfig: AuthOptions = {
|
||||
strategy: "jwt",
|
||||
},
|
||||
callbacks: {
|
||||
async signIn({ account, user }) {
|
||||
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;
|
||||
}
|
||||
|
||||
const currentTime = new Date();
|
||||
|
||||
const expirationTime = new Date(
|
||||
currentTime.getTime() + 15 * 60000
|
||||
);
|
||||
|
||||
user.access_token = res.data.access_token;
|
||||
user.refresh_token = res.data.refresh_token;
|
||||
user.expires_in = expirationTime;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
async jwt({ token, user }) {
|
||||
if (user) return { ...token, ...user };
|
||||
|
||||
return refreshToken(token);
|
||||
const UTC = new Date();
|
||||
const currentTime = new Date(UTC.getTime());
|
||||
currentTime.setTime(
|
||||
currentTime.getTime() +
|
||||
currentTime.getTimezoneOffset() * 60 * 1000 * -1
|
||||
);
|
||||
|
||||
const isValid =
|
||||
new Date(currentTime).getTime() <=
|
||||
new Date(token.expires_in).getTime();
|
||||
|
||||
if (isValid) return token;
|
||||
|
||||
return await refreshToken(token);
|
||||
},
|
||||
|
||||
async session({ token, session }) {
|
||||
session.access_token = token.access_token;
|
||||
session.refresh_token = token.refresh_token;
|
||||
session.expires_in = token.expires_in;
|
||||
|
||||
return session;
|
||||
},
|
||||
|
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 });
|
@ -3,5 +3,5 @@
|
||||
@import "./TildaSans-Medium/TildaSans-Medium.css";
|
||||
@import "./TildaSans-Semibold/TildaSans-Semibold.css";
|
||||
@import "./TildaSans-Bold/TildaSans-Bold.css";
|
||||
@import "./TildaSans-Extrabold/TildaSans-Extrabold.css";
|
||||
@import "./TildaSans-ExtraBold/TildaSans-ExtraBold.css";
|
||||
@import "./TildaSans-Black/TildaSans-Black.css";
|
||||
|
@ -1,5 +1,5 @@
|
||||
export interface IList {
|
||||
count: number | null;
|
||||
count: number;
|
||||
previous: string | null;
|
||||
next: string | null;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
export interface ILocation {
|
||||
id: 4;
|
||||
id: number;
|
||||
latitude: string;
|
||||
longitude: string;
|
||||
address: string;
|
||||
|
8
src/shared/types/metatag-type.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface IMetatag {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
keywords: string;
|
||||
og_image?: string;
|
||||
page: string;
|
||||
}
|
@ -15,3 +15,17 @@ export interface IReport {
|
||||
total_likes: number;
|
||||
count_reviews: number;
|
||||
}
|
||||
|
||||
export interface IRatingReport {
|
||||
created_at: string;
|
||||
category: null;
|
||||
author: {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
govern_status: null;
|
||||
};
|
||||
total_likes: number;
|
||||
count_reviews: number;
|
||||
address: string;
|
||||
}
|
||||
|
@ -12,3 +12,7 @@ export interface IReview {
|
||||
review: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface IReviewList extends IList {
|
||||
results: IReview[];
|
||||
}
|
||||
|
@ -3,3 +3,8 @@ export interface ITokens {
|
||||
access_token: string;
|
||||
expires_in: string;
|
||||
}
|
||||
|
||||
export interface IRefresh {
|
||||
access: string;
|
||||
expires_at: string;
|
||||
}
|
||||
|
@ -29,8 +29,8 @@
|
||||
}
|
||||
|
||||
.auth-icon2 {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
min-width: 60px;
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -90,7 +90,6 @@
|
||||
@media screen and (max-width: 550px) {
|
||||
.auth-page {
|
||||
padding: 48px 16px;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.auth-wrapper {
|
||||
|
@ -1,7 +1,16 @@
|
||||
import "./Loader.scss";
|
||||
|
||||
const Loader = () => {
|
||||
return <span className="loader"></span>;
|
||||
interface ILoader {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const Loader: React.FC<ILoader> = ({ color }: ILoader) => {
|
||||
return (
|
||||
<span
|
||||
className="loader"
|
||||
style={{ borderTop: `3px solid ${color}` }}
|
||||
></span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loader;
|
||||
|
@ -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" },
|
||||
// ];
|
||||
|
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> = {
|
||||
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)",
|
||||
|
@ -4,6 +4,19 @@
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
gap: 30px;
|
||||
background-color: rgb(15, 23, 42);
|
||||
|
||||
&__logo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
p {
|
||||
color: white;
|
||||
font-family: "Inter", sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
a,
|
||||
li,
|
||||
h4 {
|
||||
|
@ -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,18 +7,27 @@ 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";
|
||||
import NetKgTracker from "@/widgets/NetKgTracker/NetKgTracker";
|
||||
|
||||
const Footer = () => {
|
||||
const t = useTranslations("general");
|
||||
const tDisclaimer = useTranslations("disclaimer");
|
||||
const tRights = useTranslations("rights");
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div className="footer__logo">
|
||||
<Link href="/">
|
||||
<Image src={logo} alt="Logo" />
|
||||
</Link>
|
||||
<p>© {tRights("text")}</p>
|
||||
<p>{tDisclaimer("text")}</p>
|
||||
</div>
|
||||
<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}
|
||||
@ -31,11 +39,11 @@ const Footer = () => {
|
||||
</div>
|
||||
|
||||
<div className="footer__contacts">
|
||||
<h4>Контакты</h4>
|
||||
<h4>{t("contacts")}</h4>
|
||||
<ul>
|
||||
<li>admin@kgroad.org</li>
|
||||
<li>+9960312394038</li>
|
||||
{/*
|
||||
|
||||
<li>
|
||||
{[youtube, facebook, instagram].map((net, i) => (
|
||||
<Link key={i} href="#">
|
||||
@ -43,13 +51,12 @@ const Footer = () => {
|
||||
</Link>
|
||||
))}
|
||||
</li>
|
||||
*/}
|
||||
<li>Photo By ThomasG, CC BY-SA 3.0</li>
|
||||
</ul>
|
||||
<p className="text-white">Photo By ThomasG, CC BY-SA 3.0</p>
|
||||
</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="#">
|
||||
@ -58,7 +65,6 @@ const Footer = () => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
*/}
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
@ -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,8 +13,11 @@ 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();
|
||||
return (
|
||||
<>
|
||||
@ -27,7 +31,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
|
||||
: "lg"
|
||||
}`}
|
||||
>
|
||||
Профиль
|
||||
{t("profile")}
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
@ -39,7 +43,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}
|
||||
|
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 "./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}
|
||||
|
@ -11,6 +11,18 @@
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
|
||||
&__logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
&_last {
|
||||
min-width: 78px;
|
||||
max-width: 78px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__links {
|
||||
height: 40%;
|
||||
display: flex;
|
||||
@ -46,6 +58,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1220px) {
|
||||
.navbar {
|
||||
padding: 0 60px;
|
||||
|
||||
&__links {
|
||||
gap: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
.navbar {
|
||||
padding: 0 30px;
|
||||
@ -60,7 +82,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
@media screen and (max-width: 900px) {
|
||||
.navbar {
|
||||
height: 72px;
|
||||
|
||||
|
@ -1,31 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import "./Navbar.scss";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Image, { StaticImageData } from "next/image";
|
||||
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";
|
||||
import founded_ru from "./assets/founded-ru.png";
|
||||
import founded_en from "./assets/founded-en.png";
|
||||
import founded_kg from "./assets/founded-kg.png";
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
const Navbar = () => {
|
||||
const pathname = usePathname();
|
||||
const [openMenu, setOpenMenu] = useState<boolean>(false);
|
||||
const { locale } = useParams();
|
||||
|
||||
const FOUNDED: Record<string, StaticImageData> = {
|
||||
ru: founded_ru,
|
||||
kg: founded_kg,
|
||||
en: founded_en,
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="navbar">
|
||||
<div className="navbar__logo">
|
||||
<Link href="/">
|
||||
<Image src={logo} alt="Logo" />
|
||||
</Link>
|
||||
<Image
|
||||
className="navbar__logo_last"
|
||||
src={FOUNDED[locale as string]}
|
||||
alt="Founded by EU Image"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<nav className="navbar__links">
|
||||
{LINKS.map((link) => (
|
||||
{LINKS().map((link) => (
|
||||
<Link
|
||||
className={`navbar__link${
|
||||
pathname === link.pathname ? "_active" : ""
|
||||
|
BIN
src/widgets/Navbar/assets/founded-en.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
src/widgets/Navbar/assets/founded-kg.png
Normal file
After Width: | Height: | Size: 74 KiB |
BIN
src/widgets/Navbar/assets/founded-ru.png
Normal file
After Width: | Height: | Size: 27 KiB |