diff --git a/.gitignore b/.gitignore index 2bbe866d3..a162868f3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ BackEnd/celerybeat-schedule.* BackEnd/public/* !BackEnd/public/media/.gitkeep -!BackEnd/public/static/.gitkeep +!BackEnd/public/static/.gitkeep \ No newline at end of file diff --git a/BackEnd/administration/serializers.py b/BackEnd/administration/serializers.py index 98ef080c6..24cda9beb 100644 --- a/BackEnd/administration/serializers.py +++ b/BackEnd/administration/serializers.py @@ -1,5 +1,6 @@ from django.contrib.auth import get_user_model from rest_framework import serializers +from utils.administration.feedback_category import FeedbackCategory from authentication.models import CustomUser from profiles.models import ( Profile, @@ -192,3 +193,23 @@ class ModerationEmailSerializer(serializers.ModelSerializer): class Meta: model = ModerationEmail fields = ["email_moderation"] + + +class FeedbackSerializer(serializers.Serializer): + email = serializers.EmailField( + required=True, + error_messages={"required": "Please provide a valid email address."}, + ) + message = serializers.CharField( + min_length=10, + required=True, + error_messages={ + "required": "Message cannot be empty.", + "min_length": "Message must be at least 10 characters long.", + }, + ) + category = serializers.ChoiceField( + choices=FeedbackCategory.choices(), + required=True, + error_messages={"required": "Please select a category."}, + ) diff --git a/BackEnd/administration/templates/administration/admin_feedback_template.html b/BackEnd/administration/templates/administration/admin_feedback_template.html new file mode 100644 index 000000000..002256b55 --- /dev/null +++ b/BackEnd/administration/templates/administration/admin_feedback_template.html @@ -0,0 +1,41 @@ + + + + + + Нове повідомлення + + + +
+ CRAFTMERGE + +

Нове повідомлення від {{ user_email }}

+ +

Категорія: {{ category }}

+

Повідомлення:

+

{{ message }}

+ + +
+ + diff --git a/BackEnd/administration/templates/administration/user_feedback_template.html b/BackEnd/administration/templates/administration/user_feedback_template.html new file mode 100644 index 000000000..df486d219 --- /dev/null +++ b/BackEnd/administration/templates/administration/user_feedback_template.html @@ -0,0 +1,41 @@ + + + + + + Копія вашого повідомлення + + + +
+ CRAFTMERGE + +

Дякуємо за ваше повідомлення!

+ +

Ваша категорія: {{ category }}

+

Ваше повідомлення:

+

{{ message }}

+ + +
+ + diff --git a/BackEnd/administration/urls.py b/BackEnd/administration/urls.py index 7a699a1f9..d860830c3 100644 --- a/BackEnd/administration/urls.py +++ b/BackEnd/administration/urls.py @@ -8,6 +8,7 @@ UserDetailView, AutoModerationHoursView, ModerationEmailView, + FeedbackView, CreateAdminUserView, ) @@ -25,5 +26,6 @@ ), path("email/", ModerationEmailView.as_view(), name="moderation-email"), path("contacts/", ContactsView.as_view(), name="contacts"), + path("feedback/", FeedbackView.as_view(), name="feedback"), path("admin_create/", CreateAdminUserView.as_view(), name="admin-create"), ] diff --git a/BackEnd/administration/views.py b/BackEnd/administration/views.py index 3d100d5f6..aaf4acb3f 100644 --- a/BackEnd/administration/views.py +++ b/BackEnd/administration/views.py @@ -28,6 +28,11 @@ from authentication.models import CustomUser from profiles.models import Profile from .permissions import IsStaffUser, IsStaffUserOrReadOnly, IsSuperUser +from .serializers import FeedbackSerializer +from rest_framework.response import Response +from rest_framework import status +from rest_framework.views import APIView +from utils.administration.send_email_feedback import send_email_feedback from django_filters.rest_framework import DjangoFilterBackend from .filters import UsersFilter @@ -176,3 +181,26 @@ class CreateAdminUserView(CreateAPIView): IsSuperUser, ] serializer_class = AdminRegistrationSerializer + + +class FeedbackView(CreateAPIView): + serializer_class = FeedbackSerializer + + def perform_create(self, serializer): + """ + Performs the creation of a new feedback record and sends an email notification. + + Parameters: + - serializer (FeedbackSerializer): The serializer instance containing validated data. + + Returns: + None + + This method extracts the email, message, and category from the validated data in the serializer. + It then calls the `send_email_feedback` function to send an email notification with the provided feedback details. + """ + email = serializer.validated_data["email"] + message = serializer.validated_data["message"] + category = serializer.validated_data["category"] + + send_email_feedback(email, message, category) diff --git a/BackEnd/utils/administration/feedback_category.py b/BackEnd/utils/administration/feedback_category.py new file mode 100644 index 000000000..d83e0b7dd --- /dev/null +++ b/BackEnd/utils/administration/feedback_category.py @@ -0,0 +1,12 @@ +from enum import Enum + + +class FeedbackCategory(Enum): + TECHNICAL = "Технічне питання" + RECOMMENDATION = "Рекомендації" + QUESTION = "Питання" + OTHER = "Інше" + + @classmethod + def choices(cls): + return [(category.value, category.value) for category in cls] diff --git a/BackEnd/utils/administration/send_email_feedback.py b/BackEnd/utils/administration/send_email_feedback.py new file mode 100644 index 000000000..86da10f1a --- /dev/null +++ b/BackEnd/utils/administration/send_email_feedback.py @@ -0,0 +1,42 @@ +from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string +from django.conf import settings + +from utils.images.send_email import set_admin_email + +EMAIL_CONTENT_SUBTYPE = "html" + + +def send_email_feedback(user_email, message, category): + """Function for sending feedback letters to the administrator and the user.""" + context = { + "category": category, + "message": message, + "user_email": user_email, + } + + admin_email = set_admin_email() + + email_body_admin = render_to_string( + "administration/admin_feedback_template.html", context + ) + email_admin = EmailMultiAlternatives( + subject=f"Нове повідомлення: {category}", + body=email_body_admin, + from_email=settings.EMAIL_HOST_USER, + to=[admin_email], + ) + email_admin.content_subtype = EMAIL_CONTENT_SUBTYPE + email_admin.send(fail_silently=False) + + email_body_user = render_to_string( + "administration/user_feedback_template.html", context + ) + email_user = EmailMultiAlternatives( + subject="Копія вашого повідомлення", + body=email_body_user, + from_email=settings.EMAIL_HOST_USER, + to=[user_email], + ) + email_user.content_subtype = EMAIL_CONTENT_SUBTYPE + email_user.send(fail_silently=False) diff --git a/FrontEnd/src/components/Contact/Contact.jsx b/FrontEnd/src/components/Contact/Contact.jsx index 3eba2038b..0852a1fcc 100644 --- a/FrontEnd/src/components/Contact/Contact.jsx +++ b/FrontEnd/src/components/Contact/Contact.jsx @@ -1,28 +1,202 @@ -import React from 'react'; -import renderContent from '../../pages/CookiesPolicyPage/RenderContent.jsx'; +import React, { useState, useEffect, useRef } from 'react'; +import axios from 'axios'; import LinkContainer from '../../pages/CookiesPolicyPage/LinkContainer.jsx'; -import styles from './Contact.module.css'; import contactText from './text'; -import TEXT_CONTENT from './text'; import useScrollToTop from '../../hooks/useScrollToTop'; +import { + EMAIL_PATTERN, + MESSAGE_PATTERN +} from '../../constants/constants'; +import PropTypes from 'prop-types'; +import DropDownMenu from '../MiniComponents/DropDownMenu/DropDownMenu.jsx'; +import styles from './Contact.module.css'; +import { Spin } from 'antd'; const Contact = () => { - useScrollToTop(); - - return ( -
-
- - dots_12x10.png -
-
-

{contactText.title}

- {renderContent(TEXT_CONTENT)} -
-
- ); + useScrollToTop(); + + const [loading, setLoading] = useState(false); + const [email, setEmail] = useState(''); + const [message, setMessage] = useState('Привіт, хочу повідомити...'); + const [category, setCategory] = useState(null); + const [emailError, setEmailError] = useState(''); + const [messageError, setMessageError] = useState(''); + const [showModal, setShowModal] = useState(false); + const [percent, setPercent] = useState(-50); + const timerRef = useRef(); + + useEffect(() => { + timerRef.current = setTimeout(() => { + setPercent((v) => { + const nextPercent = v + 5; + return nextPercent > 150 ? -50 : nextPercent; + }); + }, 100); + return () => clearTimeout(timerRef.current); + }, [percent]); + + const categoryOptions = [ + { id: 'TECHNICAL', name: 'Технічне питання' }, + { id: 'RECOMMENDATION', name: 'Рекомендації' }, + { id: 'QUESTION', name: 'Питання' }, + { id: 'OTHER', name: 'Інше' }, + ]; + + const handleEmailChange = (e) => { + const { value } = e.target; + setEmail(value); + + if (!EMAIL_PATTERN.test(value)) { + setEmailError('Електронна пошта не відповідає вимогам'); + } else { + setEmailError(''); + } + }; + + const handleMessageChange = (e) => { + const { value } = e.target; + setMessage(value); + + if (!MESSAGE_PATTERN.test(value)) { + setMessageError('Повідомлення не може бути коротшим за 10 символів'); + } else { + setMessageError(''); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!EMAIL_PATTERN.test(email)) { + setEmailError('Електронна пошта не відповідає вимогам'); + return; + } + + if (!MESSAGE_PATTERN.test(message)) { + setMessageError('Повідомлення не може бути коротшим за 10 символів'); + return; + } + + setLoading(true); + try { + const response = await axios.post(`${process.env.REACT_APP_BASE_API_URL}/api/admin/feedback/`, { + email: email, + message: message, + category: category + }); + + if (response.status === 200) { + setShowModal(true); + setEmail(''); + setMessage('Привіт, хочу повідомити...'); + setCategory(null); + } + } catch (error) { + setShowModal(true); + } finally { + setLoading(false); + } + }; + + const handleCancel = () => { + setEmail(''); + setMessage('Привіт, хочу повідомити...'); + setCategory(null); + setEmailError(''); + setMessageError(''); + }; + + const handleRedirect = () => { + window.location.href = '/'; + }; + + const closeModal = () => { + setShowModal(false); + }; + + return ( +
+
+ + dots_12x10.png +
+
+

{contactText.title}

+
+ + + {emailError &&

{emailError}

} + setCategory(value[0] || null)} + options={categoryOptions} + updateHandler={(value) => setCategory(value[0] || null)} + className={styles['contact__select']} + placeholder="Оберіть категорію" + name="category" + label="Категорія:" + /> + +