From 43ab8245cc980baa83fd07507cce0a05e35a62dd Mon Sep 17 00:00:00 2001 From: alex karpovich Date: Sat, 5 Oct 2024 21:08:39 +0300 Subject: [PATCH 01/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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 d315737f466c9888a7e005edb42beffdbf71dd43 Mon Sep 17 00:00:00 2001 From: Ivan Popov Date: Fri, 11 Oct 2024 18:28:29 +0300 Subject: [PATCH 09/25] fix ci --- .github/workflows/build.yaml | 16 ++++++++-------- backend/yet_another_calendar/web/application.py | 1 + docker-compose.yaml | 3 ++- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 543ff54..9ec6993 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -26,14 +26,14 @@ jobs: password: ${{ env.REGISTRY_PASSWORD }} - name: Build & Publish backend to Github Container registry run: | - docker build ./backend --tag $REGISTRY_URL/$REGISTRY_USERNAME/calendarit-backend:latest \ - --tag $REGISTRY_URL/$REGISTRY_USERNAME/calendarit-backend:$IMAGE_TAG - docker push $REGISTRY_URL/$REGISTRY_USERNAME/calendarit-backend:latest - docker push $REGISTRY_URL/$REGISTRY_USERNAME/calendarit-backend:$IMAGE_TAG + docker build ./backend --tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_backend:latest \ + --tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_backend:$IMAGE_TAG + docker push $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_backend:latest + docker push $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_backend:$IMAGE_TAG - name: Build & Publish frontend to Github Container registry run: | - docker build ./frontend --tag $REGISTRY_URL/$REGISTRY_USERNAME/calendarit-frontend:latest \ - --tag $REGISTRY_URL/$REGISTRY_USERNAME/calendarit-frontend:$IMAGE_TAG - docker push $REGISTRY_URL/$REGISTRY_USERNAME/calendarit-frontend:latest - docker push $REGISTRY_URL/$REGISTRY_USERNAME/calendarit-frontend:$IMAGE_TAG \ No newline at end of file + docker build ./frontend --tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_frontend:latest \ + --tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_frontend:$IMAGE_TAG + docker push $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_frontend:latest + docker push $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_frontend:$IMAGE_TAG \ No newline at end of file diff --git a/backend/yet_another_calendar/web/application.py b/backend/yet_another_calendar/web/application.py index 589f486..7576453 100644 --- a/backend/yet_another_calendar/web/application.py +++ b/backend/yet_another_calendar/web/application.py @@ -3,6 +3,7 @@ from pathlib import Path from typing import Any +import httpcore from fastapi import FastAPI, HTTPException, Request from fastapi.responses import UJSONResponse from fastapi.staticfiles import StaticFiles diff --git a/docker-compose.yaml b/docker-compose.yaml index 9350fcb..82ac8da 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -5,7 +5,7 @@ services: build: context: backend/ dockerfile: Dockerfile - image: yet_another_calendar:${YET_ANOTHER_CALENDAR_VERSION:-latest} + image: ghcr.io/azamatkomaev/yet_another_calendar_backend:${YET_ANOTHER_CALENDAR_VERSION:-latest} restart: always volumes: - ./backend:/app/src/ @@ -37,6 +37,7 @@ services: build: context: ./frontend target: dev-run + image: ghcr.io/azamatkomaev/yet_another_calendar_frontend:${YET_ANOTHER_CALENDAR_VERSION:-latest} container_name: calendar-frontend restart: always volumes: From af5c122f01203a77d6989da3c11bb904cd017ef5 Mon Sep 17 00:00:00 2001 From: Ivan Popov Date: Fri, 11 Oct 2024 18:29:17 +0300 Subject: [PATCH 10/25] fix ci --- backend/yet_another_calendar/web/application.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/yet_another_calendar/web/application.py b/backend/yet_another_calendar/web/application.py index 7576453..589f486 100644 --- a/backend/yet_another_calendar/web/application.py +++ b/backend/yet_another_calendar/web/application.py @@ -3,7 +3,6 @@ from pathlib import Path from typing import Any -import httpcore from fastapi import FastAPI, HTTPException, Request from fastapi.responses import UJSONResponse from fastapi.staticfiles import StaticFiles From 96c1cc96f7b86306a9c4d805f100715953262a50 Mon Sep 17 00:00:00 2001 From: Ivan Popov Date: Fri, 11 Oct 2024 18:57:30 +0300 Subject: [PATCH 11/25] 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 12/25] 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 13/25] 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 14/25] 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 15/25] 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 16/25] 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 17/25] 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 18/25] 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 19/25] 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 20/25] 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 21/25] 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 22/25] 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 23/25] 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 24/25] 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 25/25] 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