From 43ab8245cc980baa83fd07507cce0a05e35a62dd Mon Sep 17 00:00:00 2001 From: alex karpovich Date: Sat, 5 Oct 2024 21:08:39 +0300 Subject: [PATCH 01/31] feat: change axios requests --- frontend/src/index.css | 2 +- frontend/src/services/api/login.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index e30857f..d99730d 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -133,7 +133,7 @@ header .login-btn:hover { .login-netologiya, .login-fio { max-width: 320px; width: 100%; - margin: 24px auto; + margin: auto; display: flex; align-items: flex-start; justify-content: center; diff --git a/frontend/src/services/api/login.js b/frontend/src/services/api/login.js index 76929fe..b678ab8 100644 --- a/frontend/src/services/api/login.js +++ b/frontend/src/services/api/login.js @@ -8,7 +8,7 @@ export function getTokenFromLocalStorage() { export async function loginModeus(username, password) { try { - return await axios.post(`${BACKEND_URL}/api/modeus/auth`, {username, password}); + return await axios.post(`${BACKEND_URL}/api/netology/auth`, {username, password}); } catch (e) { return e.response; } From c7d3a30167cabab9095474c208f0cf4292c5f372 Mon Sep 17 00:00:00 2001 From: alex karpovich Date: Tue, 8 Oct 2024 11:15:52 +0300 Subject: [PATCH 02/31] fix: change page login --- .../src/components/Login/ModeusLoginForm.jsx | 124 ------------------ frontend/src/pages/LoginRoute.jsx | 123 ++++++++++++++++- frontend/src/services/api/login.js | 28 +++- 3 files changed, 144 insertions(+), 131 deletions(-) delete mode 100644 frontend/src/components/Login/ModeusLoginForm.jsx diff --git a/frontend/src/components/Login/ModeusLoginForm.jsx b/frontend/src/components/Login/ModeusLoginForm.jsx deleted file mode 100644 index 29160c6..0000000 --- a/frontend/src/components/Login/ModeusLoginForm.jsx +++ /dev/null @@ -1,124 +0,0 @@ -import { useState } from "react"; -import { loginModeus, searchModeus} from "../../services/api/login"; -import { useNavigate } from "react-router-dom"; // Импортируем useNavigate для навигации - -const ModeusLoginForm = () => { - const [email, setEmail] = useState(null); - const [password, setPassword] = useState(null); - const [fullName, setFullName] = useState(""); // Строка для поиска - const [searchResults, setSearchResults] = useState([]); // Результаты поиска - // const [selectedName, setSelectedName] = useState(""); // Выбранное имя - const [showSuggestions, setShowSuggestions] = useState(false); // Флаг показа списка - const [errorMessage, setErrorMessage] = useState(""); // Сообщение об ошибке - - const navigate = useNavigate(); // Инициализируем хук для навигации - - const onClickSearch = async (fullName) => { - console.log("Поиск ФИО:", fullName); - - let response = await searchModeus(fullName); - if (response.status !== 200) { - setErrorMessage("Неверное ФИО. Попробуйте еще раз."); - return; - } - console.log("Результаты поиска:", response.data); - setSearchResults(response.data); - setShowSuggestions(true); // Показываем список после поиска - setErrorMessage(""); // Очищаем ошибку при успешном поиске - }; - - /// Обработчик нажатия клавиши "Enter" - const handleKeyPress = (e) => { - if (e.key === "Enter") { - onClickSearch(fullName); // Выполнить поиск, если нажата клавиша Enter - } - }; - - // Обработчик выбора варианта из списка - const handleSelect = (name) => { - setFullName(name); // Устанавливаем выбранное имя - setShowSuggestions(false); // Скрываем список после выбора - }; - - const onClickLogin = async () => { - let response = await loginModeus(email, password); - - if (response.status !== 200) { - setErrorMessage("Неверный логин или пароль. Попробуйте еще раз."); // Устанавливаем текст ошибки - return; - } - console.log(response) - localStorage.setItem("token", response.data?.token); - setErrorMessage(""); // Очищаем ошибку при успешном логине - - // Перенаправление на страницу календаря - navigate("/calendar"); - window.location.reload(); // Обновляем страницу после навигации - }; - - return ( -
-

Мое расписание

- -
- - -
- setFullName(e.target.value)} // Обновляем строку поиска - onKeyPress={handleKeyPress} // Обработчик для нажатия клавиш - /> - - {/* Рендерим выпадающий список или сообщение об отсутствии результатов */} - {showSuggestions && ( -
    - {searchResults.length > 0 ? ( - searchResults.map((person, index) => ( -
  • handleSelect(person.fullName)}> - {person.fullName} {/* Отображаем имя */} -
  • - )) - ) : ( -
  • Нет такого имени
  • // Сообщение, если список пуст - )} -
- )} -
-
- - -
- -
- setEmail(e.target.value)} - /> - setPassword(e.target.value)} - /> -
- {/* Сообщение об ошибке */} - {errorMessage &&

{errorMessage}

} - - -
-
- ); -}; - -export default ModeusLoginForm; diff --git a/frontend/src/pages/LoginRoute.jsx b/frontend/src/pages/LoginRoute.jsx index 6b0f062..b2f57bf 100644 --- a/frontend/src/pages/LoginRoute.jsx +++ b/frontend/src/pages/LoginRoute.jsx @@ -1,11 +1,124 @@ -import ModeusLoginForm from "../components/Login/ModeusLoginForm" +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import {loginModeus, searchModeus} from "../services/api/login"; const LoginRoute = () => { - return ( -
- + const [email, setEmail] = useState(null); + const [password, setPassword] = useState(null); + const [fullName, setFullName] = useState(""); // Строка для поиска + const [searchResults, setSearchResults] = useState([]); // Результаты поиска + // const [selectedName, setSelectedName] = useState(""); // Выбранное имя + const [showSuggestions, setShowSuggestions] = useState(false); // Флаг показа списка + const [errorMessage, setErrorMessage] = useState(""); // Сообщение об ошибке + + const navigate = useNavigate(); // Инициализируем хук для навигации + + const onClickSearch = async (fullName) => { + console.log("Поиск ФИО:", fullName); + + let response = await searchModeus(fullName); + if (response.status !== 200) { + setErrorMessage("Неверное ФИО. Попробуйте еще раз."); + return; + } + console.log("Результаты поиска:", response.data); + setSearchResults(response.data); + setShowSuggestions(true); // Показываем список после поиска + setErrorMessage(""); // Очищаем ошибку при успешном поиске + }; + + /// Обработчик нажатия клавиши "Enter" + const handleKeyPress = (e) => { + if (e.key === "Enter") { + onClickSearch(fullName); // Выполнить поиск, если нажата клавиша Enter + } + }; + + // Обработчик выбора варианта из списка + const handleSelect = (name) => { + setFullName(name); // Устанавливаем выбранное имя + setShowSuggestions(false); // Скрываем список после выбора + }; + + const onClickLogin = async () => { + let response = await loginModeus(email, password); + + if (response.status !== 200) { + setErrorMessage("Неверный логин или пароль. Попробуйте еще раз."); // Устанавливаем текст ошибки + return; + } + console.log(response) + localStorage.setItem("token", response.data?.token); + setErrorMessage(""); // Очищаем ошибку при успешном логине + + // Перенаправление на страницу календаря + navigate("/calendar"); + window.location.reload(); // Обновляем страницу после навигации + }; + + return ( +
+

Мое расписание

+ +
+ + +
+ setFullName(e.target.value)} // Обновляем строку поиска + onKeyPress={handleKeyPress} // Обработчик для нажатия клавиш + /> + + {/* Рендерим выпадающий список или сообщение об отсутствии результатов */} + {showSuggestions && ( +
    + {searchResults.length > 0 ? ( + searchResults.map((person, index) => ( +
  • handleSelect(person.fullName)}> + {person.fullName} {/* Отображаем имя */} +
  • + )) + ) : ( +
  • Нет такого имени
  • // Сообщение, если список пуст + )} +
+ )} +
+
+ + +
+ +
+ setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> +
+ {/* Сообщение об ошибке */} + {errorMessage &&

{errorMessage}

} + +
- ) +
+ ); } export default LoginRoute diff --git a/frontend/src/services/api/login.js b/frontend/src/services/api/login.js index b678ab8..68f4326 100644 --- a/frontend/src/services/api/login.js +++ b/frontend/src/services/api/login.js @@ -5,7 +5,7 @@ import { BACKEND_URL } from '../../variables'; export function getTokenFromLocalStorage() { return localStorage.getItem('token') } - +// login export async function loginModeus(username, password) { try { return await axios.post(`${BACKEND_URL}/api/netology/auth`, {username, password}); @@ -23,4 +23,28 @@ export async function searchModeus(fullName) { } catch (e) { return e.response; } -} \ No newline at end of file +} + +// calendar +export async function bulkEvents(username, password, sessionToken, calendarId, timeMin, timeMax, attendeePersonId) { + try { + const response = await axios.post( + `${BACKEND_URL}/api/bulk/events/?calendar_id=${calendarId}`, + { + timeMin, + timeMax, + size: 50, + attendeePersonId: [attendeePersonId], + }, + { + headers: { + "_netology-on-rails_session": sessionToken, // Токен сессии + "Content-Type": "application/json", + }, + } + ); + return response; + } catch (e) { + return e.response; + } +} \ No newline at end of file From d7de6563320b3d44a8c5ae806f99e5506d6e9999 Mon Sep 17 00:00:00 2001 From: alex karpovich Date: Tue, 8 Oct 2024 20:47:53 +0300 Subject: [PATCH 03/31] feat: add context, getNetologyCourse, bulkEvents for calendar --- frontend/src/components/Calendar.jsx | 19 ------ frontend/src/components/DataPicker.jsx | 25 -------- frontend/src/context/AuthContext.js | 21 +++++++ frontend/src/index.js | 5 +- frontend/src/pages/CalendarRoute.jsx | 81 +++++++++++++++++++++++--- frontend/src/pages/LoginRoute.jsx | 37 +++++++++--- frontend/src/services/api/login.js | 42 ++++++++++++- 7 files changed, 169 insertions(+), 61 deletions(-) delete mode 100644 frontend/src/components/Calendar.jsx delete mode 100644 frontend/src/components/DataPicker.jsx create mode 100644 frontend/src/context/AuthContext.js diff --git a/frontend/src/components/Calendar.jsx b/frontend/src/components/Calendar.jsx deleted file mode 100644 index 23766e3..0000000 --- a/frontend/src/components/Calendar.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import flatpickr from "flatpickr"; -import { useCallback, useRef } from "react"; - -function Calendar() { - const fp1 = useRef(); - - const inputRef = useCallback((node) => { - if (node !== null) { - fp1.current = flatpickr(node, { - enableTime: true, - dateFormat: "Y-m-d H:i", - }); - } - }, []); - - return (); -} - -export default Calendar; diff --git a/frontend/src/components/DataPicker.jsx b/frontend/src/components/DataPicker.jsx deleted file mode 100644 index 95b68bc..0000000 --- a/frontend/src/components/DataPicker.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { useRef, useEffect } from 'react'; -import Flatpickr from 'flatpickr'; -import weekSelect from 'flatpickr'; -import 'flatpickr/dist/flatpickr.css'; - -const DatePicker = ({ value, onChange, options }) => { - const datePickerRef = useRef(null); - - - useEffect(() => { - if (datePickerRef.current) { - const fp = new Flatpickr(datePickerRef.current, { - - }); - } - }, []); - - return ( -
- -
- ); -}; - -export default DatePicker; diff --git a/frontend/src/context/AuthContext.js b/frontend/src/context/AuthContext.js new file mode 100644 index 0000000..b4b3ce2 --- /dev/null +++ b/frontend/src/context/AuthContext.js @@ -0,0 +1,21 @@ +// AuthContext.js +import React, { createContext, useState } from 'react'; + +// Создаем контекст +export const AuthContext = createContext(); + +// Создаем провайдер для использования контекста +export const AuthProvider = ({ children }) => { + const [authData, setAuthData] = useState({ + email: null, + password: null, + person: null, + personId: null, + }); + + return ( + + {children} + + ); +}; diff --git a/frontend/src/index.js b/frontend/src/index.js index 05ae907..c74f0e1 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -5,6 +5,7 @@ import ReactDOM from "react-dom/client"; import LoginRoute from "./pages/LoginRoute"; import CalendarRoute from "./pages/CalendarRoute"; +import { AuthProvider } from './context/AuthContext'; import "./index.css"; import PrivateRoute from "./components/Calendar/PrivateRoute"; @@ -38,7 +39,9 @@ const router = createBrowserRouter([ root.render( - + {/* Оборачиваем в AuthProvider для контекста */} + + ); diff --git a/frontend/src/pages/CalendarRoute.jsx b/frontend/src/pages/CalendarRoute.jsx index 1e2fa53..58b6b02 100644 --- a/frontend/src/pages/CalendarRoute.jsx +++ b/frontend/src/pages/CalendarRoute.jsx @@ -1,13 +1,80 @@ -import DataPicker from "../components/Calendar/DataPicker" -import Header from "../components/Header/Header"; +import React, {useContext, useEffect, useState} from "react"; +import { getNetologyCourse, bulkEvents } from "../services/api/login"; +import {AuthContext} from "../context/AuthContext"; // Импортируем API функции const CalendarRoute = () => { + const { authData } = useContext(AuthContext); // Достаем данные из контекста + + const [calendarId, setCalendarId] = useState(null); // Хранение calendarId + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + console.log("Данные пользователя:", authData); + + const fetchCourseAndEvents = async () => { + const sessionToken = localStorage.getItem("token"); // Получаем токен из localStorage + + try { + // Получаем данные курса, чтобы извлечь calendarId + const courseData = await getNetologyCourse(sessionToken); + const fetchedCalendarId = courseData?.id; // Предполагаем, что calendarId есть в данных курса + setCalendarId(fetchedCalendarId); + + if (fetchedCalendarId) { + // дату получаем события для календаря + const eventsResponse = await bulkEvents( + "authData.email", // username + "authData.password", // password + sessionToken, // Токен сессии + fetchedCalendarId, // Извлеченный ID календаря + "2024-10-07T00:00:00+03:00", // Начало диапазона дат + "2024-10-13T23:59:59+03:00", // Конец диапазона дат + "authData.personId" // ID участника + ); + + setEvents(eventsResponse.data); // Записываем события в состояние + } + } catch (error) { + setError("Ошибка получения данных с сервера"); + } finally { + setLoading(false); + } + }; + + fetchCourseAndEvents(); + }, [authData]); + + if (loading) { + return
Загрузка...
; + } + + if (error) { + return
{error}
; + } + return (
-
- +

Календарь

+ {calendarId ?

ID Календаря: {calendarId}

:

Календарь не найден

} + +

События:

+ {events.length > 0 ? ( +
    + {events.map((event, index) => ( +
  • + {event.title} — {event.date} +
  • + ))} +
+ ) : ( +

Нет доступных событий

+ )}
- ) -} + ); +}; + +export default CalendarRoute; + -export default CalendarRoute diff --git a/frontend/src/pages/LoginRoute.jsx b/frontend/src/pages/LoginRoute.jsx index b2f57bf..8fde5e2 100644 --- a/frontend/src/pages/LoginRoute.jsx +++ b/frontend/src/pages/LoginRoute.jsx @@ -1,13 +1,17 @@ -import { useState } from "react"; +import {useContext, useState} from "react"; import { useNavigate } from "react-router-dom"; +import {AuthContext} from "../context/AuthContext"; import {loginModeus, searchModeus} from "../services/api/login"; const LoginRoute = () => { + const { setAuthData } = useContext(AuthContext); // Достаем setAuthData из контекста + const [email, setEmail] = useState(null); const [password, setPassword] = useState(null); const [fullName, setFullName] = useState(""); // Строка для поиска const [searchResults, setSearchResults] = useState([]); // Результаты поиска // const [selectedName, setSelectedName] = useState(""); // Выбранное имя + const [personId, setPersonId] = useState(null); // ID выбранного человека const [showSuggestions, setShowSuggestions] = useState(false); // Флаг показа списка const [errorMessage, setErrorMessage] = useState(""); // Сообщение об ошибке @@ -35,10 +39,20 @@ const LoginRoute = () => { }; // Обработчик выбора варианта из списка - const handleSelect = (name) => { - setFullName(name); // Устанавливаем выбранное имя - setShowSuggestions(false); // Скрываем список после выбора - }; + const handleSelect = (person) => { + console.log('person', person) + setFullName(person.fullName); // Устанавливаем выбранное имя + setPersonId(person.personId); + + setAuthData((prev) => ({ + person: person, + personId: personId, // Сохраняем personId в контекст + ...prev, + })); + + + setShowSuggestions(false); // Скрываем список после выбора + }; const onClickLogin = async () => { let response = await loginModeus(email, password); @@ -47,13 +61,22 @@ const LoginRoute = () => { setErrorMessage("Неверный логин или пароль. Попробуйте еще раз."); // Устанавливаем текст ошибки return; } + + setAuthData((prev) => ({ + email, + password, + ...prev, + })); + console.log(response) localStorage.setItem("token", response.data?.token); setErrorMessage(""); // Очищаем ошибку при успешном логине + // email, password + // Перенаправление на страницу календаря navigate("/calendar"); - window.location.reload(); // Обновляем страницу после навигации + // window.location.reload(); // Обновляем страницу после навигации }; return ( @@ -79,7 +102,7 @@ const LoginRoute = () => {
    {searchResults.length > 0 ? ( searchResults.map((person, index) => ( -
  • handleSelect(person.fullName)}> +
  • handleSelect(person)}> {person.fullName} {/* Отображаем имя */}
  • )) diff --git a/frontend/src/services/api/login.js b/frontend/src/services/api/login.js index 68f4326..2f1184b 100644 --- a/frontend/src/services/api/login.js +++ b/frontend/src/services/api/login.js @@ -24,7 +24,20 @@ export async function searchModeus(fullName) { return e.response; } } - +// calendar_id +export async function getNetologyCourse(sessionToken) { + try { + const response = await axios.get(`${BACKEND_URL}/api/netology/course/`, { + headers: { + "_netology-on-rails_session": sessionToken, // Токен сессии передается в заголовке + "Content-Type": "application/json" + } + }); + return response.data; // Возвращаем данные + } catch (e) { + return e.response; + } +} // calendar export async function bulkEvents(username, password, sessionToken, calendarId, timeMin, timeMax, attendeePersonId) { try { @@ -47,4 +60,29 @@ export async function bulkEvents(username, password, sessionToken, calendarId, t } catch (e) { return e.response; } -} \ No newline at end of file +} +// Refresh calendar +export async function refreshBulkEvents(sessionToken, calendarId, timeMin, timeMax, attendeePersonId) { + try { + const response = await axios.post( + `${BACKEND_URL}/api/bulk/refresh_events/?calendar_id=${calendarId}`, + { + timeMin, + timeMax, + size: 50, + attendeePersonId: [attendeePersonId], + }, + { + headers: { + "_netology-on-rails_session": sessionToken, // Токен сессии + "Content-Type": "application/json", + }, + } + ); + return response; + } catch (e) { + return e.response; + } +} + + From 2cbb57eb238a4c661876c41e516edfec099c9379 Mon Sep 17 00:00:00 2001 From: alex karpovich Date: Tue, 8 Oct 2024 21:44:43 +0300 Subject: [PATCH 04/31] fix: fetchCourseAndEvents email, password --- frontend/src/pages/CalendarRoute.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/CalendarRoute.jsx b/frontend/src/pages/CalendarRoute.jsx index 58b6b02..fa0941d 100644 --- a/frontend/src/pages/CalendarRoute.jsx +++ b/frontend/src/pages/CalendarRoute.jsx @@ -25,13 +25,13 @@ const CalendarRoute = () => { if (fetchedCalendarId) { // дату получаем события для календаря const eventsResponse = await bulkEvents( - "authData.email", // username - "authData.password", // password + authData.email, // username + authData.password, // password sessionToken, // Токен сессии fetchedCalendarId, // Извлеченный ID календаря "2024-10-07T00:00:00+03:00", // Начало диапазона дат "2024-10-13T23:59:59+03:00", // Конец диапазона дат - "authData.personId" // ID участника + authData.personId // ID участника ); setEvents(eventsResponse.data); // Записываем события в состояние From 91ae9d4eef5dae964ec524b27693a85e69d4adbd Mon Sep 17 00:00:00 2001 From: KytakBR Date: Wed, 9 Oct 2024 01:16:37 +0500 Subject: [PATCH 05/31] styled: add calendar component --- frontend/package-lock.json | 66 +++ frontend/package.json | 4 + frontend/src/App.css | 38 -- frontend/src/App.scss | 29 ++ frontend/src/components/Calendar/Calendar.jsx | 2 +- .../src/components/Calendar/left-week.png | Bin 0 -> 226 bytes .../src/components/Calendar/right-week.png | Bin 0 -> 228 bytes frontend/src/components/Header/Header.js | 96 ++-- .../src/components/Header/Shape-reverse.png | Bin 0 -> 228 bytes frontend/src/components/Header/arrow.png | Bin 0 -> 224 bytes frontend/src/components/Header/camera.png | Bin 0 -> 193 bytes frontend/src/components/Header/cross.png | Bin 0 -> 244 bytes frontend/src/components/Header/left-week.png | Bin 0 -> 226 bytes .../src/components/Login/ModeusLoginForm.jsx | 123 ++--- frontend/src/index.css | 218 --------- frontend/src/index.js | 18 +- frontend/src/index.scss | 458 ++++++++++++++++++ 17 files changed, 689 insertions(+), 363 deletions(-) delete mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.scss create mode 100644 frontend/src/components/Calendar/left-week.png create mode 100644 frontend/src/components/Calendar/right-week.png create mode 100644 frontend/src/components/Header/Shape-reverse.png create mode 100644 frontend/src/components/Header/arrow.png create mode 100644 frontend/src/components/Header/camera.png create mode 100644 frontend/src/components/Header/cross.png create mode 100644 frontend/src/components/Header/left-week.png delete mode 100644 frontend/src/index.css create mode 100644 frontend/src/index.scss diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 224de35..0ef13d3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,6 +21,10 @@ "react-router-dom": "^6.26.2", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" + }, + "devDependencies": { + "sass": "^1.79.4", + "scss-reset": "^1.4.2" } }, "node_modules/@adobe/css-tools": { @@ -9579,6 +9583,13 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -15810,6 +15821,24 @@ "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==" }, + "node_modules/sass": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.4.tgz", + "integrity": "sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/sass-loader": { "version": "12.6.0", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", @@ -15847,6 +15876,36 @@ } } }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -15920,6 +15979,13 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, + "node_modules/scss-reset": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/scss-reset/-/scss-reset-1.4.2.tgz", + "integrity": "sha512-eXtSeI5APjD/TtaIlRdiMRapgsX5GCP4I1Ti3FiUzCSE4GEYnfT1hGISrJkKGZsZbCDhwZv1bUdOOZfPGs3R1A==", + "dev": true, + "license": "MIT" + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index a0f8521..5af75bd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,5 +40,9 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "sass": "^1.79.4", + "scss-reset": "^1.4.2" } } diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index 74b5e05..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/frontend/src/App.scss b/frontend/src/App.scss new file mode 100644 index 0000000..189cdee --- /dev/null +++ b/frontend/src/App.scss @@ -0,0 +1,29 @@ +.App { + text-align: center; + &-logo { + height: 40vmin; + pointer-events: none; + } + &-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; + } + &-link { + color: #61dafb; + } +} +media (prefers-reduced-motion: no-preference) { + animation: App-logo-spin infinite 20s linear; +} +keyframes App-logo-spin { + transform: rotate(0deg); +} +to { + transform: rotate(360deg); +} diff --git a/frontend/src/components/Calendar/Calendar.jsx b/frontend/src/components/Calendar/Calendar.jsx index a2c9be1..f5934ee 100644 --- a/frontend/src/components/Calendar/Calendar.jsx +++ b/frontend/src/components/Calendar/Calendar.jsx @@ -8,7 +8,7 @@ const Calendar = () => { if (node !== null) { fp1.current = flatpickr(node, { enableTime: true, - dateFormat: "Y-m-d H:i", + dateFormat: "j, F", }); } }, []); diff --git a/frontend/src/components/Calendar/left-week.png b/frontend/src/components/Calendar/left-week.png new file mode 100644 index 0000000000000000000000000000000000000000..58b6e88d1ab05afdba6b929a206f91424b1420b2 GIT binary patch literal 226 zcmeAS@N?(olHy`uVBq!ia0vp^Y(UJx0U~c5>$3n-oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBez~WMV@L&KZl57ng8`50$3n-oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBex;|2V@L&KYWG3jW&dHX6hiNO(h0m}UA&k3vqog$ z0fyeKU)1J!thDOdB0h0qivA&o*@Z9WAAR_}{nYE*rSoe{EJOx$0z V^Zu`k-UW07gQu&X%Q~loCIH +
    -
    - Мое расписание +
    + Мое расписание +
    -
    - +
    +
    + Дедлайн Нетология 23.09.2024 +
    +
    + Программирование на Python +
    +
    + Домашнее задание с самопроверкой(дедлайн 12.12.24) +
    +
    +
    - - - - - - - + + + + + + + - - - - - - - - + + + + + + + + - - + + @@ -50,8 +70,8 @@ export default function Header() { - - + + @@ -60,8 +80,8 @@ export default function Header() { - - + + @@ -70,8 +90,10 @@ export default function Header() { - - + + @@ -80,8 +102,10 @@ export default function Header() { - - + + @@ -90,8 +114,8 @@ export default function Header() { - - + + diff --git a/frontend/src/components/Header/Shape-reverse.png b/frontend/src/components/Header/Shape-reverse.png new file mode 100644 index 0000000000000000000000000000000000000000..0ae2959721bac5a2ec2cea952e249dbf5607b04a GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^Y(UJx0U~c5>$3n-oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBex;|2V@L&KYWG3jW&dHX6hiNO(h0m}UA&k3vqog$ z0fyeKU)1J!thDOdB0h0qivA&o*@Z9WAAR_}{nYE*rSoe{EJOx$0z V^Zu`k-UW07gQu&X%Q~loCIH;4~pEG*09DT5}X48qgmlD*D{5Y)Z@gynD_U=``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBewe3=V@L&KZSQH`1_c2ZN$19C45AYKb?c63 z%X)BbOLBdjbc37mUS`>6qvpetmpSifJX^R=NN#n1)G5~G=~Je;`uU_eOss2LG$U~F k)tS@8g|6NGc=ji=>j^1FVYQFvfHp9Ay85}Sb4q9e0QV(4EC2ui literal 0 HcmV?d00001 diff --git a/frontend/src/components/Header/cross.png b/frontend/src/components/Header/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..39835f3e7cfc66821657d21e86531798aab3614c GIT binary patch literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqoCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBey694V@L&~Z@(koVFdx^y(()f*xZ+Bb?85N z`jg2$@1g8krlYq5HZGs~*0ecCan6g)5}TJ#;rcGIcz2fVj?>xP%NhJwS{1uYqMNfF zBG;dV-@NNv^f^Ld@KDXiYa laP8L2%m2^L_B{Aoai3IfXsC~BTPe^{44$rjF6*2UngF>>Rfzxq literal 0 HcmV?d00001 diff --git a/frontend/src/components/Header/left-week.png b/frontend/src/components/Header/left-week.png new file mode 100644 index 0000000000000000000000000000000000000000..58b6e88d1ab05afdba6b929a206f91424b1420b2 GIT binary patch literal 226 zcmeAS@N?(olHy`uVBq!ia0vp^Y(UJx0U~c5>$3n-oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBez~WMV@L&KZl57ng8`50 { const [email, setEmail] = useState(null); const [password, setPassword] = useState(null); - const [fullName, setFullName] = useState(""); // Строка для поиска + const [fullName, setFullName] = useState(""); // Строка для поиска const [searchResults, setSearchResults] = useState([]); // Результаты поиска // const [selectedName, setSelectedName] = useState(""); // Выбранное имя const [showSuggestions, setShowSuggestions] = useState(false); // Флаг показа списка @@ -27,7 +27,7 @@ const ModeusLoginForm = () => { setErrorMessage(""); // Очищаем ошибку при успешном поиске }; - /// Обработчик нажатия клавиши "Enter" + /// Обработчик нажатия клавиши "Enter" const handleKeyPress = (e) => { if (e.key === "Enter") { onClickSearch(fullName); // Выполнить поиск, если нажата клавиша Enter @@ -47,7 +47,7 @@ const ModeusLoginForm = () => { setErrorMessage("Неверный логин или пароль. Попробуйте еще раз."); // Устанавливаем текст ошибки return; } - console.log(response) + console.log(response); localStorage.setItem("token", response.data?.token); setErrorMessage(""); // Очищаем ошибку при успешном логине @@ -57,67 +57,68 @@ const ModeusLoginForm = () => { }; return ( -
    -

    Мое расписание

    - -
    - - -
    - setFullName(e.target.value)} // Обновляем строку поиска - onKeyPress={handleKeyPress} // Обработчик для нажатия клавиш - /> - - {/* Рендерим выпадающий список или сообщение об отсутствии результатов */} - {showSuggestions && ( -
      - {searchResults.length > 0 ? ( - searchResults.map((person, index) => ( -
    • handleSelect(person.fullName)}> - {person.fullName} {/* Отображаем имя */} -
    • - )) - ) : ( -
    • Нет такого имени
    • // Сообщение, если список пуст - )} -
    - )} -
    +
    +

    Мое расписание

    + +
    + + +
    + setFullName(e.target.value)} // Обновляем строку поиска + onKeyPress={handleKeyPress} // Обработчик для нажатия клавиш + /> + + {/* Рендерим выпадающий список или сообщение об отсутствии результатов */} + {showSuggestions && ( +
      + {searchResults.length > 0 ? ( + searchResults.map((person, index) => ( +
    • handleSelect(person.fullName)}> + {person.fullName} {/* Отображаем имя */} +
    • + )) + ) : ( +
    • Нет такого имени
    • // Сообщение, если список пуст + )} +
    + )}
    +
    - -
    - -
    - setEmail(e.target.value)} - /> - setPassword(e.target.value)} - /> -
    - {/* Сообщение об ошибке */} - {errorMessage &&

    {errorMessage}

    } - - +
    + +
    + setEmail(e.target.value)} + /> + setPassword(e.target.value)} + />
    + {/* Сообщение об ошибке */} + {errorMessage &&

    {errorMessage}

    } + +
    +
    ); }; diff --git a/frontend/src/index.css b/frontend/src/index.css deleted file mode 100644 index e30857f..0000000 --- a/frontend/src/index.css +++ /dev/null @@ -1,218 +0,0 @@ -@import url("https://fonts.googleapis.com/css2?family=Unbounded:wght@200..900&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&family=Unbounded:wght@200..900&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap"); - -html { - height: 100%; -} - -body { - height: 100%; - background: #fff; - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", - "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", - "Helvetica Neue", sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", - monospace; -} - -.wrapper { - width: 100%; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - margin-top: 20px; -} - -.header-line { - display: flex; - justify-content: space-between; - align-items: center; -} - -.raspisanie-export { - display: flex; - align-items: center; -} - -header .raspisanie { - margin-left: 120px; - font-family: "Unbounded", sans-serif; - font-weight: 500; - font-size: 32px; -} -header .export-btn { - cursor: pointer; - width: 90px; - height: 28px; - background-color: #5856d6; - color: #fff; - text-decoration: none; - border: none; - border-radius: 6px; - padding: 4px 8px; - font-family: "Roboto", sans-serif; - font-weight: 400; - font-style: normal; - margin-left: 18px; - transition: color 0.2s linear; -} - -header .export-btn:hover { - background-color: #4745b5; -} - -header .login-btn { - margin-right: 120px; - cursor: pointer; - border: none; - background-color: #5856d6; - color: #fff; - text-decoration: none; - border-radius: 6px; - padding: 10px 20px; - font-family: "Roboto", sans-serif; - font-weight: 400; - transition: color 0.2s linear; -} - -header .login-btn:hover { - background-color: #4745b5; -} - -.input-name { - margin-top: 8px; - width: 320px; - height: 32px; - border-radius: 4px; - border-color: #d9d9d9; -} - -.input-email { - margin-top: 8px; - width: 320px; - height: 32px; - border-radius: 4px; - border-color: #d9d9d9; -} - -.login-btn-log { - width: 100px; - position: relative; - cursor: pointer; - border: none; - margin: 6px auto; - background-color: #5856d6; - color: #fff; - text-decoration: none; - border-radius: 6px; - padding: 10px 20px; - font-family: "Roboto", sans-serif; - font-weight: 400; - transition: color 0.2s linear; -} -.login-btn-log:hover { - background-color: #4745b5; -} - -.login-container { - display: flex; - align-items: center; - flex-direction: column; -} - -.login-netologiya, .login-fio { - max-width: 320px; - width: 100%; - margin: 24px auto; - display: flex; - align-items: flex-start; - justify-content: center; - flex-direction: column; - text-align: left; -} - -.calendar { - position: absolute; - top: 165px; - right: 248px; - background-color: #ecedf0; - width: 406px; - height: 32px; - border-radius: 6px; - border: 1px solid rgba(217, 217, 217, 0.5); - font-family: "Unbounded", sans-serif; - font-weight: 500; - font-size: 15px; - padding-left: 14px; - transition: color 0.2s linear; -} -.calendar:hover { - background-color: #e7e7ea; -} - -.raspisanie-table { - width: 100%; - margin-top: 170px; - border-spacing: 1px; -} -.days { - padding-left: 30px; - font-family: "Unbounded", sans-serif; - font-weight: 500; - font-size: 13px; - border: 1px; - border-bottom: 1px dotted #adadad; -} -.vertical { - text-align: center; - border-bottom: 1px dotted #adadad; -} -.vertical-zagolovok { - padding-left: 47px; - padding-top: 7px; - padding-bottom: 7px; - font-family: "Roboto Mono", monospace; - font-weight: 400; - font-size: 12px; - width: 0; - border-bottom: 1px dotted #adadad; -} - -.vertical-zagolovok::first-line { - font-family: "Roboto", sans-serif; - font-weight: 700; - font-size: 13px; -} - -.off-deadline { - margin-top: 7px; - font-family: "Roboto", sans-serif; - font-weight: 400; - font-size: 13px; - color: #7b61ff; - background: none; - padding: 4px 8px; - border-radius: 6px; - border: 1px solid #7b61ff; - transition: color 0.3s linear; -} -.off-deadline:hover { - cursor: pointer; - background-color: #5856d6; - color: #fff; -} - -.error-message { - color: red; - margin-top: 10px; -} diff --git a/frontend/src/index.js b/frontend/src/index.js index 05ae907..6372ecd 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -6,7 +6,7 @@ import ReactDOM from "react-dom/client"; import LoginRoute from "./pages/LoginRoute"; import CalendarRoute from "./pages/CalendarRoute"; -import "./index.css"; +import "./index.scss"; import PrivateRoute from "./components/Calendar/PrivateRoute"; const checkAuth = () => { @@ -16,22 +16,22 @@ const checkAuth = () => { const root = ReactDOM.createRoot(document.getElementById("root")); const router = createBrowserRouter([ - { - path: "/", - element: checkAuth() ? : , + { + path: "/", + element: checkAuth() ? : , // Если токен есть — перенаправляем на /calendar, если нет — на /login }, { path: "/login", element: checkAuth() ? : , - // Перенаправление на календарь, если уже залогинен + // Перенаправление на календарь, если уже залогинен }, { path: "/calendar", element: ( - - - + // + + // ), // Защищаем страницу календаря }, ]); @@ -39,7 +39,7 @@ const router = createBrowserRouter([ root.render( - + , ); reportWebVitals(); diff --git a/frontend/src/index.scss b/frontend/src/index.scss new file mode 100644 index 0000000..ee97c90 --- /dev/null +++ b/frontend/src/index.scss @@ -0,0 +1,458 @@ +@import url("https://fonts.googleapis.com/css2?family=Unbounded:wght@200..900&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&family=Unbounded:wght@200..900&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap"); + +html { + height: 100%; +} + +body { + height: 100%; + background: #fff; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} + +.wrapper { + width: 100%; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + margin-top: 20px; +} + +.header-line { + display: flex; + justify-content: space-between; + align-items: center; +} + +.shedule-export { + display: flex; + align-items: center; +} + +header .shedule { + margin-left: 120px; + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 32px; +} + +header .export-btn { + cursor: pointer; + height: 28px; + background-color: #5856d6; + color: #fff; + text-decoration: none; + border: none; + border-radius: 6px; + padding: 4px 8px; + font-family: "Roboto", sans-serif; + font-weight: 400; + font-style: normal; + margin-left: 18px; + transition: color 0.2s linear; + &:hover { + background-color: #4745b5; + } +} + +header .cache-btn { + margin-left: 18px; + height: 28px; + font-family: "Roboto", sans-serif; + font-weight: 400; + font-size: 13px; + color: #7b61ff; + background: none; + padding: 4px 8px; + border-radius: 6px; + border: 1px solid #7b61ff; + transition: color 0.3s linear; + &:hover { + cursor: pointer; + background-color: #5856d6; + color: #fff; + } +} + +header .exit-btn { + background: none; + margin-right: 120px; + cursor: pointer; + border: none; + color: #333333; + text-decoration: none; + font-family: "Roboto", sans-serif; + font-weight: 400; + font-size: 13px; + &-cross { + margin-left: 7px; + } +} + +header #rectangle { + padding: 0px 12px; + margin: 22px 120px; + width: 534px; + height: 106px; + border-radius: 6px; + background: #f4638633; + border: 2px solid #f46386; +} + +.source { + margin-top: 7px; + color: #f46386; + font-family: "Roboto", sans-serif; + font-weight: 700; + font-size: 13px; + &-first-word { + font-family: "Roboto", sans-serif; + font-weight: 400; + } +} + +.date-event { + float: right; +} + +.name-event { + margin-top: 26px; + &-text { + color: #2c2d2e; + font-family: "Unbounded", sans-serif; + font-weight: 700; + font-size: 15px; + } +} + +.task-event { + margin-top: 7px; + &-text { + color: #2c2d2e; + font-family: "Roboto", sans-serif; + font-weight: 400; + font-size: 15px; + } +} + +.input-name { + margin-top: 8px; + width: 320px; + height: 32px; + border-radius: 4px; + border-color: #d9d9d9; +} + +.input-email { + margin-top: 8px; + width: 320px; + height: 32px; + border-radius: 4px; + border-color: #d9d9d9; +} + +.login-btn-log { + width: 100px; + position: relative; + cursor: pointer; + border: none; + margin: 6px auto; + background-color: #5856d6; + color: #fff; + text-decoration: none; + border-radius: 6px; + padding: 10px 20px; + font-family: "Roboto", sans-serif; + font-weight: 400; + transition: color 0.2s linear; + &:hover { + background-color: #4745b5; + } +} + +.login-container { + display: flex; + align-items: center; + flex-direction: column; +} + +.shedule-login { + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 32px; +} + +.login-netologiya, +.login-fio { + font-family: "Roboto", sans-serif; + font-weight: 400; + font-size: 15px; + max-width: 320px; + width: 100%; + margin: 24px auto; + display: flex; + align-items: flex-start; + justify-content: center; + flex-direction: column; + text-align: left; +} + +.calendar { + position: absolute; + top: 165px; + right: 248px; + background-color: #ecedf0; + width: 406px; + height: 32px; + border-radius: 6px; + border: 1px solid rgba(217, 217, 217, 0.5); + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 15px; + padding-left: 14px; + transition: color 0.2s linear; + &:hover { + background-color: #e7e7ea; + } +} + +.shedule-table { + width: 100%; + margin-top: 36px; + border-spacing: 1px; +} + +.days { + /* padding-left: 30px; + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 13px; + border: 1px;*/ + border-bottom: 1px dotted #adadad; + &-1 { + color: #adadad; + padding-left: 30px; + padding-bottom: 10px; + padding-top: 10px; + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 13px; + border: 1px; + border-bottom: 1px dotted #adadad; + } + &-2 { + color: #7b61ff; + padding-left: 30px; + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 13px; + border: 1px; + border-bottom: 1px dotted #adadad; + } + &-3 { + color: #333333; + padding-left: 30px; + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 13px; + border: 1px; + border-bottom: 1px dotted #adadad; + } + &-4 { + color: #333333; + padding-left: 30px; + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 13px; + border: 1px; + border-bottom: 1px dotted #adadad; + } + &-5 { + color: #333333; + padding-left: 30px; + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 13px; + border: 1px; + border-bottom: 1px dotted #adadad; + } + &-6 { + color: #333333; + padding-left: 30px; + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 13px; + border: 1px; + border-bottom: 1px dotted #adadad; + } + &-7 { + color: #333333; + padding-left: 30px; + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 13px; + border: 1px; + border-bottom: 1px dotted #adadad; + } +} + +.vertical-deadline { + vertical-align: top; + height: 72px; + width: 195px; + padding-left: 20px; + border-bottom: 1px dotted #adadad; + &-heading { + padding-left: 47px; + padding-top: 7px; + padding-bottom: 7px; + font-family: "Roboto Mono", monospace; + font-weight: 400; + font-size: 12px; + width: 0; + border-bottom: 1px dotted #adadad; + &::first-line { + font-family: "Roboto", sans-serif; + font-weight: 700; + font-size: 13px; + } + } +} + +.vertical { + height: 72px; + width: 195px; + padding-left: 20px; + border-bottom: 1px dotted #adadad; + &-heading { + padding-left: 47px; + padding-top: 7px; + padding-bottom: 7px; + font-family: "Roboto Mono", monospace; + font-weight: 400; + font-size: 12px; + width: 0; + border-bottom: 1px dotted #adadad; + &::first-line { + font-family: "Roboto", sans-serif; + font-weight: 700; + font-size: 13px; + } + } +} + +.off-deadline { + margin-top: 7px; + font-family: "Roboto", sans-serif; + font-weight: 400; + font-size: 13px; + color: #7b61ff; + background: none; + padding: 4px 8px; + border-radius: 6px; + border: 1px solid #7b61ff; + transition: color 0.3s linear; + &:hover { + cursor: pointer; + background-color: #5856d6; + color: #fff; + } +} + +.deadline { + &-info { + height: 28px; + margin-top: 6px; + width: 95%; + text-align: left; + padding: 5px 11px; + border: none; + background: #F46386; + border-radius: 6px; + color: #fff; + font-family: "Roboto" sans-serif; + font-weight: 700; + font-size: 13px; + transition: color 0.3s linear; + &:hover { + cursor: pointer; + color: #F46386; + background-color: #fff; + border: 2px solid #F46386; + } + } + &-info-on { + height: 28px; + margin-top: 4px; + width: 95%; + text-align: left; + padding: 5px 11px; + color: #F46386; + background-color: #fff; + border: 2px solid #F46386; + border-radius: 6px; + font-family: "Roboto" sans-serif; + font-weight: 700; + font-size: 13px; + transition: color 0.3s linear; + &:hover { + cursor: pointer; + color: #fff; + background-color: #F46386; + border: none; + } + } +} + +.past-lesson { + padding-left: 12px; + text-align: left; + width: 95%; + height: 95%; + background: #ECEDF0; + border: none; + border-radius: 6px; + &:hover { + cursor: pointer; + } +} + +.company-name { + vertical-align: top; + font-family: "Roboto", sans-serif; + font-weight: 500; + font-size: 10px; + color: #7B61FF; +} + +.lesson-name { + vertical-align: top; + font-family: "Roboto", sans-serif; + font-weight: 400; + font-size: 13px; + color: #2C2D2E; +} + +.error-message { + font-family: "Roboto", sans-serif; + font-weight: 400; + font-size: 15px; + text-align: center; + color: red; + margin-top: 10px; +} From 207b8c5494c98c1edc43d5036cbf107d793552d9 Mon Sep 17 00:00:00 2001 From: alex karpovich Date: Wed, 9 Oct 2024 01:18:39 +0300 Subject: [PATCH 06/31] chore: del folders, files, --- frontend/src/App.js | 11 ----- frontend/src/App.scss | 29 ------------- frontend/src/App.test.js | 8 ---- .../src/components/Login/ModeusLoginForm.jsx | 0 frontend/src/index.scss | 41 +++++++++++++++++-- frontend/src/pages/CalendarRoute.jsx | 2 +- frontend/src/pages/LoginRoute.jsx | 2 +- .../src/services/{api/login.js => api.js} | 2 +- 8 files changed, 41 insertions(+), 54 deletions(-) delete mode 100644 frontend/src/App.js delete mode 100644 frontend/src/App.scss delete mode 100644 frontend/src/App.test.js delete mode 100644 frontend/src/components/Login/ModeusLoginForm.jsx rename frontend/src/services/{api/login.js => api.js} (98%) diff --git a/frontend/src/App.js b/frontend/src/App.js deleted file mode 100644 index 1fbb552..0000000 --- a/frontend/src/App.js +++ /dev/null @@ -1,11 +0,0 @@ -import DatePicker from "./components/DataPicker"; - -function App() { - return ( -
    - -
    - ); -} - -export default App; diff --git a/frontend/src/App.scss b/frontend/src/App.scss deleted file mode 100644 index 189cdee..0000000 --- a/frontend/src/App.scss +++ /dev/null @@ -1,29 +0,0 @@ -.App { - text-align: center; - &-logo { - height: 40vmin; - pointer-events: none; - } - &-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; - } - &-link { - color: #61dafb; - } -} -media (prefers-reduced-motion: no-preference) { - animation: App-logo-spin infinite 20s linear; -} -keyframes App-logo-spin { - transform: rotate(0deg); -} -to { - transform: rotate(360deg); -} diff --git a/frontend/src/App.test.js b/frontend/src/App.test.js deleted file mode 100644 index 1f03afe..0000000 --- a/frontend/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/frontend/src/components/Login/ModeusLoginForm.jsx b/frontend/src/components/Login/ModeusLoginForm.jsx deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/index.scss b/frontend/src/index.scss index ee97c90..4a4b47f 100644 --- a/frontend/src/index.scss +++ b/frontend/src/index.scss @@ -2,6 +2,41 @@ @import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&family=Unbounded:wght@200..900&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap"); +// app file start ======== +.App { + text-align: center; + &-logo { + height: 40vmin; + pointer-events: none; + } + &-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; + } + &-link { + color: #61dafb; + } +} +//TODO: ругается здесь +//media (prefers-reduced-motion: no-preference) { +// animation: App-logo-spin infinite 20s linear; +//} +//keyframes App-logo-spin { +// transform: rotate(0deg); +//} +//to { +// transform: rotate(360deg); +//} +// app file end ======== + + + html { height: 100%; } @@ -105,7 +140,7 @@ header .exit-btn { } header #rectangle { - padding: 0px 12px; + padding: 0 12px; margin: 22px 120px; width: 534px; height: 106px; @@ -385,7 +420,7 @@ header #rectangle { background: #F46386; border-radius: 6px; color: #fff; - font-family: "Roboto" sans-serif; + font-family: "Roboto", sans-serif; font-weight: 700; font-size: 13px; transition: color 0.3s linear; @@ -406,7 +441,7 @@ header #rectangle { background-color: #fff; border: 2px solid #F46386; border-radius: 6px; - font-family: "Roboto" sans-serif; + font-family: "Roboto", sans-serif; font-weight: 700; font-size: 13px; transition: color 0.3s linear; diff --git a/frontend/src/pages/CalendarRoute.jsx b/frontend/src/pages/CalendarRoute.jsx index fa0941d..cbf148f 100644 --- a/frontend/src/pages/CalendarRoute.jsx +++ b/frontend/src/pages/CalendarRoute.jsx @@ -1,5 +1,5 @@ import React, {useContext, useEffect, useState} from "react"; -import { getNetologyCourse, bulkEvents } from "../services/api/login"; +import { getNetologyCourse, bulkEvents } from "../services/api"; import {AuthContext} from "../context/AuthContext"; // Импортируем API функции const CalendarRoute = () => { diff --git a/frontend/src/pages/LoginRoute.jsx b/frontend/src/pages/LoginRoute.jsx index 8fde5e2..7b550b6 100644 --- a/frontend/src/pages/LoginRoute.jsx +++ b/frontend/src/pages/LoginRoute.jsx @@ -1,7 +1,7 @@ import {useContext, useState} from "react"; import { useNavigate } from "react-router-dom"; import {AuthContext} from "../context/AuthContext"; -import {loginModeus, searchModeus} from "../services/api/login"; +import {loginModeus, searchModeus} from "../services/api"; const LoginRoute = () => { const { setAuthData } = useContext(AuthContext); // Достаем setAuthData из контекста diff --git a/frontend/src/services/api/login.js b/frontend/src/services/api.js similarity index 98% rename from frontend/src/services/api/login.js rename to frontend/src/services/api.js index 2f1184b..cd8b458 100644 --- a/frontend/src/services/api/login.js +++ b/frontend/src/services/api.js @@ -1,5 +1,5 @@ import axios from 'axios'; -import { BACKEND_URL } from '../../variables'; +import { BACKEND_URL } from '../variables'; export function getTokenFromLocalStorage() { From 1ea789cea6ab93b4c91a94a23cddd162483f4a45 Mon Sep 17 00:00:00 2001 From: KytakBR Date: Thu, 10 Oct 2024 11:48:56 +0500 Subject: [PATCH 07/31] styled: add calendar component --- frontend/src/components/Header/Header.js | 93 +++++++++++++++--------- frontend/src/index.js | 12 ++- frontend/src/index.scss | 48 ++++++++++-- 3 files changed, 109 insertions(+), 44 deletions(-) diff --git a/frontend/src/components/Header/Header.js b/frontend/src/components/Header/Header.js index 09df4d8..1ce3a7f 100644 --- a/frontend/src/components/Header/Header.js +++ b/frontend/src/components/Header/Header.js @@ -31,6 +31,7 @@ export default function Header() { Домашнее задание с самопроверкой(дедлайн 12.12.24)
    +
    ПнВтСрЧтПтСбВсПн 23.09Вт 24.09Ср 25.09Чт 26.09Пт 27.09Сб 28.09Вс 29.09
    - дедлайны - - 1234567 + дедлайны + + + + +
    2 пара 10:15 11:4512 пара
    10:15 11:45
    2 3 47
    3 пара 12:00 13:3013 пара
    12:00 13:30
    2 3 47
    4 пара 14:00 15:3014 пара
    14:00 15:30
    2 3 47
    5 пара 15:45 17:1515 пара
    15:45 17:15
    2 3 47
    6 пара 17:30 19:0016 пара
    17:30 19:00
    2 3 47
    7 пара 19:10 20:4017 пара
    19:10 20:40
    2 3 4
    @@ -62,66 +63,86 @@ export default function Header() { - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + +
    2 пара
    10:15 11:45
    234567
    3 пара
    12:00 13:30
    234567
    4 пара
    14:00 15:30
    234567
    5 пара
    15:45 17:15
    234567
    6 пара
    17:30 19:00
    234567
    7 пара
    19:10 20:40
    234567
    diff --git a/frontend/src/index.js b/frontend/src/index.js index 8af0ec3..cc3823f 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -3,6 +3,8 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import reportWebVitals from "./reportWebVitals"; import ReactDOM from "react-dom/client"; +import DataPicker from "./components/Calendar/DataPicker" +import Header from "./components/Header/Header"; import LoginRoute from "./pages/LoginRoute"; import CalendarRoute from "./pages/CalendarRoute"; import { AuthProvider } from './context/AuthContext'; @@ -30,9 +32,13 @@ const router = createBrowserRouter([ { path: "/calendar", element: ( - - - + <> +
    + + + // + // + // ), // Защищаем страницу календаря }, ]); diff --git a/frontend/src/index.scss b/frontend/src/index.scss index ee97c90..51a6a3d 100644 --- a/frontend/src/index.scss +++ b/frontend/src/index.scss @@ -238,11 +238,6 @@ header #rectangle { } .days { - /* padding-left: 30px; - font-family: "Unbounded", sans-serif; - font-weight: 500; - font-size: 13px; - border: 1px;*/ border-bottom: 1px dotted #adadad; &-1 { color: #adadad; @@ -420,6 +415,9 @@ header #rectangle { } .past-lesson { + display: flex; + flex-direction: column; + justify-content: space-evenly; padding-left: 12px; text-align: left; width: 95%; @@ -448,6 +446,46 @@ header #rectangle { color: #2C2D2E; } +.netology-lesson { + display: flex; + flex-direction: column; + justify-content: space-evenly; + padding-left: 12px; + text-align: left; + width: 95%; + height: 95%; + background: #00A8A833; + border: none; + border-radius: 6px; + &:hover { + cursor: pointer; + } +} + +.TyumGU-lesson { + display: flex; + flex-direction: column; + justify-content: space-evenly; + padding-left: 12px; + text-align: left; + width: 95%; + height: 95%; + background: #7B61FF33; + border: none; + border-radius: 6px; + &:hover { + cursor: pointer; + } +} + +.vertical-line { + margin-top: 17px; + margin-left: 365px; + position: absolute; + border-left: 2px solid #7B61FF; + height: 590px; +} + .error-message { font-family: "Roboto", sans-serif; font-weight: 400; From 0d2d0af4635d9812132d88abb16a045e75160d3a Mon Sep 17 00:00:00 2001 From: KytakBR Date: Thu, 10 Oct 2024 12:09:27 +0500 Subject: [PATCH 08/31] fix change route calendar --- frontend/src/index.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/frontend/src/index.js b/frontend/src/index.js index cc3823f..12f3815 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -3,7 +3,6 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import reportWebVitals from "./reportWebVitals"; import ReactDOM from "react-dom/client"; -import DataPicker from "./components/Calendar/DataPicker" import Header from "./components/Header/Header"; import LoginRoute from "./pages/LoginRoute"; import CalendarRoute from "./pages/CalendarRoute"; @@ -32,13 +31,9 @@ const router = createBrowserRouter([ { path: "/calendar", element: ( - <> -
    - - - // - // - // + + + ), // Защищаем страницу календаря }, ]); From 96c1cc96f7b86306a9c4d805f100715953262a50 Mon Sep 17 00:00:00 2001 From: Ivan Popov Date: Fri, 11 Oct 2024 18:57:30 +0300 Subject: [PATCH 09/31] fix ci --- backend/yet_another_calendar/settings.py | 2 +- backend/yet_another_calendar/web/api/router.py | 4 +++- docker-compose.yaml | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/yet_another_calendar/settings.py b/backend/yet_another_calendar/settings.py index fb2fd35..fb804d3 100644 --- a/backend/yet_another_calendar/settings.py +++ b/backend/yet_another_calendar/settings.py @@ -34,7 +34,7 @@ class Settings(BaseSettings): host: str = "127.0.0.1" port: int = 8000 # quantity of workers for uvicorn - workers_count: int = 1 + workers_count: int = env.int("YET_ANOTHER_WORKERS_COUNT", 1) # Enable uvicorn reloading reload: bool = env.bool("YET_ANOTHER_CALENDAR_RELOAD", False) diff --git a/backend/yet_another_calendar/web/api/router.py b/backend/yet_another_calendar/web/api/router.py index ec2dc19..94169ff 100644 --- a/backend/yet_another_calendar/web/api/router.py +++ b/backend/yet_another_calendar/web/api/router.py @@ -1,10 +1,12 @@ from fastapi.routing import APIRouter +from yet_another_calendar.settings import settings from yet_another_calendar.web.api import docs, monitoring, netology, modeus, bulk api_router = APIRouter() api_router.include_router(monitoring.router) -api_router.include_router(docs.router) +if settings.debug: + api_router.include_router(docs.router) api_router.include_router(netology.router, prefix="/netology", tags=["netology"]) api_router.include_router(modeus.router, prefix="/modeus", tags=["modeus"]) api_router.include_router(bulk.router, prefix="/bulk", tags=["bulk"]) diff --git a/docker-compose.yaml b/docker-compose.yaml index 82ac8da..445414a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,7 @@ services: api: &main_app ports: - - "8000:8000" + - "127.0.0.1:8000:8000" build: context: backend/ dockerfile: Dockerfile @@ -43,7 +43,7 @@ services: volumes: - ./frontend/src:/app/src ports: - - "3000:3000" + - "127.0.0.1:3000:3000" volumes: redis_data: From 929b22db95efb245f7cfcc6606f95244d33f6f79 Mon Sep 17 00:00:00 2001 From: Ivan Popov Date: Fri, 11 Oct 2024 20:12:51 +0300 Subject: [PATCH 10/31] fix api/docs --- .../{static => static_backend}/docs/redoc.standalone.js | 0 .../{static => static_backend}/docs/swagger-ui-bundle.js | 0 .../{static => static_backend}/docs/swagger-ui.css | 0 backend/yet_another_calendar/web/api/docs/views.py | 4 ++-- 4 files changed, 2 insertions(+), 2 deletions(-) rename backend/yet_another_calendar/{static => static_backend}/docs/redoc.standalone.js (100%) rename backend/yet_another_calendar/{static => static_backend}/docs/swagger-ui-bundle.js (100%) rename backend/yet_another_calendar/{static => static_backend}/docs/swagger-ui.css (100%) diff --git a/backend/yet_another_calendar/static/docs/redoc.standalone.js b/backend/yet_another_calendar/static_backend/docs/redoc.standalone.js similarity index 100% rename from backend/yet_another_calendar/static/docs/redoc.standalone.js rename to backend/yet_another_calendar/static_backend/docs/redoc.standalone.js diff --git a/backend/yet_another_calendar/static/docs/swagger-ui-bundle.js b/backend/yet_another_calendar/static_backend/docs/swagger-ui-bundle.js similarity index 100% rename from backend/yet_another_calendar/static/docs/swagger-ui-bundle.js rename to backend/yet_another_calendar/static_backend/docs/swagger-ui-bundle.js diff --git a/backend/yet_another_calendar/static/docs/swagger-ui.css b/backend/yet_another_calendar/static_backend/docs/swagger-ui.css similarity index 100% rename from backend/yet_another_calendar/static/docs/swagger-ui.css rename to backend/yet_another_calendar/static_backend/docs/swagger-ui.css diff --git a/backend/yet_another_calendar/web/api/docs/views.py b/backend/yet_another_calendar/web/api/docs/views.py index d75854f..3770cca 100644 --- a/backend/yet_another_calendar/web/api/docs/views.py +++ b/backend/yet_another_calendar/web/api/docs/views.py @@ -22,8 +22,8 @@ async def swagger_ui_html(request: Request) -> HTMLResponse: openapi_url=request.app.openapi_url, title=f"{title} - Swagger UI", oauth2_redirect_url=str(request.url_for("swagger_ui_redirect")), - swagger_js_url="/static/docs/swagger-ui-bundle.js", - swagger_css_url="/static/docs/swagger-ui.css", + swagger_js_url="/static_backend/docs/swagger-ui-bundle.js", + swagger_css_url="/static_backend/docs/swagger-ui.css", ) From f4df161fb6d86fbcac0eaf0e7239c3ed0a5482f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B0=D1=80=D0=BF=D0=BE=D0=B2=D0=B8=D1=87=20=D0=90?= =?UTF-8?q?=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Fri, 11 Oct 2024 20:18:24 +0300 Subject: [PATCH 11/31] fix: add varable .env --- frontend/src/context/AuthContext.js | 6 +- frontend/src/pages/CalendarRoute.jsx | 38 +++++ frontend/src/pages/LoginRoute.jsx | 222 +++++++++++++-------------- frontend/src/services/api.js | 6 +- frontend/src/variables.js | 1 - 5 files changed, 156 insertions(+), 117 deletions(-) delete mode 100644 frontend/src/variables.js diff --git a/frontend/src/context/AuthContext.js b/frontend/src/context/AuthContext.js index b4b3ce2..b2d980e 100644 --- a/frontend/src/context/AuthContext.js +++ b/frontend/src/context/AuthContext.js @@ -4,13 +4,11 @@ import React, { createContext, useState } from 'react'; // Создаем контекст export const AuthContext = createContext(); -// Создаем провайдер для использования контекста +// Создаем провайдер export const AuthProvider = ({ children }) => { const [authData, setAuthData] = useState({ email: null, password: null, - person: null, - personId: null, }); return ( @@ -18,4 +16,4 @@ export const AuthProvider = ({ children }) => { {children} ); -}; +}; \ No newline at end of file diff --git a/frontend/src/pages/CalendarRoute.jsx b/frontend/src/pages/CalendarRoute.jsx index cbf148f..aca706a 100644 --- a/frontend/src/pages/CalendarRoute.jsx +++ b/frontend/src/pages/CalendarRoute.jsx @@ -6,6 +6,7 @@ const CalendarRoute = () => { const { authData } = useContext(AuthContext); // Достаем данные из контекста const [calendarId, setCalendarId] = useState(null); // Хранение calendarId + const [courses, setCourses] = useState({ homework: [], webinars: [] }); // Для хранения курсов const [events, setEvents] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -22,6 +23,12 @@ const CalendarRoute = () => { const fetchedCalendarId = courseData?.id; // Предполагаем, что calendarId есть в данных курса setCalendarId(fetchedCalendarId); + // Сохраняем курсы (домашние задания и вебинары) + if (courseData?.netology) { + setCourses(courseData.netology); + } + + if (fetchedCalendarId) { // дату получаем события для календаря const eventsResponse = await bulkEvents( @@ -71,6 +78,37 @@ const CalendarRoute = () => { ) : (

    Нет доступных событий

    )} + +

    Курсы

    +

    Домашние задания

    + {courses.homework.length > 0 ? ( + + ) : ( +

    Нет домашних заданий

    + )} + +

    Вебинары

    + {courses.webinars.length > 0 ? ( + + ) : ( +

    Нет вебинаров

    + )}
); }; diff --git a/frontend/src/pages/LoginRoute.jsx b/frontend/src/pages/LoginRoute.jsx index 7b550b6..b07cd50 100644 --- a/frontend/src/pages/LoginRoute.jsx +++ b/frontend/src/pages/LoginRoute.jsx @@ -1,10 +1,10 @@ import {useContext, useState} from "react"; -import { useNavigate } from "react-router-dom"; +import {useNavigate} from "react-router-dom"; import {AuthContext} from "../context/AuthContext"; import {loginModeus, searchModeus} from "../services/api"; const LoginRoute = () => { - const { setAuthData } = useContext(AuthContext); // Достаем setAuthData из контекста + const {setAuthData} = useContext(AuthContext); // Достаем setAuthData из контекста const [email, setEmail] = useState(null); const [password, setPassword] = useState(null); @@ -18,130 +18,130 @@ const LoginRoute = () => { const navigate = useNavigate(); // Инициализируем хук для навигации const onClickSearch = async (fullName) => { - console.log("Поиск ФИО:", fullName); - - let response = await searchModeus(fullName); - if (response.status !== 200) { - setErrorMessage("Неверное ФИО. Попробуйте еще раз."); - return; - } - console.log("Результаты поиска:", response.data); - setSearchResults(response.data); - setShowSuggestions(true); // Показываем список после поиска - setErrorMessage(""); // Очищаем ошибку при успешном поиске - }; + console.log("Поиск ФИО:", fullName); + + let response = await searchModeus(fullName); + if (response.status !== 200) { + setErrorMessage("Неверное ФИО. Попробуйте еще раз."); + return; + } + console.log("Результаты поиска:", response.data); + setSearchResults(response.data); + setShowSuggestions(true); // Показываем список после поиска + setErrorMessage(""); // Очищаем ошибку при успешном поиске + }; /// Обработчик нажатия клавиши "Enter" - const handleKeyPress = (e) => { - if (e.key === "Enter") { - onClickSearch(fullName); // Выполнить поиск, если нажата клавиша Enter - } - }; + const handleKeyPress = (e) => { + if (e.key === "Enter") { + onClickSearch(fullName); // Выполнить поиск, если нажата клавиша Enter + } + }; - // Обработчик выбора варианта из списка + // Обработчик выбора варианта из списка const handleSelect = (person) => { console.log('person', person) setFullName(person.fullName); // Устанавливаем выбранное имя setPersonId(person.personId); - setAuthData((prev) => ({ - person: person, - personId: personId, // Сохраняем personId в контекст - ...prev, - })); - + // setAuthData((prev) => ({ + // person: person, + // personId: personId, // Сохраняем personId в контекст + // ...prev, + // })); + // setAuthData({ person }); setShowSuggestions(false); // Скрываем список после выбора }; - const onClickLogin = async () => { - let response = await loginModeus(email, password); - - if (response.status !== 200) { - setErrorMessage("Неверный логин или пароль. Попробуйте еще раз."); // Устанавливаем текст ошибки - return; - } - - setAuthData((prev) => ({ - email, - password, - ...prev, - })); - - console.log(response) - localStorage.setItem("token", response.data?.token); - setErrorMessage(""); // Очищаем ошибку при успешном логине - // email, password - - - // Перенаправление на страницу календаря - navigate("/calendar"); - // window.location.reload(); // Обновляем страницу после навигации - }; - - return ( -
-

Мое расписание

- -
- - -
- setFullName(e.target.value)} // Обновляем строку поиска - onKeyPress={handleKeyPress} // Обработчик для нажатия клавиш - /> - - {/* Рендерим выпадающий список или сообщение об отсутствии результатов */} - {showSuggestions && ( -
    - {searchResults.length > 0 ? ( - searchResults.map((person, index) => ( -
  • handleSelect(person)}> - {person.fullName} {/* Отображаем имя */} -
  • - )) - ) : ( -
  • Нет такого имени
  • // Сообщение, если список пуст - )} -
- )} -
-
+ const onClickLogin = async () => { + let response = await loginModeus(email, password); + // console.log('email', email) + // console.log('password', password) + + if (response.status !== 200) { + setErrorMessage("Неверный логин или пароль. Попробуйте еще раз."); // Устанавливаем текст ошибки + return; + } + console.log('setAuthData получил', setAuthData) + + // Set email, password, and personId in the AuthContext + setAuthData({ email, password }); + + console.log('setAuthData передал ', setAuthData) + + localStorage.setItem("token", response.data["_netology-on-rails_session"]); + setErrorMessage(""); // Очищаем ошибку при успешном логине + + // Перенаправление на страницу календаря + navigate("/calendar"); + window.location.reload(); // Обновляем страницу после навигации + }; -
- -
- setEmail(e.target.value)} - /> - setPassword(e.target.value)} - /> -
- {/* Сообщение об ошибке */} - {errorMessage &&

{errorMessage}

} - - + return ( +
+

Мое расписание

+ +
+ + +
+ setFullName(e.target.value)} // Обновляем строку поиска + onKeyPress={handleKeyPress} // Обработчик для нажатия клавиш + /> + + {/* Рендерим выпадающий список или сообщение об отсутствии результатов */} + {showSuggestions && ( +
    + {searchResults.length > 0 ? ( + searchResults.map((person, index) => ( +
  • handleSelect(person)}> + {person.fullName} {/* Отображаем имя */} +
  • + )) + ) : ( +
  • Нет такого имени
  • // Сообщение, если список пуст + )} +
+ )} +
+
+ + +
+ +
+ setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> +
+ {/* Сообщение об ошибке */} + {errorMessage &&

{errorMessage}

} + + +
-
- ); + ); } export default LoginRoute diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index cd8b458..89a4948 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -1,10 +1,13 @@ import axios from 'axios'; -import { BACKEND_URL } from '../variables'; + +// env variable +const BACKEND_URL = process.env.BACKEND_URL; export function getTokenFromLocalStorage() { return localStorage.getItem('token') } + // login export async function loginModeus(username, password) { try { @@ -26,6 +29,7 @@ export async function searchModeus(fullName) { } // calendar_id export async function getNetologyCourse(sessionToken) { + console.log('sessionToken', sessionToken) try { const response = await axios.get(`${BACKEND_URL}/api/netology/course/`, { headers: { diff --git a/frontend/src/variables.js b/frontend/src/variables.js deleted file mode 100644 index 5b61d0e..0000000 --- a/frontend/src/variables.js +++ /dev/null @@ -1 +0,0 @@ -export const BACKEND_URL="http://localhost:8000" \ No newline at end of file From 252ba6f60cf8dfcb6ccf15706ad632af58e7638a Mon Sep 17 00:00:00 2001 From: Ivan Popov Date: Fri, 11 Oct 2024 20:18:53 +0300 Subject: [PATCH 12/31] fix api/docs --- .../{static_backend => static}/docs/redoc.standalone.js | 0 .../{static_backend => static}/docs/swagger-ui-bundle.js | 0 .../{static_backend => static}/docs/swagger-ui.css | 0 backend/yet_another_calendar/web/api/docs/views.py | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) rename backend/yet_another_calendar/{static_backend => static}/docs/redoc.standalone.js (100%) rename backend/yet_another_calendar/{static_backend => static}/docs/swagger-ui-bundle.js (100%) rename backend/yet_another_calendar/{static_backend => static}/docs/swagger-ui.css (100%) diff --git a/backend/yet_another_calendar/static_backend/docs/redoc.standalone.js b/backend/yet_another_calendar/static/docs/redoc.standalone.js similarity index 100% rename from backend/yet_another_calendar/static_backend/docs/redoc.standalone.js rename to backend/yet_another_calendar/static/docs/redoc.standalone.js diff --git a/backend/yet_another_calendar/static_backend/docs/swagger-ui-bundle.js b/backend/yet_another_calendar/static/docs/swagger-ui-bundle.js similarity index 100% rename from backend/yet_another_calendar/static_backend/docs/swagger-ui-bundle.js rename to backend/yet_another_calendar/static/docs/swagger-ui-bundle.js diff --git a/backend/yet_another_calendar/static_backend/docs/swagger-ui.css b/backend/yet_another_calendar/static/docs/swagger-ui.css similarity index 100% rename from backend/yet_another_calendar/static_backend/docs/swagger-ui.css rename to backend/yet_another_calendar/static/docs/swagger-ui.css diff --git a/backend/yet_another_calendar/web/api/docs/views.py b/backend/yet_another_calendar/web/api/docs/views.py index 3770cca..33942ce 100644 --- a/backend/yet_another_calendar/web/api/docs/views.py +++ b/backend/yet_another_calendar/web/api/docs/views.py @@ -49,5 +49,5 @@ async def redoc_html(request: Request) -> HTMLResponse: return get_redoc_html( openapi_url=request.app.openapi_url, title=f"{title} - ReDoc", - redoc_js_url="/static/docs/redoc.standalone.js", + redoc_js_url="/static_backend/docs/redoc.standalone.js", ) From bd0d4e5005eddae9fe9cd2f99a8f0054295c4e2d Mon Sep 17 00:00:00 2001 From: Ivan Popov Date: Fri, 11 Oct 2024 20:22:33 +0300 Subject: [PATCH 13/31] fix api/docs --- backend/yet_another_calendar/web/application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/yet_another_calendar/web/application.py b/backend/yet_another_calendar/web/application.py index 589f486..711aa39 100644 --- a/backend/yet_another_calendar/web/application.py +++ b/backend/yet_another_calendar/web/application.py @@ -86,6 +86,6 @@ def get_app() -> FastAPI: app.include_router(router=api_router, prefix="/api") # Adds static directory. # This directory is used to access swagger files. - app.mount("/static", StaticFiles(directory=APP_ROOT / "static"), name="static") + app.mount("/static_backend", StaticFiles(directory=APP_ROOT / "static"), name="static") return app From 1da5954ee34bd7e1b9bce3e1797c6d5d4a18f805 Mon Sep 17 00:00:00 2001 From: Ivan Popov Date: Fri, 11 Oct 2024 20:35:53 +0300 Subject: [PATCH 14/31] fix frontend issues --- docker-compose.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index 445414a..0aa9691 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -39,6 +39,8 @@ services: target: dev-run image: ghcr.io/azamatkomaev/yet_another_calendar_frontend:${YET_ANOTHER_CALENDAR_VERSION:-latest} container_name: calendar-frontend + env_file: + - frontend/.env restart: always volumes: - ./frontend/src:/app/src From a50326dbfdf78958bc7b6768053ec953cf6a31c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B0=D1=80=D0=BF=D0=BE=D0=B2=D0=B8=D1=87=20=D0=90?= =?UTF-8?q?=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Fri, 11 Oct 2024 20:53:08 +0300 Subject: [PATCH 15/31] fix: rename varable .env --- .gitignore | 2 +- frontend/src/services/api.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 449688b..388c996 100644 --- a/.gitignore +++ b/.gitignore @@ -124,7 +124,7 @@ celerybeat.pid *.sage.py # Environments -.env +frontend/.env .venv env/ venv/ diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 89a4948..388f054 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -1,9 +1,11 @@ import axios from 'axios'; // env variable -const BACKEND_URL = process.env.BACKEND_URL; +const BACKEND_URL = process.env.REACT_APP_BACKEND_URL; +console.log("Backend URL:", BACKEND_URL); + export function getTokenFromLocalStorage() { return localStorage.getItem('token') } From 2ee0dccddb74944f4ae903841ca5605d529a3fed Mon Sep 17 00:00:00 2001 From: Ivan Popov Date: Fri, 11 Oct 2024 22:29:40 +0300 Subject: [PATCH 16/31] Timezone big update --- .../web/api/bulk/integration.py | 34 ++++++++------ .../web/api/bulk/schema.py | 12 +++++ .../web/api/bulk/views.py | 10 ++-- .../web/api/modeus/schema.py | 28 +++++++++++ .../web/api/netology/schema.py | 47 ++++++++++++++++--- 5 files changed, 105 insertions(+), 26 deletions(-) diff --git a/backend/yet_another_calendar/web/api/bulk/integration.py b/backend/yet_another_calendar/web/api/bulk/integration.py index 6261a22..5ec157d 100644 --- a/backend/yet_another_calendar/web/api/bulk/integration.py +++ b/backend/yet_another_calendar/web/api/bulk/integration.py @@ -21,14 +21,14 @@ def create_ics_event(title: str, starts_at: datetime.datetime, ends_at: datetime.datetime, - lesson_id: Any, timezone: datetime.tzinfo, description: Optional[str] = None, + lesson_id: Any, description: Optional[str] = None, webinar_url: Optional[str] = None) -> icalendar.Event: event = icalendar.Event() - dt_now = datetime.datetime.now(tz=timezone) + dt_now = datetime.datetime.now() event.add('summary', title) event.add('location', webinar_url) - event.add('dtstart', starts_at.astimezone(timezone)) - event.add('dtend', ends_at.astimezone(timezone)) + event.add('dtstart', starts_at) + event.add('dtend', ends_at) event.add('dtstamp', dt_now) event.add('uid', lesson_id) if description: @@ -36,11 +36,7 @@ def create_ics_event(title: str, starts_at: datetime.datetime, ends_at: datetime return event -def export_to_ics(calendar: schema.CalendarResponse, timezone: str) -> Iterable[bytes]: - try: - tz = pytz.timezone(timezone) - except pytz.exceptions.UnknownTimeZoneError: - raise HTTPException(detail="Wrong timezone", status_code=status.HTTP_400_BAD_REQUEST) from None +def export_to_ics(calendar: schema.CalendarResponse) -> Iterable[bytes]: ics_calendar = icalendar.Calendar() ics_calendar.add('version', '2.0') ics_calendar.add('prodid', 'yet_another_calendar') @@ -50,12 +46,12 @@ def export_to_ics(calendar: schema.CalendarResponse, timezone: str) -> Iterable[ continue event = create_ics_event(title=f"Netology: {netology_lesson.title}", starts_at=netology_lesson.starts_at, ends_at=netology_lesson.ends_at, lesson_id=netology_lesson.id, - timezone=tz, webinar_url=netology_lesson.webinar_url) + webinar_url=netology_lesson.webinar_url) ics_calendar.add_component(event) for modeus_lesson in calendar.modeus: event = create_ics_event(title=f"Modeus: {modeus_lesson.name}", starts_at=modeus_lesson.start_time, ends_at=modeus_lesson.end_time, - lesson_id=modeus_lesson.id, timezone=tz, + lesson_id=modeus_lesson.id, description=modeus_lesson.description) ics_calendar.add_component(event) yield ics_calendar.to_ical() @@ -66,11 +62,12 @@ async def refresh_events( jwt_token: str, calendar_id: int, cookies: netology_schema.NetologyCookies, + timezone: str ) -> schema.RefreshedCalendarResponse: """Clear events cache.""" - cached_json = await get_cached_calendar(body, jwt_token, calendar_id, cookies) + cached_json = await get_cached_calendar(body, jwt_token, calendar_id, cookies, timezone) cached_calendar = schema.CalendarResponse.model_validate(cached_json) - calendar = await get_calendar(body, jwt_token, calendar_id, cookies) + calendar = await get_calendar(body, jwt_token, calendar_id, cookies, timezone) changed = cached_calendar.get_hash() != calendar.get_hash() try: cache_key = default_key_builder(get_cached_calendar, args=(body, jwt_token, calendar_id, cookies), kwargs={}) @@ -93,13 +90,19 @@ async def get_calendar( jwt_token: str, calendar_id: int, cookies: netology_schema.NetologyCookies, + timezone: str ) -> schema.CalendarResponse: + try: + tz = pytz.timezone(timezone) + except pytz.exceptions.UnknownTimeZoneError: + raise HTTPException(detail="Wrong timezone", status_code=status.HTTP_400_BAD_REQUEST) from None + async with asyncio.TaskGroup() as tg: netology_response = tg.create_task(netology_views.get_calendar(body, calendar_id, cookies)) modeus_response = tg.create_task(modeus_views.get_calendar(body, jwt_token)) return schema.CalendarResponse.model_validate( {"netology": netology_response.result(), "modeus": modeus_response.result()}, - ) + ).change_timezone(tz) @cache(expire=settings.redis_events_time_live) @@ -108,5 +111,6 @@ async def get_cached_calendar( jwt_token: str, calendar_id: int, cookies: netology_schema.NetologyCookies, + timezone: str, ) -> schema.CalendarResponse: - return await get_calendar(body, jwt_token, calendar_id, cookies) + return await get_calendar(body, jwt_token, calendar_id, cookies, timezone) diff --git a/backend/yet_another_calendar/web/api/bulk/schema.py b/backend/yet_another_calendar/web/api/bulk/schema.py index 93c31bd..544b220 100644 --- a/backend/yet_another_calendar/web/api/bulk/schema.py +++ b/backend/yet_another_calendar/web/api/bulk/schema.py @@ -11,6 +11,18 @@ class BulkResponse(BaseModel): netology: netology_schema.SerializedEvents modeus: list[modeus_schema.FullEvent] + def change_timezone(self, timezone: datetime.tzinfo): + for homework in self.netology.homework: + if homework.deadline: + homework.deadline = homework.deadline.astimezone(timezone) + for webinar in self.netology.webinars: + webinar.starts_at = webinar.validate_starts_at(webinar.starts_at, timezone) + webinar.ends_at = webinar.validate_ends_at(webinar.ends_at, timezone) + + for event in self.modeus: + event.start_time = event.validate_starts_at(event.start_time, timezone) + event.end_time = event.validate_end_time(event.end_time, timezone) + return self class CalendarResponse(BulkResponse): cached_at: datetime.datetime = Field(default_factory=datetime.datetime.now, alias="cached_at") diff --git a/backend/yet_another_calendar/web/api/bulk/views.py b/backend/yet_another_calendar/web/api/bulk/views.py index c7507ac..035d0fb 100644 --- a/backend/yet_another_calendar/web/api/bulk/views.py +++ b/backend/yet_another_calendar/web/api/bulk/views.py @@ -21,12 +21,13 @@ async def get_calendar( cookies: Annotated[netology_schema.NetologyCookies, Depends(netology_schema.get_cookies_from_headers)], jwt_token: Annotated[str, Depends(modeus_schema.get_cookies_from_headers)], calendar_id: int = settings.netology_default_course_id, + time_zone: str = "Europe/Moscow", ) -> schema.CalendarResponse: """ Get events from Netology and Modeus, cached. """ - cached_calendar = await integration.get_cached_calendar(body, jwt_token, calendar_id, cookies) + cached_calendar = await integration.get_cached_calendar(body, jwt_token, calendar_id, cookies, time_zone) return schema.CalendarResponse.model_validate(cached_calendar) @@ -36,12 +37,13 @@ async def refresh_calendar( cookies: Annotated[netology_schema.NetologyCookies, Depends(netology_schema.get_cookies_from_headers)], jwt_token: Annotated[str, Depends(modeus_schema.get_cookies_from_headers)], calendar_id: int = settings.netology_default_course_id, + time_zone: str = "Europe/Moscow", ) -> schema.RefreshedCalendarResponse: """ Refresh events in redis. """ - return await integration.refresh_events(body, jwt_token, calendar_id, cookies) + return await integration.refresh_events(body, jwt_token, calendar_id, cookies, time_zone) @router.post("/export_ics/") @@ -55,5 +57,5 @@ async def export_ics( """ Export into .ics format """ - calendar = await integration.get_calendar(body, jwt_token, calendar_id, cookies) - return StreamingResponse(integration.export_to_ics(calendar, time_zone)) + calendar = await integration.get_calendar(body, jwt_token, calendar_id, cookies, time_zone) + return StreamingResponse(integration.export_to_ics(calendar)) diff --git a/backend/yet_another_calendar/web/api/modeus/schema.py b/backend/yet_another_calendar/web/api/modeus/schema.py index d7b67f3..9c69840 100644 --- a/backend/yet_another_calendar/web/api/modeus/schema.py +++ b/backend/yet_another_calendar/web/api/modeus/schema.py @@ -19,10 +19,12 @@ class ModeusCreds(BaseModel): username: str password: str = Field(repr=False) + class ModeusTimeBody(BaseModel): time_min: datetime.datetime = Field(alias="timeMin", examples=["2024-09-23T00:00:00+03:00"]) time_max: datetime.datetime = Field(alias="timeMax", examples=["2024-09-29T23:59:59+03:00"]) + # noinspection PyNestedDecorators class ModeusEventsBody(ModeusTimeBody): """Modeus search events body.""" @@ -84,6 +86,18 @@ class Event(BaseModel): end_time: datetime.datetime = Field(alias="end") id: uuid.UUID + @field_validator("start_time") + @classmethod + def validate_starts_at(cls, start_time: datetime.datetime, + timezone=datetime.timezone.utc) -> datetime.datetime: + return start_time.astimezone(timezone) + + @field_validator("end_time") + @classmethod + def validate_end_time(cls, end_time: datetime.datetime, + timezone=datetime.timezone.utc) -> datetime.datetime: + return end_time.astimezone(timezone) + class Href(BaseModel): href: str @@ -157,6 +171,20 @@ class StudentsSpeciality(BaseModel): specialty_name: Optional[str] = Field(alias="specialtyName") specialty_profile: Optional[str] = Field(alias="specialtyProfile") + @field_validator("learning_start_date") + @classmethod + def validate_starts_at(cls, learning_start_date: Optional[datetime.datetime]) -> datetime.datetime: + if not learning_start_date: + return learning_start_date + return learning_start_date.astimezone(datetime.timezone.utc) + + @field_validator("learning_end_date") + @classmethod + def validate_learning_end_date(cls, learning_end_date: Optional[datetime.datetime]) -> datetime.datetime: + if not learning_end_date: + return learning_end_date + return learning_end_date.astimezone(datetime.timezone.utc) + class ExtendedPerson(StudentsSpeciality, ShortPerson): pass diff --git a/backend/yet_another_calendar/web/api/netology/schema.py b/backend/yet_another_calendar/web/api/netology/schema.py index 5539371..f3eecdc 100644 --- a/backend/yet_another_calendar/web/api/netology/schema.py +++ b/backend/yet_another_calendar/web/api/netology/schema.py @@ -4,7 +4,7 @@ from urllib.parse import urljoin from fastapi import Header -from pydantic import BaseModel, Field, computed_field +from pydantic import BaseModel, Field, computed_field, field_validator, model_validator, ConfigDict from yet_another_calendar.settings import settings from yet_another_calendar.web.api.modeus.schema import ModeusTimeBody @@ -64,6 +64,22 @@ class LessonWebinar(BaseLesson): video_url: Optional[str] = None webinar_url: Optional[str] = None + @field_validator("starts_at") + @classmethod + def validate_starts_at(cls, starts_at: Optional[datetime.datetime], + timezone=datetime.timezone.utc) -> datetime.datetime: + if not starts_at: + return starts_at + return starts_at.astimezone(timezone) + + @field_validator("ends_at") + @classmethod + def validate_ends_at(cls, ends_at: Optional[datetime.datetime], + timezone=datetime.timezone.utc) -> datetime.datetime: + if not ends_at: + return ends_at + return ends_at.astimezone(timezone) + def is_suitable_time(self, time_min: datetime.datetime, time_max: datetime.datetime) -> bool: """Check if lesson have suitable time""" if not self.starts_at or time_min > self.starts_at: @@ -75,21 +91,27 @@ def is_suitable_time(self, time_min: datetime.datetime, time_max: datetime.datet # noinspection PyNestedDecorators class LessonTask(BaseLesson): + model_config = ConfigDict(arbitrary_types_allowed=True) + path: str + deadline: Optional[datetime] = Field(default=None) @computed_field # type: ignore @property def url(self) -> str: return urljoin(settings.netology_url, self.path) - @computed_field # type: ignore - @property - def deadline(self) -> Optional[datetime.datetime]: - match = re.search(_DATE_PATTERN, self.title) + @model_validator(mode='before') + @classmethod + def deadline_validation(self, data: Any) -> Any: + if not isinstance(data, dict): + return data + match = re.search(_DATE_PATTERN, data.get('title', '')) if not match: - return None + return data date = match.group(0) - return datetime.datetime.strptime(date, "%d.%m.%y").replace(tzinfo=datetime.timezone.utc) + data['deadline'] = datetime.datetime.strptime(date, "%d.%m.%y").astimezone(datetime.timezone.utc) + return data def is_suitable_time(self, time_min: datetime.datetime, time_max: datetime.datetime) -> bool: """Check if lesson have suitable time""" @@ -158,6 +180,16 @@ class DetailedProgram(BaseModel): start_date: datetime.datetime finish_date: datetime.datetime + @field_validator("start_date") + @classmethod + def validate_start_date(cls, start_date: datetime.datetime) -> datetime.datetime: + return start_date.astimezone(datetime.timezone.utc) + + @field_validator("finish_date") + @classmethod + def validate_finish_date(cls, finish_date: datetime.datetime) -> datetime.datetime: + return finish_date.astimezone(datetime.timezone.utc) + class Program(BaseModel): detailed_program: DetailedProgram = Field(alias='program') @@ -173,6 +205,7 @@ def get_lesson_ids(self) -> set[int]: program_ids.add(program.detailed_program.id) return program_ids + class SerializedEvents(BaseModel): """Structure for displaying frontend.""" homework: list[LessonTask] From d1fc6bc50ac6bf08b2da46bbe48f547febdb9ab1 Mon Sep 17 00:00:00 2001 From: Ivan Popov Date: Fri, 11 Oct 2024 22:35:21 +0300 Subject: [PATCH 17/31] linter fixes --- backend/yet_another_calendar/web/api/bulk/integration.py | 4 ++-- backend/yet_another_calendar/web/api/bulk/schema.py | 3 ++- backend/yet_another_calendar/web/api/modeus/schema.py | 8 ++++---- backend/yet_another_calendar/web/api/netology/schema.py | 8 ++++---- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/backend/yet_another_calendar/web/api/bulk/integration.py b/backend/yet_another_calendar/web/api/bulk/integration.py index 5ec157d..e35d8fd 100644 --- a/backend/yet_another_calendar/web/api/bulk/integration.py +++ b/backend/yet_another_calendar/web/api/bulk/integration.py @@ -62,7 +62,7 @@ async def refresh_events( jwt_token: str, calendar_id: int, cookies: netology_schema.NetologyCookies, - timezone: str + timezone: str, ) -> schema.RefreshedCalendarResponse: """Clear events cache.""" cached_json = await get_cached_calendar(body, jwt_token, calendar_id, cookies, timezone) @@ -90,7 +90,7 @@ async def get_calendar( jwt_token: str, calendar_id: int, cookies: netology_schema.NetologyCookies, - timezone: str + timezone: str, ) -> schema.CalendarResponse: try: tz = pytz.timezone(timezone) diff --git a/backend/yet_another_calendar/web/api/bulk/schema.py b/backend/yet_another_calendar/web/api/bulk/schema.py index 544b220..3f16f32 100644 --- a/backend/yet_another_calendar/web/api/bulk/schema.py +++ b/backend/yet_another_calendar/web/api/bulk/schema.py @@ -1,5 +1,6 @@ import datetime import hashlib +from typing import Self from pydantic import BaseModel, Field @@ -11,7 +12,7 @@ class BulkResponse(BaseModel): netology: netology_schema.SerializedEvents modeus: list[modeus_schema.FullEvent] - def change_timezone(self, timezone: datetime.tzinfo): + def change_timezone(self, timezone: datetime.tzinfo) -> Self: for homework in self.netology.homework: if homework.deadline: homework.deadline = homework.deadline.astimezone(timezone) diff --git a/backend/yet_another_calendar/web/api/modeus/schema.py b/backend/yet_another_calendar/web/api/modeus/schema.py index 9c69840..1b8f5f4 100644 --- a/backend/yet_another_calendar/web/api/modeus/schema.py +++ b/backend/yet_another_calendar/web/api/modeus/schema.py @@ -89,13 +89,13 @@ class Event(BaseModel): @field_validator("start_time") @classmethod def validate_starts_at(cls, start_time: datetime.datetime, - timezone=datetime.timezone.utc) -> datetime.datetime: + timezone: datetime.tzinfo = datetime.timezone.utc) -> datetime.datetime: return start_time.astimezone(timezone) @field_validator("end_time") @classmethod def validate_end_time(cls, end_time: datetime.datetime, - timezone=datetime.timezone.utc) -> datetime.datetime: + timezone: datetime.tzinfo = datetime.timezone.utc) -> datetime.datetime: return end_time.astimezone(timezone) @@ -173,14 +173,14 @@ class StudentsSpeciality(BaseModel): @field_validator("learning_start_date") @classmethod - def validate_starts_at(cls, learning_start_date: Optional[datetime.datetime]) -> datetime.datetime: + def validate_starts_at(cls, learning_start_date: Optional[datetime.datetime]) -> Optional[datetime.datetime]: if not learning_start_date: return learning_start_date return learning_start_date.astimezone(datetime.timezone.utc) @field_validator("learning_end_date") @classmethod - def validate_learning_end_date(cls, learning_end_date: Optional[datetime.datetime]) -> datetime.datetime: + def validate_learning_end_date(cls, learning_end_date: Optional[datetime.datetime]) -> Optional[datetime.datetime]: if not learning_end_date: return learning_end_date return learning_end_date.astimezone(datetime.timezone.utc) diff --git a/backend/yet_another_calendar/web/api/netology/schema.py b/backend/yet_another_calendar/web/api/netology/schema.py index f3eecdc..6f38d55 100644 --- a/backend/yet_another_calendar/web/api/netology/schema.py +++ b/backend/yet_another_calendar/web/api/netology/schema.py @@ -67,7 +67,7 @@ class LessonWebinar(BaseLesson): @field_validator("starts_at") @classmethod def validate_starts_at(cls, starts_at: Optional[datetime.datetime], - timezone=datetime.timezone.utc) -> datetime.datetime: + timezone: datetime.tzinfo = datetime.timezone.utc) -> Optional[datetime.datetime]: if not starts_at: return starts_at return starts_at.astimezone(timezone) @@ -75,7 +75,7 @@ def validate_starts_at(cls, starts_at: Optional[datetime.datetime], @field_validator("ends_at") @classmethod def validate_ends_at(cls, ends_at: Optional[datetime.datetime], - timezone=datetime.timezone.utc) -> datetime.datetime: + timezone: datetime.tzinfo = datetime.timezone.utc) -> Optional[datetime.datetime]: if not ends_at: return ends_at return ends_at.astimezone(timezone) @@ -94,7 +94,7 @@ class LessonTask(BaseLesson): model_config = ConfigDict(arbitrary_types_allowed=True) path: str - deadline: Optional[datetime] = Field(default=None) + deadline: Optional[datetime.datetime] = Field(default=None) @computed_field # type: ignore @property @@ -103,7 +103,7 @@ def url(self) -> str: @model_validator(mode='before') @classmethod - def deadline_validation(self, data: Any) -> Any: + def deadline_validation(cls, data: Any) -> Any: if not isinstance(data, dict): return data match = re.search(_DATE_PATTERN, data.get('title', '')) From f5ca2ca5a9bd4b15dfd2e55c59dd0860bfc3ad15 Mon Sep 17 00:00:00 2001 From: Ivan Popov Date: Fri, 11 Oct 2024 23:06:29 +0300 Subject: [PATCH 18/31] fix workers count --- backend/.env.dist | 3 ++- backend/yet_another_calendar/settings.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/.env.dist b/backend/.env.dist index 4c31759..5d284c2 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -1,2 +1,3 @@ MODEUS_USERNAME=test_username -MODEUS_PASSWORD=test_password \ No newline at end of file +MODEUS_PASSWORD=test_password +YET_ANOTHER_CALENDAR_WORKERS_COUNT=10 \ No newline at end of file diff --git a/backend/yet_another_calendar/settings.py b/backend/yet_another_calendar/settings.py index fb804d3..fb2fd35 100644 --- a/backend/yet_another_calendar/settings.py +++ b/backend/yet_another_calendar/settings.py @@ -34,7 +34,7 @@ class Settings(BaseSettings): host: str = "127.0.0.1" port: int = 8000 # quantity of workers for uvicorn - workers_count: int = env.int("YET_ANOTHER_WORKERS_COUNT", 1) + workers_count: int = 1 # Enable uvicorn reloading reload: bool = env.bool("YET_ANOTHER_CALENDAR_RELOAD", False) From 37abae0a1097b712c75a2ef62462b9e7c5da9d1d Mon Sep 17 00:00:00 2001 From: Ivan Popov Date: Sat, 12 Oct 2024 10:45:10 +0300 Subject: [PATCH 19/31] refactor env vars, add docker-compose.prod.yaml --- backend/.env.dist | 3 +- backend/yet_another_calendar/settings.py | 4 +- docker-compose.prod.yaml | 49 ++++++++++++++++++++++++ docker-compose.yaml | 8 ++-- 4 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 docker-compose.prod.yaml diff --git a/backend/.env.dist b/backend/.env.dist index 5d284c2..78a6ac2 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -1,3 +1,4 @@ MODEUS_USERNAME=test_username MODEUS_PASSWORD=test_password -YET_ANOTHER_CALENDAR_WORKERS_COUNT=10 \ No newline at end of file +YET_ANOTHER_CALENDAR_WORKERS_COUNT=10 +YET_ANOTHER_CALENDAR_DEBUG=False \ No newline at end of file diff --git a/backend/yet_another_calendar/settings.py b/backend/yet_another_calendar/settings.py index fb2fd35..01e0532 100644 --- a/backend/yet_another_calendar/settings.py +++ b/backend/yet_another_calendar/settings.py @@ -35,9 +35,7 @@ class Settings(BaseSettings): port: int = 8000 # quantity of workers for uvicorn workers_count: int = 1 - # Enable uvicorn reloading - reload: bool = env.bool("YET_ANOTHER_CALENDAR_RELOAD", False) - + # Enable uvicorn reloading, debug and docs debug: bool = env.bool("YET_ANOTHER_CALENDAR_DEBUG", False) log_level: LogLevel = LogLevel.INFO diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml new file mode 100644 index 0000000..b6b2371 --- /dev/null +++ b/docker-compose.prod.yaml @@ -0,0 +1,49 @@ +services: + api: &main_app + ports: + - "127.0.0.1:8000:8000" + container_name: yet_another_calendar-api + image: ghcr.io/azamatkomaev/yet_another_calendar_backend:${YET_ANOTHER_CALENDAR_VERSION:-latest} + restart: always + volumes: + - ./backend:/app/src/ + env_file: + - backend/.env + depends_on: + redis: + condition: service_healthy + environment: + YET_ANOTHER_CALENDAR_HOST: 0.0.0.0 + YET_ANOTHER_CALENDAR_REDIS_HOST: yet_another_calendar-redis + LOGURU_DIAGNOSE: "False" + + redis: + image: redis:latest + hostname: "yet_another_calendar-redis" + restart: always + volumes: + - redis_data:/data + environment: + ALLOW_EMPTY_PASSWORD: "yes" + healthcheck: + test: redis-cli ping + interval: 1s + timeout: 3s + retries: 50 + + frontend: + image: ghcr.io/azamatkomaev/yet_another_calendar_frontend:${YET_ANOTHER_CALENDAR_VERSION:-latest} + build: + context: ./frontend + target: dev-run + container_name: yet_another_calendar-frontend + env_file: + - frontend/.env + restart: always + volumes: + - ./frontend/src:/app/src + ports: + - "127.0.0.1:3000:3000" + +volumes: + redis_data: diff --git a/docker-compose.yaml b/docker-compose.yaml index 0aa9691..39bc9dd 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,7 @@ services: api: &main_app ports: - - "127.0.0.1:8000:8000" + - "8000:8000" build: context: backend/ dockerfile: Dockerfile @@ -17,7 +17,7 @@ services: environment: YET_ANOTHER_CALENDAR_HOST: 0.0.0.0 YET_ANOTHER_CALENDAR_REDIS_HOST: yet_another_calendar-redis - LOGURU_DIAGNOSE: "False" + YET_ANOTHER_CALENDAR_DEBUG: True redis: image: redis:latest @@ -36,7 +36,7 @@ services: frontend: build: context: ./frontend - target: dev-run + target: prod-run image: ghcr.io/azamatkomaev/yet_another_calendar_frontend:${YET_ANOTHER_CALENDAR_VERSION:-latest} container_name: calendar-frontend env_file: @@ -45,7 +45,7 @@ services: volumes: - ./frontend/src:/app/src ports: - - "127.0.0.1:3000:3000" + - "3000:3000" volumes: redis_data: From 73b6376ba56b05727714dafa786b0f1adcd6d04f Mon Sep 17 00:00:00 2001 From: Ivan Popov Date: Sat, 12 Oct 2024 10:52:00 +0300 Subject: [PATCH 20/31] refactor docker compose --- docker-compose.prod.yaml | 2 +- docker-compose.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml index b6b2371..ec5b80b 100644 --- a/docker-compose.prod.yaml +++ b/docker-compose.prod.yaml @@ -35,7 +35,7 @@ services: image: ghcr.io/azamatkomaev/yet_another_calendar_frontend:${YET_ANOTHER_CALENDAR_VERSION:-latest} build: context: ./frontend - target: dev-run + target: prod-run container_name: yet_another_calendar-frontend env_file: - frontend/.env diff --git a/docker-compose.yaml b/docker-compose.yaml index 39bc9dd..448f9a5 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -36,7 +36,7 @@ services: frontend: build: context: ./frontend - target: prod-run + target: dev-run image: ghcr.io/azamatkomaev/yet_another_calendar_frontend:${YET_ANOTHER_CALENDAR_VERSION:-latest} container_name: calendar-frontend env_file: From 28f9d3222ae451f4996a440567253109087c31c5 Mon Sep 17 00:00:00 2001 From: Ivan Popov Date: Sat, 12 Oct 2024 10:53:44 +0300 Subject: [PATCH 21/31] refactor docker compose --- docker-compose.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 448f9a5..6702acb 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -5,7 +5,7 @@ services: build: context: backend/ dockerfile: Dockerfile - image: ghcr.io/azamatkomaev/yet_another_calendar_backend:${YET_ANOTHER_CALENDAR_VERSION:-latest} + image: ghcr.io/azamatkomaev/yet_another_calendar_backend:${YET_ANOTHER_CALENDAR_VERSION:-dev} restart: always volumes: - ./backend:/app/src/ @@ -37,7 +37,7 @@ services: build: context: ./frontend target: dev-run - image: ghcr.io/azamatkomaev/yet_another_calendar_frontend:${YET_ANOTHER_CALENDAR_VERSION:-latest} + image: ghcr.io/azamatkomaev/yet_another_calendar_frontend:${YET_ANOTHER_CALENDAR_VERSION:-dev} container_name: calendar-frontend env_file: - frontend/.env From f5d25f5341182d065688bced2d9b41b41dcffb9a Mon Sep 17 00:00:00 2001 From: Ivan Popov Date: Sat, 12 Oct 2024 10:58:33 +0300 Subject: [PATCH 22/31] refactor docker compose --- docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 6702acb..b989878 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -45,7 +45,7 @@ services: volumes: - ./frontend/src:/app/src ports: - - "3000:3000" + - "3000:80" volumes: redis_data: From 5ff45c4c2819f4d016f7caafce85ec6600526b2b Mon Sep 17 00:00:00 2001 From: Ivan <60302361+depocoder@users.noreply.github.com> Date: Sat, 12 Oct 2024 11:00:07 +0300 Subject: [PATCH 23/31] Delete backend/requirements.txt --- backend/requirements.txt | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 backend/requirements.txt diff --git a/backend/requirements.txt b/backend/requirements.txt deleted file mode 100644 index 1e98b42..0000000 --- a/backend/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -blacksheep[full]~=2.0.0 -uvicorn==0.22.0 -pydantic-settings -MarkupSafe==2.1.3 -pydantic From 5a228275e21bedfb31db63dc8f3f83bc406207fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B0=D1=80=D0=BF=D0=BE=D0=B2=D0=B8=D1=87=20=D0=90?= =?UTF-8?q?=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Sun, 13 Oct 2024 12:12:17 +0300 Subject: [PATCH 24/31] feat: create app page and add took some logic from index page, del useContext, get data calendar page, add img folder --- frontend/src/App.jsx | 69 +++++++ frontend/src/components/Calendar/Calendar.jsx | 111 +++++++++-- frontend/src/components/Header/Header.js | 32 ++-- frontend/src/context/AuthContext.js | 19 -- frontend/src/img/Shape-reverse.png | Bin 0 -> 228 bytes frontend/src/img/arrow.png | Bin 0 -> 224 bytes frontend/src/img/camera.png | Bin 0 -> 193 bytes frontend/src/img/cross.png | Bin 0 -> 244 bytes frontend/src/img/left-week.png | Bin 0 -> 226 bytes frontend/src/img/right-week.png | Bin 0 -> 228 bytes frontend/src/index.js | 37 +--- frontend/src/pages/CalendarRoute.jsx | 176 ++++++------------ frontend/src/pages/LoginRoute.jsx | 115 +++++------- frontend/src/services/api.js | 6 +- 14 files changed, 294 insertions(+), 271 deletions(-) create mode 100644 frontend/src/App.jsx delete mode 100644 frontend/src/context/AuthContext.js create mode 100644 frontend/src/img/Shape-reverse.png create mode 100644 frontend/src/img/arrow.png create mode 100644 frontend/src/img/camera.png create mode 100644 frontend/src/img/cross.png create mode 100644 frontend/src/img/left-week.png create mode 100644 frontend/src/img/right-week.png diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..fdd7a31 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,69 @@ +import React, { useState } from 'react'; +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import LoginRoute from './pages/LoginRoute'; +import CalendarRoute from './pages/CalendarRoute'; +import { loginModeus, searchModeus } from './services/api'; // Ваши API-запросы + +const App = () => { + const [authData, setAuthData] = useState({ + email: '', + password: '', + personId: '' + }); + + // Функция для обработки логина + const handleLogin = async (email, password, personId) => { + console.log('handleLogin personId', personId) + try { + let response = await loginModeus(email, password); + + console.log('response handleLogin', response) + + if (response.status === 200) { + setAuthData({ email, password, personId }); + localStorage.setItem('token', response.data["_netology-on-rails_session"]); + return { success: true }; + } else { + return { success: false, message: "Неверный логин или пароль." }; + } + } catch (error) { + return { success: false, message: "Произошла ошибка. Попробуйте снова." }; + } + }; + + // Функция для поиска пользователя по ФИО + const handleSearch = async (fullName) => { + try { + let response = await searchModeus(fullName); + + if (response.status === 200) { + return { success: true, data: response.data }; + } else { + return { success: false, message: "Неверное ФИО. Попробуйте снова." }; + } + } catch (error) { + return { success: false, message: "Произошла ошибка. Попробуйте снова." }; + } + }; + + return ( + + + } /> + + } + /> + + + ); +}; + +export default App; diff --git a/frontend/src/components/Calendar/Calendar.jsx b/frontend/src/components/Calendar/Calendar.jsx index f5934ee..84ef2ee 100644 --- a/frontend/src/components/Calendar/Calendar.jsx +++ b/frontend/src/components/Calendar/Calendar.jsx @@ -1,19 +1,98 @@ -import flatpickr from "flatpickr"; -import { useCallback, useRef } from "react"; - -const Calendar = () => { - const fp1 = useRef(); - - const inputRef = useCallback((node) => { - if (node !== null) { - fp1.current = flatpickr(node, { - enableTime: true, - dateFormat: "j, F", - }); - } - }, []); - - return ; +import React from 'react'; +import arrow from "../../img/cross.png"; +import cross from "../../img/arrow.png"; +// import camera from "../../img/camera.png"; + +const Calendar = ({ events }) => { + if (!events) { + return
Нет данных для отображения.
; + } + + const netology = events?.netology; + const modeus = events?.modeus; + + return ( +
+
+
+ Мое расписание + + +
+ +
+ + {/* Информация о дедлайнах */} + {netology?.homework?.length > 0 && ( +
+
+ Дедлайн Нетология{" "} + + arrow{" "} + {new Date(netology.homework[0].deadline).toLocaleDateString()} + +
+
+ {netology.homework[0].title} +
+
+ + Домашнее задание с самопроверкой (дедлайн{" "} + {new Date(netology.homework[0].deadline).toLocaleDateString()}) + +
+
+ )} + +
+ + {/* Таблица расписания */} + + + + + + + + + + + + + + + {/* Дедлайны */} + + + + + + + + + + + + {/* Проход по урокам из modeus */} + {modeus?.map((lesson, index) => ( + + + + + + + + + + + ))} + +
ПнВтСрЧтПтСбВс
Дедлайны{/* Дедлайны для Пн */}{/* Дедлайны для Вт */}{/* Дедлайны для Ср */}{/* Дедлайны для Чт */}{/* Дедлайны для Пт */}{/* Дедлайны для Сб */}{/* Дедлайны для Вс */}
{lesson.nameShort}{/* Уроки на Пн */}{/* Уроки на Вт */}{/* Уроки на Ср */}{/* Уроки на Чт */}{/* Уроки на Пт */}{/* Уроки на Сб */}{/* Уроки на Вс */}
+
+ ); }; export default Calendar; diff --git a/frontend/src/components/Header/Header.js b/frontend/src/components/Header/Header.js index 1ce3a7f..a48e36b 100644 --- a/frontend/src/components/Header/Header.js +++ b/frontend/src/components/Header/Header.js @@ -1,12 +1,12 @@ import React from "react"; -import { useNavigate } from "react-router-dom"; +// import { useNavigate } from "react-router-dom"; import cross from "./cross.png"; import arrow from "./arrow.png" import camera from "./camera.png" export default function Header() { - const navigate = useNavigate(); + // const navigate = useNavigate(); return (
@@ -17,12 +17,12 @@ export default function Header() {
- Дедлайн Нетология 23.09.2024 + Дедлайн Нетология {arrow} 23.09.2024
Программирование на Python @@ -68,7 +68,7 @@ export default function Header() { @@ -80,7 +80,7 @@ export default function Header() { @@ -91,7 +91,7 @@ export default function Header() { @@ -99,15 +99,15 @@ export default function Header() { 5 пара
15:45 17:15 @@ -115,14 +115,14 @@ export default function Header() { 6 пара
17:30 19:00 @@ -132,13 +132,13 @@ export default function Header() { 7 пара
19:10 20:40 diff --git a/frontend/src/context/AuthContext.js b/frontend/src/context/AuthContext.js deleted file mode 100644 index b2d980e..0000000 --- a/frontend/src/context/AuthContext.js +++ /dev/null @@ -1,19 +0,0 @@ -// AuthContext.js -import React, { createContext, useState } from 'react'; - -// Создаем контекст -export const AuthContext = createContext(); - -// Создаем провайдер -export const AuthProvider = ({ children }) => { - const [authData, setAuthData] = useState({ - email: null, - password: null, - }); - - return ( - - {children} - - ); -}; \ No newline at end of file diff --git a/frontend/src/img/Shape-reverse.png b/frontend/src/img/Shape-reverse.png new file mode 100644 index 0000000000000000000000000000000000000000..0ae2959721bac5a2ec2cea952e249dbf5607b04a GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^Y(UJx0U~c5>$3n-oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBex;|2V@L&KYWG3jW&dHX6hiNO(h0m}UA&k3vqog$ z0fyeKU)1J!thDOdB0h0qivA&o*@Z9WAAR_}{nYE*rSoe{EJOx$0z V^Zu`k-UW07gQu&X%Q~loCIH;4~pEG*09DT5}X48qgmlD*D{5Y)Z@gynD_U=``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBewe3=V@L&KZSQH`1_c2ZN$19C45AYKb?c63 z%X)BbOLBdjbc37mUS`>6qvpetmpSifJX^R=NN#n1)G5~G=~Je;`uU_eOss2LG$U~F k)tS@8g|6NGc=ji=>j^1FVYQFvfHp9Ay85}Sb4q9e0QV(4EC2ui literal 0 HcmV?d00001 diff --git a/frontend/src/img/cross.png b/frontend/src/img/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..39835f3e7cfc66821657d21e86531798aab3614c GIT binary patch literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqoCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBey694V@L&~Z@(koVFdx^y(()f*xZ+Bb?85N z`jg2$@1g8krlYq5HZGs~*0ecCan6g)5}TJ#;rcGIcz2fVj?>xP%NhJwS{1uYqMNfF zBG;dV-@NNv^f^Ld@KDXiYa laP8L2%m2^L_B{Aoai3IfXsC~BTPe^{44$rjF6*2UngF>>Rfzxq literal 0 HcmV?d00001 diff --git a/frontend/src/img/left-week.png b/frontend/src/img/left-week.png new file mode 100644 index 0000000000000000000000000000000000000000..58b6e88d1ab05afdba6b929a206f91424b1420b2 GIT binary patch literal 226 zcmeAS@N?(olHy`uVBq!ia0vp^Y(UJx0U~c5>$3n-oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBez~WMV@L&KZl57ng8`50$3n-oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBex;|2V@L&KYWG3jW&dHX6hiNO(h0m}UA&k3vqog$ z0fyeKU)1J!thDOdB0h0qivA&o*@Z9WAAR_}{nYE*rSoe{EJOx$0z V^Zu`k-UW07gQu&X%Q~loCIH { - // Проверка наличия токена в localStorage - return localStorage.getItem("token"); -}; +import App from "./App"; const root = ReactDOM.createRoot(document.getElementById("root")); -const router = createBrowserRouter([ - { - path: "/", - element: checkAuth() ? : , - // Если токен есть — перенаправляем на /calendar, если нет — на /login - }, - { - path: "/login", - element: checkAuth() ? : , - // Перенаправление на календарь, если уже залогинен - }, - { - path: "/calendar", - element: ( - - - - ), // Защищаем страницу календаря - }, -]); - root.render( - {/* Оборачиваем в AuthProvider для контекста */} - - + ); diff --git a/frontend/src/pages/CalendarRoute.jsx b/frontend/src/pages/CalendarRoute.jsx index aca706a..3c10107 100644 --- a/frontend/src/pages/CalendarRoute.jsx +++ b/frontend/src/pages/CalendarRoute.jsx @@ -1,118 +1,64 @@ -import React, {useContext, useEffect, useState} from "react"; -import { getNetologyCourse, bulkEvents } from "../services/api"; -import {AuthContext} from "../context/AuthContext"; // Импортируем API функции - -const CalendarRoute = () => { - const { authData } = useContext(AuthContext); // Достаем данные из контекста - - const [calendarId, setCalendarId] = useState(null); // Хранение calendarId - const [courses, setCourses] = useState({ homework: [], webinars: [] }); // Для хранения курсов - const [events, setEvents] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - console.log("Данные пользователя:", authData); - - const fetchCourseAndEvents = async () => { - const sessionToken = localStorage.getItem("token"); // Получаем токен из localStorage - - try { - // Получаем данные курса, чтобы извлечь calendarId - const courseData = await getNetologyCourse(sessionToken); - const fetchedCalendarId = courseData?.id; // Предполагаем, что calendarId есть в данных курса - setCalendarId(fetchedCalendarId); - - // Сохраняем курсы (домашние задания и вебинары) - if (courseData?.netology) { - setCourses(courseData.netology); - } - - - if (fetchedCalendarId) { - // дату получаем события для календаря - const eventsResponse = await bulkEvents( - authData.email, // username - authData.password, // password - sessionToken, // Токен сессии - fetchedCalendarId, // Извлеченный ID календаря - "2024-10-07T00:00:00+03:00", // Начало диапазона дат - "2024-10-13T23:59:59+03:00", // Конец диапазона дат - authData.personId // ID участника - ); - - setEvents(eventsResponse.data); // Записываем события в состояние - } - } catch (error) { - setError("Ошибка получения данных с сервера"); - } finally { - setLoading(false); - } - }; - - fetchCourseAndEvents(); - }, [authData]); - - if (loading) { - return
Загрузка...
; - } - - if (error) { - return
{error}
; - } - - return ( -
-

Календарь

- {calendarId ?

ID Календаря: {calendarId}

:

Календарь не найден

} - -

События:

- {events.length > 0 ? ( -
    - {events.map((event, index) => ( -
  • - {event.title} — {event.date} -
  • - ))} -
- ) : ( -

Нет доступных событий

- )} - -

Курсы

-

Домашние задания

- {courses.homework.length > 0 ? ( - - ) : ( -

Нет домашних заданий

- )} - -

Вебинары

- {courses.webinars.length > 0 ? ( - - ) : ( -

Нет вебинаров

- )} -
- ); +import React, { useEffect, useState } from 'react'; +import { getNetologyCourse, bulkEvents } from '../services/api'; // Ваши API-запросы +import Calendar from "../components/Calendar/Calendar"; + +const CalendarRoute = ({ email, password, personId, token }) => { + console.log('Полученные данные:', { email, password, personId, token }); + const [events, setEvents] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + + useEffect(() => { + const fetchCourseAndEvents = async () => { + if (!token || !email || !password || !personId) { + console.error('Ошибка авторизации. Проверьте введенные данные.'); + return; + } + + try { + const courseData = await getNetologyCourse(token); + console.log('Данные курса:', courseData); // Проверьте, что курс получен + + const fetchedCalendarId = courseData?.id; + + if (fetchedCalendarId) { + const eventsResponse = await bulkEvents( + email, // Email пользователя + password, // Пароль пользователя + token, // Токен сессии + fetchedCalendarId, // ID календаря + "2024-10-07T00:00:00+03:00", // Дата начала + "2024-10-13T23:59:59+03:00", // Дата окончания + personId // ID участника + ); + console.log('События:', eventsResponse.data); // Проверьте, что события получены + setEvents(eventsResponse.data); + } + } catch (error) { + console.error('Ошибка при получении данных с сервера:', error); + setError("Ошибка при получении данных с сервера."); + } finally { + setLoading(false); + } + }; + + fetchCourseAndEvents(); + }, [email, password, personId, token]); + + if (loading) { + return
Загрузка данных...
; + } + + if (error) { + return
{error}
; + } + + return ( +
+ +
+ ); }; export default CalendarRoute; - - diff --git a/frontend/src/pages/LoginRoute.jsx b/frontend/src/pages/LoginRoute.jsx index b07cd50..e40e339 100644 --- a/frontend/src/pages/LoginRoute.jsx +++ b/frontend/src/pages/LoginRoute.jsx @@ -1,82 +1,66 @@ -import {useContext, useState} from "react"; -import {useNavigate} from "react-router-dom"; -import {AuthContext} from "../context/AuthContext"; -import {loginModeus, searchModeus} from "../services/api"; +import {useEffect, useState} from "react"; +import { useNavigate } from "react-router-dom"; -const LoginRoute = () => { - const {setAuthData} = useContext(AuthContext); // Достаем setAuthData из контекста - - const [email, setEmail] = useState(null); - const [password, setPassword] = useState(null); +const LoginRoute = ({ onLogin, onSearch }) => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); const [fullName, setFullName] = useState(""); // Строка для поиска const [searchResults, setSearchResults] = useState([]); // Результаты поиска - // const [selectedName, setSelectedName] = useState(""); // Выбранное имя - const [personId, setPersonId] = useState(null); // ID выбранного человека + const [personId, setPersonId] = useState(null); // Здесь сохраняем personId const [showSuggestions, setShowSuggestions] = useState(false); // Флаг показа списка const [errorMessage, setErrorMessage] = useState(""); // Сообщение об ошибке + const [debounceTimeout, setDebounceTimeout] = useState(null); // Для хранения таймера - const navigate = useNavigate(); // Инициализируем хук для навигации + const navigate = useNavigate(); // Инициализируем хук для навигации + // Функция для выполнения поиска const onClickSearch = async (fullName) => { - console.log("Поиск ФИО:", fullName); - - let response = await searchModeus(fullName); - if (response.status !== 200) { - setErrorMessage("Неверное ФИО. Попробуйте еще раз."); - return; + const result = await onSearch(fullName); + if (result.success) { + setSearchResults(result.data); + setShowSuggestions(true); // Показываем список после поиска + setErrorMessage(""); // Очищаем ошибку при успешном поиске + } else { + setErrorMessage(result.message); } - console.log("Результаты поиска:", response.data); - setSearchResults(response.data); - setShowSuggestions(true); // Показываем список после поиска - setErrorMessage(""); // Очищаем ошибку при успешном поиске }; - /// Обработчик нажатия клавиши "Enter" - const handleKeyPress = (e) => { - if (e.key === "Enter") { - onClickSearch(fullName); // Выполнить поиск, если нажата клавиша Enter + // Обрабатываем изменение поля поиска с задержкой + useEffect(() => { + // Очищаем предыдущий таймер, чтобы избежать лишних вызовов + if (debounceTimeout) { + clearTimeout(debounceTimeout); } - }; + + // Устанавливаем новый таймер на 500 мс + const newTimeout = setTimeout(() => { + if (fullName.trim()) { + onClickSearch(fullName); // Выполняем поиск после задержки + } + }, 500); + + setDebounceTimeout(newTimeout); + + // Очищаем таймер при размонтировании или изменении fullName + return () => clearTimeout(newTimeout); + }, [fullName]); // Обработчик выбора варианта из списка const handleSelect = (person) => { - console.log('person', person) setFullName(person.fullName); // Устанавливаем выбранное имя - setPersonId(person.personId); - - // setAuthData((prev) => ({ - // person: person, - // personId: personId, // Сохраняем personId в контекст - // ...prev, - // })); - - // setAuthData({ person }); + setPersonId(person.personId); // Сохраняем personId setShowSuggestions(false); // Скрываем список после выбора }; const onClickLogin = async () => { - let response = await loginModeus(email, password); - // console.log('email', email) - // console.log('password', password) + const result = await onLogin(email, password, personId); - if (response.status !== 200) { - setErrorMessage("Неверный логин или пароль. Попробуйте еще раз."); // Устанавливаем текст ошибки - return; + if (result.success) { + setErrorMessage(""); // Очищаем ошибку при успешном логине + navigate("/calendar"); + } else { + setErrorMessage(result.message); } - - console.log('setAuthData получил', setAuthData) - - // Set email, password, and personId in the AuthContext - setAuthData({ email, password }); - - console.log('setAuthData передал ', setAuthData) - - localStorage.setItem("token", response.data["_netology-on-rails_session"]); - setErrorMessage(""); // Очищаем ошибку при успешном логине - - // Перенаправление на страницу календаря - navigate("/calendar"); - window.location.reload(); // Обновляем страницу после навигации }; return ( @@ -89,12 +73,10 @@ const LoginRoute = () => {
setFullName(e.target.value)} // Обновляем строку поиска - onKeyPress={handleKeyPress} // Обработчик для нажатия клавиш + value={fullName} + onChange={(e) => setFullName(e.target.value)} /> {/* Рендерим выпадающий список или сообщение об отсутствии результатов */} @@ -103,31 +85,28 @@ const LoginRoute = () => { {searchResults.length > 0 ? ( searchResults.map((person, index) => (
  • handleSelect(person)}> - {person.fullName} {/* Отображаем имя */} + {person.fullName}
  • )) ) : ( -
  • Нет такого имени
  • // Сообщение, если список пуст +
  • Нет такого имени
  • )} )}
    -
    setEmail(e.target.value)} /> setPassword(e.target.value)} @@ -142,6 +121,6 @@ const LoginRoute = () => {
    ); -} +}; -export default LoginRoute +export default LoginRoute; diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 388f054..9793c54 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -2,10 +2,9 @@ import axios from 'axios'; // env variable const BACKEND_URL = process.env.REACT_APP_BACKEND_URL; +// const BACKEND_URL = 'http://localhost:8000'; -console.log("Backend URL:", BACKEND_URL); - export function getTokenFromLocalStorage() { return localStorage.getItem('token') } @@ -29,6 +28,7 @@ export async function searchModeus(fullName) { return e.response; } } + // calendar_id export async function getNetologyCourse(sessionToken) { console.log('sessionToken', sessionToken) @@ -44,6 +44,7 @@ export async function getNetologyCourse(sessionToken) { return e.response; } } + // calendar export async function bulkEvents(username, password, sessionToken, calendarId, timeMin, timeMax, attendeePersonId) { try { @@ -67,6 +68,7 @@ export async function bulkEvents(username, password, sessionToken, calendarId, t return e.response; } } + // Refresh calendar export async function refreshBulkEvents(sessionToken, calendarId, timeMin, timeMax, attendeePersonId) { try { From d930074d0b18e8efbe763b68858f4d514d0c4f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B0=D1=80=D0=BF=D0=BE=D0=B2=D0=B8=D1=87=20=D0=90?= =?UTF-8?q?=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Sun, 13 Oct 2024 22:46:21 +0300 Subject: [PATCH 25/31] feat: calendar ui --- frontend/package-lock.json | 357 ++++++++++++- frontend/package.json | 3 +- frontend/src/App.jsx | 116 ++--- frontend/src/components/Calendar/Calendar.jsx | 243 +++++---- .../src/components/Calendar/PrivateRoute.jsx | 17 +- .../src/components/Calendar/left-week.png | Bin 226 -> 0 bytes .../src/components/Calendar/right-week.png | Bin 228 -> 0 bytes frontend/src/components/Header/Header.js | 10 +- .../src/components/Header/Shape-reverse.png | Bin 228 -> 0 bytes frontend/src/components/Header/arrow.png | Bin 224 -> 0 bytes frontend/src/components/Header/camera.png | Bin 193 -> 0 bytes frontend/src/components/Header/cross.png | Bin 244 -> 0 bytes frontend/src/components/Header/left-week.png | Bin 226 -> 0 bytes frontend/src/index.scss | 468 +----------------- frontend/src/logo.svg | 1 - frontend/src/pages/CalendarRoute.jsx | 34 +- frontend/src/pages/LoginRoute.jsx | 6 +- frontend/src/style/calendar.scss | 255 ++++++++++ frontend/src/style/header.scss | 231 +++++++++ frontend/src/style/login.scss | 0 20 files changed, 1106 insertions(+), 635 deletions(-) delete mode 100644 frontend/src/components/Calendar/left-week.png delete mode 100644 frontend/src/components/Calendar/right-week.png delete mode 100644 frontend/src/components/Header/Shape-reverse.png delete mode 100644 frontend/src/components/Header/arrow.png delete mode 100644 frontend/src/components/Header/camera.png delete mode 100644 frontend/src/components/Header/cross.png delete mode 100644 frontend/src/components/Header/left-week.png delete mode 100644 frontend/src/logo.svg create mode 100644 frontend/src/style/calendar.scss create mode 100644 frontend/src/style/header.scss create mode 100644 frontend/src/style/login.scss diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0ef13d3..1808cd1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,10 +20,11 @@ "react-router": "^6.26.2", "react-router-dom": "^6.26.2", "react-scripts": "5.0.1", + "sass-loader": "^16.0.2", "web-vitals": "^2.1.4" }, "devDependencies": { - "sass": "^1.79.4", + "sass": "^1.79.5", "scss-reset": "^1.4.2" } }, @@ -3255,6 +3256,279 @@ "node": ">= 8" } }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "devOptional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -7073,6 +7347,18 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "devOptional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -12903,6 +13189,12 @@ "tslib": "^2.0.3" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "devOptional": true + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -15314,6 +15606,43 @@ } } }, + "node_modules/react-scripts/node_modules/sass-loader": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", + "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", + "dependencies": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -15822,12 +16151,12 @@ "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==" }, "node_modules/sass": { - "version": "1.79.4", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.4.tgz", - "integrity": "sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg==", + "version": "1.79.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz", + "integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==", "devOptional": true, - "license": "MIT", "dependencies": { + "@parcel/watcher": "^2.4.1", "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" @@ -15840,29 +16169,28 @@ } }, "node_modules/sass-loader": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", - "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.2.tgz", + "integrity": "sha512-Ll6iXZ1EYwYT19SqW4mSBb76vSSi8JgzElmzIerhEGgzB5hRjDQIWsPmuk1UrAXkR16KJHqVY0eH+5/uw9Tmfw==", "dependencies": { - "klona": "^2.0.4", "neo-async": "^2.6.2" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", "sass": "^1.3.0", "sass-embedded": "*", "webpack": "^5.0.0" }, "peerDependenciesMeta": { - "fibers": { + "@rspack/core": { "optional": true }, "node-sass": { @@ -15873,6 +16201,9 @@ }, "sass-embedded": { "optional": true + }, + "webpack": { + "optional": true } } }, diff --git a/frontend/package.json b/frontend/package.json index 5af75bd..91eb7a4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "react-router": "^6.26.2", "react-router-dom": "^6.26.2", "react-scripts": "5.0.1", + "sass-loader": "^16.0.2", "web-vitals": "^2.1.4" }, "scripts": { @@ -42,7 +43,7 @@ ] }, "devDependencies": { - "sass": "^1.79.4", + "sass": "^1.79.5", "scss-reset": "^1.4.2" } } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index fdd7a31..caa3691 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,69 +1,71 @@ -import React, { useState } from 'react'; -import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import React, {useState} from 'react'; +import {BrowserRouter as Router, Routes, Route, Navigate} from 'react-router-dom'; import LoginRoute from './pages/LoginRoute'; import CalendarRoute from './pages/CalendarRoute'; -import { loginModeus, searchModeus } from './services/api'; // Ваши API-запросы +import {loginModeus, searchModeus} from './services/api'; +import PrivateRoute from "./components/Calendar/PrivateRoute"; // Ваши API-запросы const App = () => { - const [authData, setAuthData] = useState({ - email: '', - password: '', - personId: '' - }); + const [authData, setAuthData] = useState({ + email: '', + password: '', + personId: '' + }); - // Функция для обработки логина - const handleLogin = async (email, password, personId) => { - console.log('handleLogin personId', personId) - try { - let response = await loginModeus(email, password); + // Функция для обработки логина + const handleLogin = async (email, password, personId) => { + try { + let response = await loginModeus(email, password); - console.log('response handleLogin', response) + if (response.status === 200) { + setAuthData({email, password, personId}); + localStorage.setItem('token', response.data["_netology-on-rails_session"]); + return {success: true}; + } else { + return {success: false, message: "Неверный логин или пароль."}; + } + } catch (error) { + return {success: false, message: "Произошла ошибка. Попробуйте снова."}; + } + }; - if (response.status === 200) { - setAuthData({ email, password, personId }); - localStorage.setItem('token', response.data["_netology-on-rails_session"]); - return { success: true }; - } else { - return { success: false, message: "Неверный логин или пароль." }; - } - } catch (error) { - return { success: false, message: "Произошла ошибка. Попробуйте снова." }; - } - }; + // Функция для поиска пользователя по ФИО + const handleSearch = async (fullName) => { + try { + let response = await searchModeus(fullName); - // Функция для поиска пользователя по ФИО - const handleSearch = async (fullName) => { - try { - let response = await searchModeus(fullName); + if (response.status === 200) { + return {success: true, data: response.data}; + } else { + return {success: false, message: "Неверное ФИО. Попробуйте снова."}; + } + } catch (error) { + return {success: false, message: "Произошла ошибка. Попробуйте снова."}; + } + }; - if (response.status === 200) { - return { success: true, data: response.data }; - } else { - return { success: false, message: "Неверное ФИО. Попробуйте снова." }; - } - } catch (error) { - return { success: false, message: "Произошла ошибка. Попробуйте снова." }; - } - }; - - return ( - - - } /> - - } - /> - - - ); + return ( + + + }/> + + + + } + /> + {/* Дефолтный маршрут, который перенаправляет на /login */} + } /> + + + ); }; export default App; diff --git a/frontend/src/components/Calendar/Calendar.jsx b/frontend/src/components/Calendar/Calendar.jsx index 84ef2ee..d8f026e 100644 --- a/frontend/src/components/Calendar/Calendar.jsx +++ b/frontend/src/components/Calendar/Calendar.jsx @@ -3,96 +3,171 @@ import arrow from "../../img/cross.png"; import cross from "../../img/arrow.png"; // import camera from "../../img/camera.png"; -const Calendar = ({ events }) => { - if (!events) { - return
    Нет данных для отображения.
    ; - } +import '../../style/header.scss'; +import '../../style/calendar.scss'; - const netology = events?.netology; - const modeus = events?.modeus; +const Calendar = ({events}) => { + console.log('events', events) + if (!events) { + return
    Нет данных для отображения.
    ; + } - return ( -
    -
    -
    - Мое расписание - - -
    - -
    + const modeus = events?.modeus; + const netology = events?.netology; + const homework = netology?.homework; + const webinars = netology?.webinars; + console.log('modeus', modeus) + console.log('netology', netology) + console.log('webinars', webinars) + console.log('homework', homework) - {/* Информация о дедлайнах */} - {netology?.homework?.length > 0 && ( -
    -
    - Дедлайн Нетология{" "} - - arrow{" "} - {new Date(netology.homework[0].deadline).toLocaleDateString()} - -
    -
    - {netology.homework[0].title} -
    -
    - - Домашнее задание с самопроверкой (дедлайн{" "} - {new Date(netology.homework[0].deadline).toLocaleDateString()}) - -
    -
    - )} + // // Функция для получения дня недели (0 = Воскресенье, 6 = Суббота) + // const getDayOfWeek = (dateString) => { + // return new Date(dateString).getDay(); + // }; + // + // // Массив для распределения занятий по дням недели + // const weekDays = Array(7).fill([]); // 0 - Воскресенье, 1 - Понедельник и т.д. + // + // // Распределение занятий по дням недели + // modeus?.forEach((lesson) => { + // const dayOfWeek = getDayOfWeek(lesson.start); + // weekDays[dayOfWeek] = [...weekDays[dayOfWeek], lesson]; // Добавляем занятия в соответствующий день недели + // }); + + // Function to get the day of the week (0 = Sunday, 6 = Saturday) + const getDayOfWeek = (dateString) => { + return new Date(dateString).getDay(); + }; + + // Create an array for each day of the week + const weekDays = Array(7).fill([]); // 0 - Sunday, 1 - Monday, and so on + + // Distribute modeus events across the week by day of the week + modeus?.forEach((lesson) => { + const dayOfWeek = getDayOfWeek(lesson.start); + weekDays[dayOfWeek] = [...weekDays[dayOfWeek], lesson]; + }); -
    + return ( +
    +
    +
    +
    + Мое расписание + + +
    + +
    - {/* Таблица расписания */} - - - - - - - - - - - - - - - {/* Дедлайны */} - - - - - - - - - - + {/* Информация о дедлайнах */} + + - {/* Проход по урокам из modeus */} - {modeus?.map((lesson, index) => ( - - - - - - - - - - - ))} - -
    ПнВтСрЧтПтСбВс
    Дедлайны{/* Дедлайны для Пн */}{/* Дедлайны для Вт */}{/* Дедлайны для Ср */}{/* Дедлайны для Чт */}{/* Дедлайны для Пт */}{/* Дедлайны для Сб */}{/* Дедлайны для Вс */}
    {lesson.nameShort}{/* Уроки на Пн */}{/* Уроки на Вт */}{/* Уроки на Ср */}{/* Уроки на Чт */}{/* Уроки на Пт */}{/* Уроки на Сб */}{/* Уроки на Вс */}
    -
    - ); +
    +
    + + {/* Таблица расписания */} + + + + + + + + + + + + + + + {/* Дедлайны */} + + + + + + + + + + + + {/*/!* Проход по урокам из modeus *!/*/} + {/*{modeus?.map((lesson, index) => (*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/*))}*/} + + {weekDays.map((lessons, dayIndex) => ( + + + {lessons.length > 0 ? ( + lessons.map((lesson, index) => ( + + )) + ) : ( + + )} + + ))} + +
    ПнВтСрЧтПтСбВс
    Дедлайны{/* Дедлайны для Пн */}{/* Дедлайны для Вт */}{/* Дедлайны для Ср */}{/* Дедлайны для Чт */}{/* Дедлайны для Пт */}{/* Дедлайны для Сб */}{/* Дедлайны для Вс */}
    {lesson.nameShort}/!* Уроки на Пн *!//!* Уроки на Вт *!//!* Уроки на Ср *!//!* Уроки на Чт *!//!* Уроки на Пт *!//!* Уроки на Сб *!//!* Уроки на Вс *!/
    {["Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб"][dayIndex]} +
    + {lesson.nameShort} + + {new Date(lesson.start).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit' + })} - + {new Date(lesson.end).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit' + })} + +
    +
    +
    +
    +) + ; }; export default Calendar; diff --git a/frontend/src/components/Calendar/PrivateRoute.jsx b/frontend/src/components/Calendar/PrivateRoute.jsx index ec55f0a..d7e53e9 100644 --- a/frontend/src/components/Calendar/PrivateRoute.jsx +++ b/frontend/src/components/Calendar/PrivateRoute.jsx @@ -1,12 +1,21 @@ -import LoginRoute from "../../pages/LoginRoute"; -import React from "react"; +// import LoginRoute from "../../pages/LoginRoute"; +import React, {useEffect} from "react"; +import { useNavigate } from "react-router-dom"; const PrivateRoute = ({ children }) => { const token = localStorage.getItem("token"); // Проверяем наличие токена + const navigate = useNavigate(); - if (!token) { + useEffect(() => { + if (!token) { // Если токена нет, перенаправляем на страницу логина - return ; + // return ; + navigate("/", { replace: true }); + } + }, [token, navigate]); + + if (!token) { + return null; // Пока идет редирект, не отображаем ничего } return children; // Если токен есть, отображаем защищённый компонент diff --git a/frontend/src/components/Calendar/left-week.png b/frontend/src/components/Calendar/left-week.png deleted file mode 100644 index 58b6e88d1ab05afdba6b929a206f91424b1420b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 226 zcmeAS@N?(olHy`uVBq!ia0vp^Y(UJx0U~c5>$3n-oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBez~WMV@L&KZl57ng8`50$3n-oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBex;|2V@L&KYWG3jW&dHX6hiNO(h0m}UA&k3vqog$ z0fyeKU)1J!thDOdB0h0qivA&o*@Z9WAAR_}{nYE*rSoe{EJOx$0z V^Zu`k-UW07gQu&X%Q~loCIH
    + diff --git a/frontend/src/components/Header/Shape-reverse.png b/frontend/src/components/Header/Shape-reverse.png deleted file mode 100644 index 0ae2959721bac5a2ec2cea952e249dbf5607b04a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^Y(UJx0U~c5>$3n-oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBex;|2V@L&KYWG3jW&dHX6hiNO(h0m}UA&k3vqog$ z0fyeKU)1J!thDOdB0h0qivA&o*@Z9WAAR_}{nYE*rSoe{EJOx$0z V^Zu`k-UW07gQu&X%Q~loCIH;4~pEG*09DT5}X48qgmlD*D{5Y)Z@gynD_U=``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBewe3=V@L&KZSQH`1_c2ZN$19C45AYKb?c63 z%X)BbOLBdjbc37mUS`>6qvpetmpSifJX^R=NN#n1)G5~G=~Je;`uU_eOss2LG$U~F k)tS@8g|6NGc=ji=>j^1FVYQFvfHp9Ay85}Sb4q9e0QV(4EC2ui diff --git a/frontend/src/components/Header/cross.png b/frontend/src/components/Header/cross.png deleted file mode 100644 index 39835f3e7cfc66821657d21e86531798aab3614c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqoCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBey694V@L&~Z@(koVFdx^y(()f*xZ+Bb?85N z`jg2$@1g8krlYq5HZGs~*0ecCan6g)5}TJ#;rcGIcz2fVj?>xP%NhJwS{1uYqMNfF zBG;dV-@NNv^f^Ld@KDXiYa laP8L2%m2^L_B{Aoai3IfXsC~BTPe^{44$rjF6*2UngF>>Rfzxq diff --git a/frontend/src/components/Header/left-week.png b/frontend/src/components/Header/left-week.png deleted file mode 100644 index 58b6e88d1ab05afdba6b929a206f91424b1420b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 226 zcmeAS@N?(olHy`uVBq!ia0vp^Y(UJx0U~c5>$3n-oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBez~WMV@L&KZl57ng8`50 \ No newline at end of file diff --git a/frontend/src/pages/CalendarRoute.jsx b/frontend/src/pages/CalendarRoute.jsx index 3c10107..e6ddbd4 100644 --- a/frontend/src/pages/CalendarRoute.jsx +++ b/frontend/src/pages/CalendarRoute.jsx @@ -1,16 +1,38 @@ import React, { useEffect, useState } from 'react'; import { getNetologyCourse, bulkEvents } from '../services/api'; // Ваши API-запросы import Calendar from "../components/Calendar/Calendar"; +import Header from "../components/Header/Header"; const CalendarRoute = ({ email, password, personId, token }) => { - console.log('Полученные данные:', { email, password, personId, token }); const [events, setEvents] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + // Функция для загрузки событий из localStorage + const loadEventsFromLocalStorage = () => { + const savedEvents = localStorage.getItem('events'); + if (savedEvents) { + return JSON.parse(savedEvents); + } + return null; + }; + + // Сохраняем события в localStorage + const saveEventsToLocalStorage = (eventsData) => { + localStorage.setItem('events', JSON.stringify(eventsData)); + }; useEffect(() => { const fetchCourseAndEvents = async () => { + // Попытка загрузки событий из localStorage + const cachedEvents = loadEventsFromLocalStorage(); + if (cachedEvents) { + console.log("События загружены из localStorage:", cachedEvents); + setEvents(cachedEvents); + setLoading(false); + return; + } + if (!token || !email || !password || !personId) { console.error('Ошибка авторизации. Проверьте введенные данные.'); return; @@ -18,7 +40,7 @@ const CalendarRoute = ({ email, password, personId, token }) => { try { const courseData = await getNetologyCourse(token); - console.log('Данные курса:', courseData); // Проверьте, что курс получен + console.log('Данные курса:', courseData); const fetchedCalendarId = courseData?.id; @@ -32,8 +54,11 @@ const CalendarRoute = ({ email, password, personId, token }) => { "2024-10-13T23:59:59+03:00", // Дата окончания personId // ID участника ); - console.log('События:', eventsResponse.data); // Проверьте, что события получены + console.log('События:', eventsResponse.data); setEvents(eventsResponse.data); + + // Сохраняем события в localStorage + saveEventsToLocalStorage(eventsResponse.data); } } catch (error) { console.error('Ошибка при получении данных с сервера:', error); @@ -55,8 +80,9 @@ const CalendarRoute = ({ email, password, personId, token }) => { } return ( -
    +
    +
    ); }; diff --git a/frontend/src/pages/LoginRoute.jsx b/frontend/src/pages/LoginRoute.jsx index e40e339..46adfcc 100644 --- a/frontend/src/pages/LoginRoute.jsx +++ b/frontend/src/pages/LoginRoute.jsx @@ -1,7 +1,7 @@ import {useEffect, useState} from "react"; -import { useNavigate } from "react-router-dom"; +import {useNavigate} from "react-router-dom"; -const LoginRoute = ({ onLogin, onSearch }) => { +const LoginRoute = ({onLogin, onSearch}) => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [fullName, setFullName] = useState(""); // Строка для поиска @@ -11,7 +11,7 @@ const LoginRoute = ({ onLogin, onSearch }) => { const [errorMessage, setErrorMessage] = useState(""); // Сообщение об ошибке const [debounceTimeout, setDebounceTimeout] = useState(null); // Для хранения таймера - const navigate = useNavigate(); // Инициализируем хук для навигации + const navigate = useNavigate(); // Инициализируем хук для навигации // Функция для выполнения поиска const onClickSearch = async (fullName) => { diff --git a/frontend/src/style/calendar.scss b/frontend/src/style/calendar.scss new file mode 100644 index 0000000..e5b8648 --- /dev/null +++ b/frontend/src/style/calendar.scss @@ -0,0 +1,255 @@ +//&-info { +// height: 28px; +// margin-top: 6px; +// width: 95%; +// text-align: left; +// padding: 5px 11px; +// border: none; +// background: #F46386; +// border-radius: 6px; +// color: #fff; +// font-family: "Roboto", sans-serif; +// font-weight: 700; +// font-size: 13px; +// transition: color 0.3s linear; +// +// &:hover { +// cursor: pointer; +// color: #F46386; +// background-color: #fff; +// border: 2px solid #F46386; +// } +// } + +//&-info-on { +// height: 28px; +// margin-top: 4px; +// width: 95%; +// text-align: left; +// padding: 5px 11px; +// color: #F46386; +// background-color: #fff; +// border: 2px solid #F46386; +// border-radius: 6px; +// font-family: "Roboto", sans-serif; +// font-weight: 700; +// font-size: 13px; +// transition: color 0.3s linear; +// +// &:hover { +// cursor: pointer; +// color: #fff; +// background-color: #F46386; +// border: none; +// } +// } + +.shedule-table { + width: 100%; + margin-top: 36px; + border-spacing: 1px; +} + +.days { + border-bottom: 1px dotted #adadad; + &-1 { + color: #adadad; + padding-left: 30px; + padding-bottom: 10px; + padding-top: 10px; + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 13px; + border: 1px; + border-bottom: 1px dotted #adadad; + } + &-2 { + color: #7b61ff; + padding-left: 30px; + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 13px; + border: 1px; + border-bottom: 1px dotted #adadad; + } + &-3 { + color: #333333; + padding-left: 30px; + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 13px; + border: 1px; + border-bottom: 1px dotted #adadad; + } + &-4 { + color: #333333; + padding-left: 30px; + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 13px; + border: 1px; + border-bottom: 1px dotted #adadad; + } + &-5 { + color: #333333; + padding-left: 30px; + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 13px; + border: 1px; + border-bottom: 1px dotted #adadad; + } + &-6 { + color: #333333; + padding-left: 30px; + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 13px; + border: 1px; + border-bottom: 1px dotted #adadad; + } + &-7 { + color: #333333; + padding-left: 30px; + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 13px; + border: 1px; + border-bottom: 1px dotted #adadad; + } +} + +.vertical-deadline { + vertical-align: top; + height: 72px; + width: 195px; + padding-left: 20px; + border-bottom: 1px dotted #adadad; + &-heading { + padding-left: 47px; + padding-top: 7px; + padding-bottom: 7px; + font-family: "Roboto Mono", monospace; + font-weight: 400; + font-size: 12px; + width: 0; + border-bottom: 1px dotted #adadad; + &::first-line { + font-family: "Roboto", sans-serif; + font-weight: 700; + font-size: 13px; + } + } +} + +.vertical { + height: 72px; + width: 195px; + padding-left: 20px; + border-bottom: 1px dotted #adadad; + &-heading { + padding-left: 47px; + padding-top: 7px; + padding-bottom: 7px; + font-family: "Roboto Mono", monospace; + font-weight: 400; + font-size: 12px; + width: 0; + border-bottom: 1px dotted #adadad; + &::first-line { + font-family: "Roboto", sans-serif; + font-weight: 700; + font-size: 13px; + } + } +} + +.off-deadline { + margin-top: 7px; + font-family: "Roboto", sans-serif; + font-weight: 400; + font-size: 13px; + color: #7b61ff; + background: none; + padding: 4px 8px; + border-radius: 6px; + border: 1px solid #7b61ff; + transition: color 0.3s linear; + &:hover { + cursor: pointer; + background-color: #5856d6; + color: #fff; + } +} + +.past-lesson { + display: flex; + flex-direction: column; + justify-content: space-evenly; + padding-left: 12px; + text-align: left; + width: 95%; + height: 95%; + background: #ECEDF0; + border: none; + border-radius: 6px; + &:hover { + cursor: pointer; + } +} + +.company-name { + vertical-align: top; + font-family: "Roboto", sans-serif; + font-weight: 500; + font-size: 10px; + color: #7B61FF; +} + +.lesson-name { + vertical-align: top; + font-family: "Roboto", sans-serif; + font-weight: 400; + font-size: 13px; + color: #2C2D2E; +} + +.netology-lesson { + display: flex; + flex-direction: column; + justify-content: space-evenly; + padding-left: 12px; + text-align: left; + width: 95%; + height: 95%; + background: #00A8A833; + border: none; + border-radius: 6px; + &:hover { + cursor: pointer; + } +} + +.TyumGU-lesson { + display: flex; + flex-direction: column; + justify-content: space-evenly; + padding-left: 12px; + text-align: left; + width: 95%; + height: 95%; + background: #7B61FF33; + border: none; + border-radius: 6px; + &:hover { + cursor: pointer; + } +} + +.vertical-line { + margin-top: 17px; + margin-left: 365px; + position: absolute; + border-left: 2px solid #7B61FF; + height: 590px; +} diff --git a/frontend/src/style/header.scss b/frontend/src/style/header.scss new file mode 100644 index 0000000..d02a8e9 --- /dev/null +++ b/frontend/src/style/header.scss @@ -0,0 +1,231 @@ +.wrapper { + width: 100%; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + margin-top: 20px; +} + +.header { + max-width: 80%; + margin: auto; + + &-line { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 22px; + } + + &-deadline { + display: flex; + justify-content: flex-end; + flex-direction: row-reverse; + align-items: stretch; + + &-info { + display: block; + padding: 0 12px; + //margin: 22px 120px; + margin-right: 5px; + width: 534px; + min-height: 106px; + border-radius: 6px; + background: #f4638633; + border: 2px solid #f46386; + text-decoration: none; + } + } +} + + +.shedule-export { + display: flex; + align-items: center; +} + +.shedule { + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 32px; +} + +.export-btn { + cursor: pointer; + height: 28px; + background-color: #5856d6; + color: #fff; + text-decoration: none; + border: none; + border-radius: 6px; + padding: 4px 8px; + font-family: "Roboto", sans-serif; + font-weight: 400; + font-style: normal; + margin-left: 18px; + transition: color 0.2s linear; + + &:hover { + background-color: #4745b5; + } +} + +.cache-btn { + margin-left: 18px; + height: 28px; + font-family: "Roboto", sans-serif; + font-weight: 400; + font-size: 13px; + color: #7b61ff; + background: none; + padding: 4px 8px; + border-radius: 6px; + border: 1px solid #7b61ff; + transition: color 0.3s linear; + + &:hover { + cursor: pointer; + background-color: #5856d6; + color: #fff; + } +} + +.exit-btn { + background: none; + cursor: pointer; + border: none; + color: #333333; + text-decoration: none; + font-family: "Roboto", sans-serif; + font-weight: 400; + font-size: 13px; + + &-cross { + margin-left: 7px; + } +} + +.source { + margin-top: 7px; + color: #f46386; + font-family: "Roboto", sans-serif; + font-weight: 700; + font-size: 13px; + + &-first-word { + font-family: "Roboto", sans-serif; + font-weight: 400; + } +} + +.date-event { + float: right; +} + +.name-event { + margin-top: 26px; + + &-text { + color: #2c2d2e; + font-family: "Unbounded", sans-serif; + font-weight: 700; + font-size: 15px; + } +} + +.task-event { + margin-top: 7px; + + &-text { + color: #2c2d2e; + font-family: "Roboto", sans-serif; + font-weight: 400; + font-size: 15px; + } +} + +.input-name { + margin-top: 8px; + width: 320px; + height: 32px; + border-radius: 4px; + border-color: #d9d9d9; +} + +.input-email { + margin-top: 8px; + width: 320px; + height: 32px; + border-radius: 4px; + border-color: #d9d9d9; +} + +.login-btn-log { + width: 100px; + position: relative; + cursor: pointer; + border: none; + margin: 6px auto; + background-color: #5856d6; + color: #fff; + text-decoration: none; + border-radius: 6px; + padding: 10px 20px; + font-family: "Roboto", sans-serif; + font-weight: 400; + transition: color 0.2s linear; + + &:hover { + background-color: #4745b5; + } +} + +.login-container { + display: flex; + align-items: center; + flex-direction: column; +} + +.shedule-login { + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 32px; +} + +.login-netologiya, +.login-fio { + font-family: "Roboto", sans-serif; + font-weight: 400; + font-size: 15px; + max-width: 320px; + width: 100%; + margin: 24px auto; + display: flex; + align-items: flex-start; + justify-content: center; + flex-direction: column; + text-align: left; +} + +.calendar-date { + position: absolute; + top: 165px; + right: 248px; + background-color: #ecedf0; + width: 406px; + height: 32px; + border-radius: 6px; + border: 1px solid rgba(217, 217, 217, 0.5); + font-family: "Unbounded", sans-serif; + font-weight: 500; + font-size: 15px; + padding-left: 14px; + transition: color 0.2s linear; + + &:hover { + background-color: #e7e7ea; + } +} \ No newline at end of file diff --git a/frontend/src/style/login.scss b/frontend/src/style/login.scss new file mode 100644 index 0000000..e69de29 From 3b483db7d58f682deed795e9f84a3ac730f1a4f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B0=D1=80=D0=BF=D0=BE=D0=B2=D0=B8=D1=87=20=D0=90?= =?UTF-8?q?=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Mon, 14 Oct 2024 19:08:44 +0300 Subject: [PATCH 26/31] fix: fix errors --- .../src/components/Calendar/PrivateRoute.jsx | 24 +++++++++---------- frontend/src/pages/LoginRoute.jsx | 20 ++++++++-------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/Calendar/PrivateRoute.jsx b/frontend/src/components/Calendar/PrivateRoute.jsx index d7e53e9..b7171b2 100644 --- a/frontend/src/components/Calendar/PrivateRoute.jsx +++ b/frontend/src/components/Calendar/PrivateRoute.jsx @@ -1,21 +1,21 @@ -// import LoginRoute from "../../pages/LoginRoute"; -import React, {useEffect} from "react"; -import { useNavigate } from "react-router-dom"; +import LoginRoute from "../../pages/LoginRoute"; +// import React, {useEffect} from "react"; +// import { useNavigate } from "react-router-dom"; const PrivateRoute = ({ children }) => { const token = localStorage.getItem("token"); // Проверяем наличие токена - const navigate = useNavigate(); + // const navigate = useNavigate(); - useEffect(() => { - if (!token) { - // Если токена нет, перенаправляем на страницу логина - // return ; - navigate("/", { replace: true }); - } - }, [token, navigate]); + // useEffect(() => { + // if (!token) { + // // Если токена нет, перенаправляем на страницу логина + // // return ; + // navigate("/login", { replace: true }); + // } + // }, [token, navigate]); if (!token) { - return null; // Пока идет редирект, не отображаем ничего + return ; // Пока идет редирект, не отображаем ничего } return children; // Если токен есть, отображаем защищённый компонент diff --git a/frontend/src/pages/LoginRoute.jsx b/frontend/src/pages/LoginRoute.jsx index 46adfcc..c3afbf1 100644 --- a/frontend/src/pages/LoginRoute.jsx +++ b/frontend/src/pages/LoginRoute.jsx @@ -1,4 +1,4 @@ -import {useEffect, useState} from "react"; +import {useEffect, useState, useCallback} from "react"; import {useNavigate} from "react-router-dom"; const LoginRoute = ({onLogin, onSearch}) => { @@ -9,12 +9,12 @@ const LoginRoute = ({onLogin, onSearch}) => { const [personId, setPersonId] = useState(null); // Здесь сохраняем personId const [showSuggestions, setShowSuggestions] = useState(false); // Флаг показа списка const [errorMessage, setErrorMessage] = useState(""); // Сообщение об ошибке - const [debounceTimeout, setDebounceTimeout] = useState(null); // Для хранения таймера + // const [debounceTimeout, setDebounceTimeout] = useState(null); // Для хранения таймера const navigate = useNavigate(); // Инициализируем хук для навигации // Функция для выполнения поиска - const onClickSearch = async (fullName) => { + const onClickSearch = useCallback(async (fullName) => { const result = await onSearch(fullName); if (result.success) { setSearchResults(result.data); @@ -23,14 +23,14 @@ const LoginRoute = ({onLogin, onSearch}) => { } else { setErrorMessage(result.message); } - }; + }, [onSearch]); // Обрабатываем изменение поля поиска с задержкой useEffect(() => { - // Очищаем предыдущий таймер, чтобы избежать лишних вызовов - if (debounceTimeout) { - clearTimeout(debounceTimeout); - } + // // Очищаем предыдущий таймер, чтобы избежать лишних вызовов + // if (debounceTimeout) { + // clearTimeout(debounceTimeout); + // } // Устанавливаем новый таймер на 500 мс const newTimeout = setTimeout(() => { @@ -39,11 +39,11 @@ const LoginRoute = ({onLogin, onSearch}) => { } }, 500); - setDebounceTimeout(newTimeout); + // setDebounceTimeout(newTimeout); // Очищаем таймер при размонтировании или изменении fullName return () => clearTimeout(newTimeout); - }, [fullName]); + }, [fullName, onClickSearch]); // Обработчик выбора варианта из списка const handleSelect = (person) => { From 5d1384093da5ea61219782920b6e7b2d15523696 Mon Sep 17 00:00:00 2001 From: AzamatKomaev Date: Mon, 14 Oct 2024 19:40:21 +0300 Subject: [PATCH 27/31] change default image tag in docker-compose.yml --- docker-compose.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index b989878..f012e3a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,11 +1,11 @@ services: - api: &main_app + api: ports: - "8000:8000" build: context: backend/ dockerfile: Dockerfile - image: ghcr.io/azamatkomaev/yet_another_calendar_backend:${YET_ANOTHER_CALENDAR_VERSION:-dev} + image: ghcr.io/azamatkomaev/yet_another_calendar_backend:${YET_ANOTHER_CALENDAR_VERSION:-latest} restart: always volumes: - ./backend:/app/src/ @@ -37,7 +37,7 @@ services: build: context: ./frontend target: dev-run - image: ghcr.io/azamatkomaev/yet_another_calendar_frontend:${YET_ANOTHER_CALENDAR_VERSION:-dev} + image: ghcr.io/azamatkomaev/yet_another_calendar_frontend:${YET_ANOTHER_CALENDAR_VERSION:-latest} container_name: calendar-frontend env_file: - frontend/.env From 43f0afeff43e3a7fe72d447d871e1adecbb9c8ec Mon Sep 17 00:00:00 2001 From: AzamatKomaev Date: Mon, 14 Oct 2024 20:23:23 +0300 Subject: [PATCH 28/31] add deploy.yaml --- .github/workflows/build.yaml | 3 ++- .github/workflows/deploy.yaml | 35 +++++++++++++++++++++++++++++++++++ .gitignore | 1 + docker-compose.yaml | 2 -- 4 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/deploy.yaml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9ec6993..3837291 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -2,7 +2,8 @@ name: build on: push: branches: - - "*" + - dev + - main paths-ignore: - "README.md" workflow_dispatch: diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..dc530c8 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,35 @@ +name: deploy +on: + push: + branches: + - main + workflow_run: + workflows: [build] + types: + - completed + workflow_dispatch: + +env: + IMAGE_TAG: ${{ github.sha }} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + SSH_HOST: ${{ secrets.SSH_HOST }} + SSH_USER: ${{ secrets.SSH_USER }} + + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: 'command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )' + - name: Set up SSH private key. + run: eval "$(ssh-agent -s)" + - run: mkdir -p ~/.ssh + - run: ssh-keyscan ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts + - run: ssh-agent -a $SSH_AUTH_SOCK > /dev/null + - run: ssh-add - <<< "${{ secrets.SSH_KEY }}" + + - name: Update docker compose + run: | + ssh $SSH_USER@$SSH_HOST "cd YetAnotherCalendar/ && YET_ANOTHER_CALENDAR_VERSION=$IMAGE_TAG docker compose up -d --build" + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 388c996..a7ef229 100644 --- a/.gitignore +++ b/.gitignore @@ -124,6 +124,7 @@ celerybeat.pid *.sage.py # Environments +.env.version frontend/.env .venv env/ diff --git a/docker-compose.yaml b/docker-compose.yaml index f012e3a..5b03b63 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -9,7 +9,6 @@ services: restart: always volumes: - ./backend:/app/src/ - env_file: - backend/.env depends_on: redis: @@ -39,7 +38,6 @@ services: target: dev-run image: ghcr.io/azamatkomaev/yet_another_calendar_frontend:${YET_ANOTHER_CALENDAR_VERSION:-latest} container_name: calendar-frontend - env_file: - frontend/.env restart: always volumes: From b9302861ecd127be8cafe8ce16704026e4051eee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B0=D1=80=D0=BF=D0=BE=D0=B2=D0=B8=D1=87=20=D0=90?= =?UTF-8?q?=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Mon, 14 Oct 2024 23:29:26 +0300 Subject: [PATCH 29/31] fix: fix errors --- frontend/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/index.js b/frontend/src/index.js index 12f3815..a0f65ac 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -3,7 +3,7 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import reportWebVitals from "./reportWebVitals"; import ReactDOM from "react-dom/client"; -import Header from "./components/Header/Header"; +// import Header from "./components/Header/Header"; import LoginRoute from "./pages/LoginRoute"; import CalendarRoute from "./pages/CalendarRoute"; import { AuthProvider } from './context/AuthContext'; From eb834fa7dbda1bac10fdb544232cf557c2a54a71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B0=D1=80=D0=BF=D0=BE=D0=B2=D0=B8=D1=87=20=D0=90?= =?UTF-8?q?=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Tue, 15 Oct 2024 03:30:31 +0300 Subject: [PATCH 30/31] feat: add logic ui calendar --- frontend/src/components/Calendar/Calendar.jsx | 218 ++++++++++-------- .../src/components/Calendar/PrivateRoute.jsx | 21 +- frontend/src/pages/CalendarRoute.jsx | 8 +- 3 files changed, 135 insertions(+), 112 deletions(-) diff --git a/frontend/src/components/Calendar/Calendar.jsx b/frontend/src/components/Calendar/Calendar.jsx index d8f026e..6fedcd5 100644 --- a/frontend/src/components/Calendar/Calendar.jsx +++ b/frontend/src/components/Calendar/Calendar.jsx @@ -5,49 +5,77 @@ import cross from "../../img/arrow.png"; import '../../style/header.scss'; import '../../style/calendar.scss'; - const Calendar = ({events}) => { - console.log('events', events) + console.log('events:', events); + if (!events) { return
    Нет данных для отображения.
    ; } const modeus = events?.modeus; const netology = events?.netology; - const homework = netology?.homework; - const webinars = netology?.webinars; + // const homework = netology?.homework; + // const webinars = netology?.webinars; console.log('modeus', modeus) - console.log('netology', netology) - console.log('webinars', webinars) - console.log('homework', homework) - - // // Функция для получения дня недели (0 = Воскресенье, 6 = Суббота) - // const getDayOfWeek = (dateString) => { - // return new Date(dateString).getDay(); - // }; - // - // // Массив для распределения занятий по дням недели - // const weekDays = Array(7).fill([]); // 0 - Воскресенье, 1 - Понедельник и т.д. - // - // // Распределение занятий по дням недели - // modeus?.forEach((lesson) => { - // const dayOfWeek = getDayOfWeek(lesson.start); - // weekDays[dayOfWeek] = [...weekDays[dayOfWeek], lesson]; // Добавляем занятия в соответствующий день недели - // }); - - // Function to get the day of the week (0 = Sunday, 6 = Saturday) - const getDayOfWeek = (dateString) => { - return new Date(dateString).getDay(); + // console.log('netology', netology) + // console.log('webinars', webinars) + // console.log('homework', homework) + + // Функция для получения дней недели + const getWeekDays = () => { + const today = new Date(); + const startOfWeek = today.getDate() - today.getDay() + (today.getDay() === 0 ? -6 : 1); // Начало недели с понедельника + const weekDays = []; + + // Генерация всех дней недели (с понедельника по воскресенье) + for (let i = 0; i < 7; i++) { + const day = new Date(today.setDate(startOfWeek + i)); + weekDays.push({ + day: day.toLocaleDateString('ru-RU', { weekday: 'short', day: '2-digit', month: '2-digit' }), + date: day.toISOString().split('T')[0] // Формат даты YYYY-MM-DD + }); + } + return weekDays; + }; + + const weekDays = getWeekDays(); + + // Функция для фильтрации занятий на определенный день + const getEventsForDay = (day) => { + return modeus.filter(event => { + const eventDate = event.start.split('T')[0]; // Извлекаем только дату в формате YYYY-MM-DD + return eventDate === day; + }); }; - // Create an array for each day of the week - const weekDays = Array(7).fill([]); // 0 - Sunday, 1 - Monday, and so on + // Функция для определения номера пары по времени + const getLessonNumber = (eventStart) => { + const startTime = new Date(eventStart).getHours(); + const startMinutes = new Date(eventStart).getMinutes(); + + // 1 пара: 08:00 - 09:30 + if (startTime === 8 || (startTime === 9 && startMinutes <= 30)) return 1; + + // 2 пара: 10:00 - 11:30 + if (startTime === 10 || (startTime === 11 && startMinutes <= 30)) return 2; + + // 3 пара: 12:00 - 13:30 + if (startTime === 12 || (startTime === 13 && startMinutes <= 30)) return 3; - // Distribute modeus events across the week by day of the week - modeus?.forEach((lesson) => { - const dayOfWeek = getDayOfWeek(lesson.start); - weekDays[dayOfWeek] = [...weekDays[dayOfWeek], lesson]; - }); + // 4 пара: 14:00 - 15:30 + if (startTime === 14 || (startTime === 15 && startMinutes <= 30)) return 4; + + // 5 пара: 15:45 - 17:15 + if ((startTime === 15 && startMinutes >= 45) || startTime === 16 || (startTime === 17 && startMinutes <= 15)) return 5; + + // 6 пара: 17:30 - 19:00 + if ((startTime === 17 && startMinutes >= 30) || startTime === 18 || (startTime === 19 && startMinutes === 0)) return 6; + + // 7 пара: 19:10 - 20:40 + if ((startTime === 19 && startMinutes >= 10) || (startTime === 20 && startMinutes <= 40)) return 7; + + return null; + }; return (
    @@ -93,81 +121,83 @@ const Calendar = ({events}) => {
    + {/*TODO: написать логику движении линии, для отображения текущего дня*/}
    - {/* Таблица расписания */}
    - - - - - - - - + + {weekDays.map((day, index) => ( + + ))} - - - {/* Дедлайны */} + {/*TODO: написать логику*/} - - - - - - - - + + + + + + + + - {/*/!* Проход по урокам из modeus *!/*/} - {/*{modeus?.map((lesson, index) => (*/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/*))}*/} - - {weekDays.map((lessons, dayIndex) => ( - - - {lessons.length > 0 ? ( - lessons.map((lesson, index) => ( - - )) - ) : ( - - )} + + {/*TODO: дописать логику, поправить стили*/} + + {[1, 2, 3, 4, 5, 6, 7].map((lessonNumber) => ( + + + {weekDays.map((day, index) => { + const eventsForDay = getEventsForDay(day.date); + const eventsForSlot = eventsForDay.filter(event => getLessonNumber(event.start) === lessonNumber); + return ( + + ); + })} - ))} + ))}
    ПнВтСрЧтПтСбВс{day.day}
    Дедлайны{/* Дедлайны для Пн */}{/* Дедлайны для Вт */}{/* Дедлайны для Ср */}{/* Дедлайны для Чт */}{/* Дедлайны для Пт */}{/* Дедлайны для Сб */}{/* Дедлайны для Вс */} + дедлайны + + + + + + + + +
    {lesson.nameShort}/!* Уроки на Пн *!//!* Уроки на Вт *!//!* Уроки на Ср *!//!* Уроки на Чт *!//!* Уроки на Пт *!//!* Уроки на Сб *!//!* Уроки на Вс *!/
    {["Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб"][dayIndex]} -
    - {lesson.nameShort} - - {new Date(lesson.start).toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit' - })} - - {new Date(lesson.end).toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit' - })} - -
    -
    + {lessonNumber} пара
    {lessonNumber * 2 + 8}:00 - {lessonNumber * 2 + 9}:30 +
    + {eventsForSlot.length > 0 ? ( + eventsForSlot.map(event => ( +
    +
    {event.nameShort}
    +
    {event.name}
    +
    + {new Date(event.start).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit' + })} - + {new Date(event.end).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit' + })} +
    + {event.teacher_full_name} +
    + )) + ) : ( +
    + )} +
    -) - ; + ); }; -export default Calendar; +export default Calendar; \ No newline at end of file diff --git a/frontend/src/components/Calendar/PrivateRoute.jsx b/frontend/src/components/Calendar/PrivateRoute.jsx index b7171b2..b4584cb 100644 --- a/frontend/src/components/Calendar/PrivateRoute.jsx +++ b/frontend/src/components/Calendar/PrivateRoute.jsx @@ -1,24 +1,17 @@ -import LoginRoute from "../../pages/LoginRoute"; -// import React, {useEffect} from "react"; -// import { useNavigate } from "react-router-dom"; +import { Navigate } from "react-router-dom"; +// Компонент для защиты маршрутов const PrivateRoute = ({ children }) => { const token = localStorage.getItem("token"); // Проверяем наличие токена - // const navigate = useNavigate(); - - // useEffect(() => { - // if (!token) { - // // Если токена нет, перенаправляем на страницу логина - // // return ; - // navigate("/login", { replace: true }); - // } - // }, [token, navigate]); if (!token) { - return ; // Пока идет редирект, не отображаем ничего + // Если токена нет, перенаправляем на страницу логина + return ; } - return children; // Если токен есть, отображаем защищённый компонент + // Если токен есть, отображаем защищённый компонент + return children; }; export default PrivateRoute; + diff --git a/frontend/src/pages/CalendarRoute.jsx b/frontend/src/pages/CalendarRoute.jsx index e6ddbd4..83067f4 100644 --- a/frontend/src/pages/CalendarRoute.jsx +++ b/frontend/src/pages/CalendarRoute.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { getNetologyCourse, bulkEvents } from '../services/api'; // Ваши API-запросы import Calendar from "../components/Calendar/Calendar"; -import Header from "../components/Header/Header"; +// import Header from "../components/Header/Header"; const CalendarRoute = ({ email, password, personId, token }) => { const [events, setEvents] = useState(null); @@ -50,8 +50,8 @@ const CalendarRoute = ({ email, password, personId, token }) => { password, // Пароль пользователя token, // Токен сессии fetchedCalendarId, // ID календаря - "2024-10-07T00:00:00+03:00", // Дата начала - "2024-10-13T23:59:59+03:00", // Дата окончания + "2024-10-14T00:00:00+03:00", // Дата начала + "2024-10-20T23:59:59+03:00", // Дата окончания personId // ID участника ); console.log('События:', eventsResponse.data); @@ -82,7 +82,7 @@ const CalendarRoute = ({ email, password, personId, token }) => { return (
    -
    + {/*
    */}
    ); }; From f50ea6a027814f49ed2c20739dd04df53b8320a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B0=D1=80=D0=BF=D0=BE=D0=B2=D0=B8=D1=87=20=D0=90?= =?UTF-8?q?=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Wed, 16 Oct 2024 03:17:27 +0300 Subject: [PATCH 31/31] feat: add logic datapicker, style calendar, prepared date for export,cash_at --- frontend/src/App.jsx | 1 + frontend/src/components/Calendar/Calendar.jsx | 57 ++++++------ .../src/components/Calendar/DataPicker.jsx | 82 +++++++++++------ .../src/components/Calendar/exportICS.jsx | 0 frontend/src/components/Header/Header.js | 30 ++++--- frontend/src/pages/CalendarRoute.jsx | 16 ++-- frontend/src/pages/LoginRoute.jsx | 1 + frontend/src/services/api.js | 24 +++++ frontend/src/style/DatePicker.scss | 40 +++++++++ frontend/src/style/calendar.scss | 89 +++++++++---------- frontend/src/style/header.scss | 42 ++++----- 11 files changed, 244 insertions(+), 138 deletions(-) create mode 100644 frontend/src/components/Calendar/exportICS.jsx create mode 100644 frontend/src/style/DatePicker.scss diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index caa3691..23562fc 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -20,6 +20,7 @@ const App = () => { if (response.status === 200) { setAuthData({email, password, personId}); localStorage.setItem('token', response.data["_netology-on-rails_session"]); + return {success: true}; } else { return {success: false, message: "Неверный логин или пароль."}; diff --git a/frontend/src/components/Calendar/Calendar.jsx b/frontend/src/components/Calendar/Calendar.jsx index 6fedcd5..c4ec2c3 100644 --- a/frontend/src/components/Calendar/Calendar.jsx +++ b/frontend/src/components/Calendar/Calendar.jsx @@ -5,6 +5,9 @@ import cross from "../../img/arrow.png"; import '../../style/header.scss'; import '../../style/calendar.scss'; +import DatePicker from "./DataPicker"; +import camera from "../../img/camera.png"; + const Calendar = ({events}) => { console.log('events:', events); @@ -91,10 +94,9 @@ const Calendar = ({events}) => { - {/* Информация о дедлайнах */} -
    +
    {netology?.homework?.length > 0 && netology.homework.map((homeworkItem, index) => ( - { ))}
    + +
    @@ -126,29 +130,28 @@ const Calendar = ({events}) => { - + {weekDays.map((day, index) => ( ))} - {/*TODO: написать логику*/} - - + @@ -160,7 +163,7 @@ const Calendar = ({events}) => { {[1, 2, 3, 4, 5, 6, 7].map((lessonNumber) => ( {weekDays.map((day, index) => { const eventsForDay = getEventsForDay(day.date); @@ -169,13 +172,14 @@ const Calendar = ({events}) => { - ); - })} - - ))} - + ) : (
    + )} + + ); + })} + + ) + ) + } +
    {day.day}
    - дедлайны - + {/*TODO: написать логику*/} +
    дедлайны +
    Скрыть
    - - +
    ТюмГУ
    +
    Нетология
    - +
    ТюмГУ
    - +
    Нетология
    - {lessonNumber} пара
    {lessonNumber * 2 + 8}:00 - {lessonNumber * 2 + 9}:30 + {lessonNumber} пара
    {lessonNumber * 2 + 8}:00 {lessonNumber * 2 + 9}:30
    {eventsForSlot.length > 0 ? ( eventsForSlot.map(event => ( -
    +
    + {camera}/ ТюмГУ
    {event.nameShort}
    {event.name}
    {new Date(event.start).toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit' + hour: '2-digit', + minute: '2-digit' })} - {new Date(event.end).toLocaleTimeString([], { hour: '2-digit', @@ -185,15 +189,16 @@ const Calendar = ({events}) => { {event.teacher_full_name}
    )) - ) : ( -
    - )} -
    diff --git a/frontend/src/components/Calendar/DataPicker.jsx b/frontend/src/components/Calendar/DataPicker.jsx index 6bed40b..92660af 100644 --- a/frontend/src/components/Calendar/DataPicker.jsx +++ b/frontend/src/components/Calendar/DataPicker.jsx @@ -1,41 +1,67 @@ -import React, { useRef, useEffect, useState } from "react"; -import Flatpickr from "flatpickr"; -import weekSelect from "flatpickr"; +import React, { useEffect, useState } from "react"; import "flatpickr/dist/flatpickr.css"; -import flatpickr from "flatpickr"; -import { Russian } from "flatpickr/dist/l10n/ru.js"; +// import flatpickr from "flatpickr"; +// import { Russian } from "flatpickr/dist/l10n/ru.js"; +import "../../style/DatePicker.scss"; // Для стилей компонента const DatePicker = () => { - const datePickerRef = useRef(null); + // const datePickerRef = useRef(null); + const [currentDate, setCurrentDate] = useState(new Date()); // Текущая дата + const [weekRange, setWeekRange] = useState(""); - const [dateOnCalendar, setDateOnCalendar] = useState("2024-09-19"); - const [weekNumber, setWeekNumber] = useState(null); + // Рассчитать начало и конец недели + const calculateWeekRange = (date) => { + const startOfWeek = new Date(date); + const endOfWeek = new Date(date); + // Получаем понедельник текущей недели + startOfWeek.setDate(date.getDate() - date.getDay() + 1); // Понедельник + endOfWeek.setDate(startOfWeek.getDate() + 6); // Воскресенье + + const formatOptions = { day: "numeric", month: "long" }; + const startFormatted = startOfWeek.toLocaleDateString("ru-RU", formatOptions); + const endFormatted = endOfWeek.toLocaleDateString("ru-RU", formatOptions); + + return `${startFormatted} – ${endFormatted}`; + }; + + // Обновить диапазон недели при изменении даты useEffect(() => { - flatpickr(datePickerRef.current, { - locale: Russian, - onChange: [ - () => { - setWeekNumber( - this.selectedDates[0] ? setWeekNumber(this.selectedDates[0]) : null, - ); - console.log(weekNumber); - }, - ], + setWeekRange(calculateWeekRange(currentDate)); + }, [currentDate]); + + // Обработчик для переключения недель + const handlePrevWeek = () => { + setCurrentDate((prevDate) => { + const newDate = new Date(prevDate); + newDate.setDate(prevDate.getDate() - 7); // Переключение на предыдущую неделю + return newDate; }); - }, []); + }; + + const handleNextWeek = () => { + setCurrentDate((prevDate) => { + const newDate = new Date(prevDate); + newDate.setDate(prevDate.getDate() + 7); // Переключение на следующую неделю + return newDate; + }); + }; return ( -
    - setDateOnCalendar(e.target.value)} - /> +
    +
    + {weekRange} +
    + + +
    +
    ); }; -flatpickr.l10ns.default.firstDayOfWeek = 1; + export default DatePicker; diff --git a/frontend/src/components/Calendar/exportICS.jsx b/frontend/src/components/Calendar/exportICS.jsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/Header/Header.js b/frontend/src/components/Header/Header.js index aa61965..a5a592c 100644 --- a/frontend/src/components/Header/Header.js +++ b/frontend/src/components/Header/Header.js @@ -23,15 +23,18 @@ export default function Header() { {cross}
    -
    -
    - Дедлайн Нетология {arrow} 23.09.2024 -
    -
    - Программирование на Python -
    -
    - Домашнее задание с самопроверкой(дедлайн 12.12.24) +
    +
    +
    + Дедлайн Нетология {arrow}/ 23.09.2024 +
    +
    + Программирование на Python +
    +
    + Домашнее задание с самопроверкой(дедлайн 12.12.24) +
    @@ -48,7 +51,7 @@ export default function Header() { Вс 29.09 - + дедлайны @@ -71,9 +74,12 @@ export default function Header() { - + Математический анализ +
    + diff --git a/frontend/src/pages/CalendarRoute.jsx b/frontend/src/pages/CalendarRoute.jsx index 83067f4..b2548cd 100644 --- a/frontend/src/pages/CalendarRoute.jsx +++ b/frontend/src/pages/CalendarRoute.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { getNetologyCourse, bulkEvents } from '../services/api'; // Ваши API-запросы import Calendar from "../components/Calendar/Calendar"; -// import Header from "../components/Header/Header"; +import Header from "../components/Header/Header"; const CalendarRoute = ({ email, password, personId, token }) => { const [events, setEvents] = useState(null); @@ -42,21 +42,25 @@ const CalendarRoute = ({ email, password, personId, token }) => { const courseData = await getNetologyCourse(token); console.log('Данные курса:', courseData); - const fetchedCalendarId = courseData?.id; + const calendarId = courseData?.id; + console.log('calendarId add storage', calendarId) + localStorage.setItem('calendarId', calendarId); - if (fetchedCalendarId) { + if (calendarId) { const eventsResponse = await bulkEvents( email, // Email пользователя password, // Пароль пользователя token, // Токен сессии - fetchedCalendarId, // ID календаря + calendarId, // ID календаря "2024-10-14T00:00:00+03:00", // Дата начала "2024-10-20T23:59:59+03:00", // Дата окончания personId // ID участника ); console.log('События:', eventsResponse.data); setEvents(eventsResponse.data); - + // cached_at + localStorage.setItem('cached_at', eventsResponse.data.cached_at); // Сохраняем cached_at localstorage + console.log('eventsResponse.data.cached_at', eventsResponse.data.cached_at) // Сохраняем события в localStorage saveEventsToLocalStorage(eventsResponse.data); } @@ -82,7 +86,7 @@ const CalendarRoute = ({ email, password, personId, token }) => { return (
    - {/*
    */} +
    ); }; diff --git a/frontend/src/pages/LoginRoute.jsx b/frontend/src/pages/LoginRoute.jsx index c3afbf1..c0b0617 100644 --- a/frontend/src/pages/LoginRoute.jsx +++ b/frontend/src/pages/LoginRoute.jsx @@ -49,6 +49,7 @@ const LoginRoute = ({onLogin, onSearch}) => { const handleSelect = (person) => { setFullName(person.fullName); // Устанавливаем выбранное имя setPersonId(person.personId); // Сохраняем personId + localStorage.setItem('personId', personId); // Сохраняем personId localstorage setShowSuggestions(false); // Скрываем список после выбора }; diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 9793c54..ed3affd 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -93,4 +93,28 @@ export async function refreshBulkEvents(sessionToken, calendarId, timeMin, timeM } } +// export file +export async function exportICS(sessionToken, calendarId, timeMin, timeMax, attendeePersonId) { + try { + const response = await axios.post( + `${BACKEND_URL}/api/bulk/export_ics/?calendar_id=${calendarId}`, // URL с calendar_id в параметрах + { + timeMin, + timeMax, + size: 50, + attendeePersonId: [attendeePersonId], + }, + { + headers: { + "_netology-on-rails_session": sessionToken, // Токен сессии + "Content-Type": "application/json", + "time_zone": "Europe/Moscow", // Добавляем time_zone в заголовки + }, + } + ); + return response; + } catch (e) { + return e.response; + } +} diff --git a/frontend/src/style/DatePicker.scss b/frontend/src/style/DatePicker.scss new file mode 100644 index 0000000..a423e45 --- /dev/null +++ b/frontend/src/style/DatePicker.scss @@ -0,0 +1,40 @@ +.date-picker-wrapper { + width: fit-content; + margin-left: auto; + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px; + background-color: #f3f4f6; + border-radius: 8px; + font-family: Arial, sans-serif; +} + +.week-display { + display: flex; + align-items: center; +} + +.week-range { + font-size: 16px; + font-weight: bold; +} + +.week-navigation { + display: flex; + margin-left: 10px; +} + +.prev-week-btn, .next-week-btn { + background-color: #e0e0e0; + border: none; + padding: 5px 10px; + margin: 0 5px; + border-radius: 4px; + cursor: pointer; + font-size: 16px; +} + +.prev-week-btn:hover, .next-week-btn:hover { + background-color: #d0d0d0; +} diff --git a/frontend/src/style/calendar.scss b/frontend/src/style/calendar.scss index e5b8648..e0fd383 100644 --- a/frontend/src/style/calendar.scss +++ b/frontend/src/style/calendar.scss @@ -1,48 +1,47 @@ -//&-info { -// height: 28px; -// margin-top: 6px; -// width: 95%; -// text-align: left; -// padding: 5px 11px; -// border: none; -// background: #F46386; -// border-radius: 6px; -// color: #fff; -// font-family: "Roboto", sans-serif; -// font-weight: 700; -// font-size: 13px; -// transition: color 0.3s linear; -// -// &:hover { -// cursor: pointer; -// color: #F46386; -// background-color: #fff; -// border: 2px solid #F46386; -// } -// } - -//&-info-on { -// height: 28px; -// margin-top: 4px; -// width: 95%; -// text-align: left; -// padding: 5px 11px; -// color: #F46386; -// background-color: #fff; -// border: 2px solid #F46386; -// border-radius: 6px; -// font-family: "Roboto", sans-serif; -// font-weight: 700; -// font-size: 13px; -// transition: color 0.3s linear; -// -// &:hover { -// cursor: pointer; -// color: #fff; -// background-color: #F46386; -// border: none; -// } -// } +.deadline { + &-info { + height: 28px; + margin-top: 6px; + width: 95%; + text-align: left; + padding: 5px 11px; + border: none; + background: #F46386; + border-radius: 6px; + color: #fff; + font-family: "Roboto", sans-serif; + font-weight: 700; + font-size: 13px; + transition: color 0.3s linear; + &:hover { + cursor: pointer; + color: #F46386; + background-color: #fff; + border: 2px solid #F46386; + } + } + &-info-on { + height: 28px; + margin-top: 4px; + width: 95%; + text-align: left; + padding: 5px 11px; + color: #F46386; + background-color: #fff; + border: 2px solid #F46386; + border-radius: 6px; + font-family: "Roboto", sans-serif; + font-weight: 700; + font-size: 13px; + transition: color 0.3s linear; + &:hover { + cursor: pointer; + color: #fff; + background-color: #F46386; + border: none; + } + } +} .shedule-table { width: 100%; diff --git a/frontend/src/style/header.scss b/frontend/src/style/header.scss index d02a8e9..ad78dbb 100644 --- a/frontend/src/style/header.scss +++ b/frontend/src/style/header.scss @@ -19,8 +19,9 @@ align-items: center; margin-bottom: 22px; } +} - &-deadline { +.rectangle { display: flex; justify-content: flex-end; flex-direction: row-reverse; @@ -39,7 +40,6 @@ text-decoration: none; } } -} .shedule-export { @@ -210,22 +210,22 @@ text-align: left; } -.calendar-date { - position: absolute; - top: 165px; - right: 248px; - background-color: #ecedf0; - width: 406px; - height: 32px; - border-radius: 6px; - border: 1px solid rgba(217, 217, 217, 0.5); - font-family: "Unbounded", sans-serif; - font-weight: 500; - font-size: 15px; - padding-left: 14px; - transition: color 0.2s linear; - - &:hover { - background-color: #e7e7ea; - } -} \ No newline at end of file +//.calendar-date { +// position: absolute; +// top: 165px; +// right: 248px; +// background-color: #ecedf0; +// width: 406px; +// height: 32px; +// border-radius: 6px; +// border: 1px solid rgba(217, 217, 217, 0.5); +// font-family: "Unbounded", sans-serif; +// font-weight: 500; +// font-size: 15px; +// padding-left: 14px; +// transition: color 0.2s linear; +// +// &:hover { +// background-color: #e7e7ea; +// } +//} \ No newline at end of file