diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6895cba..c41a9ac 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,16 +1,18 @@ import {Route, Routes} from "react-router-dom"; import './App.css' import AppTheme from './theme/AppTheme'; -import TripsTape from './components/TripsTape.tsx'; +import {TripsTape} from './components/TripsTape.tsx'; import {Container, CssBaseline} from "@mui/material"; import AppAppBar from "./components/AppAppBar.tsx"; import {useEffect, useState} from "react"; import {AuthForm} from "./components/AuthForm.tsx"; import {SuccessLoginToast} from "./components/SuccessLoginToast.tsx"; import {Modal, Box} from '@mui/material'; -import {MeService} from "./api/generated"; -import MyTripsTape from "./components/MyTripsTape.tsx"; +import {MeService, TripsService} from "./api/generated"; +import {MyTripsTape} from "./components/MyTripsTape.tsx"; import AppBarProvider from "./AppBarContext"; +import type {ITrip} from "./models/ITrip.ts"; +import Utils from "./services/Utils.ts"; function checkAuthFromStorage(): boolean { const idToken = localStorage.getItem('idToken'); @@ -23,8 +25,22 @@ function App() { const [, setIsProfileComplete] = useState(false); const [toastMessage, setToastMessage] = useState(''); const [user, setUser] = useState<{ studentId?: string } | null>(null); + const [userId, setUserId] = useState(""); + const [globalTrips, setGlobalTrips] = useState([]); + + const getTrips = async () => { + try { + const response = await TripsService.getApiV1Trips(); + const mapped = response.map(Utils.mapTripResponseToITrip); + + setGlobalTrips(mapped); + } catch (e) { + console.error("Ошибка:", e); + } + }; useEffect(() => { + getTrips(); if (checkAuthFromStorage()) { setIsAuthenticated(true); } @@ -36,6 +52,7 @@ function App() { const profile = await MeService.getApiV1Me(); setUser({studentId: `${profile.first_name} ${profile.last_name} (${profile.student_id})`}); + setUserId(profile.id); const isComplete = !!profile.bio && !!profile.social_network_username; @@ -67,42 +84,42 @@ function App() { // @ts-ignore return ( <> - - - + - - - + + + + - {toastMessage && ( - setToastMessage('')} - /> - )} + {toastMessage && ( + setToastMessage('')} + /> + )} @@ -124,8 +141,14 @@ function App() { }} > - }/> - }/> + }/> + await getTrips()} + />} + /> diff --git a/frontend/src/TestData.ts b/frontend/src/TestData.ts index 92ba546..c335dd5 100644 --- a/frontend/src/TestData.ts +++ b/frontend/src/TestData.ts @@ -61,43 +61,53 @@ export const trips: ITrip[] = [ arrival_time: new Date('2025-11-25T15:30:00Z'), transport_type: 'Общественный транспорт', trip_frequency: 'Каждую неделю', - comment: 'Быстрее всего на метро, согласны?' + comment: 'Быстрее всего на метро, согласны?', + firstAddr: 'Площадь Восстания', + lastAddr: 'ИТМО на Кронверском' }, { author: authors[1], - departure_coords: [59.92768158712746,30.36048332235471], - arrival_coords: [59.95718670847085,30.308284464092527], + departure_coords: [59.9369, 30.4801], + arrival_coords: [59.94403, 30.2952], arrival_time: new Date('2025-11-25T20:30:00Z'), transport_type: 'Такси', trip_frequency: 'Разовая', - comment: '' + comment: '', + firstAddr: 'Метро Проспект Большевиков', + lastAddr: 'ИТМО на Биржевой' }, { author: authors[2], - departure_coords: [59.92768158712746,30.36048332235471], - arrival_coords: [59.95718670847085,30.308284464092527], + departure_coords: [59.936562064167354,30.499978000000002], + arrival_coords: [59.92351402504148,30.376095926709972], arrival_time: new Date('2025-11-20T15:30:00Z'), transport_type: 'Общественный транспорт', trip_frequency: 'Каждую неделю', - comment: 'Люблю автобусы' + comment: 'Люблю автобусы', + firstAddr: 'Общежитие на Белорусской', + lastAddr: 'Миргородская 3' }, { author: authors[3], - departure_coords: [59.92768158712746,30.36048332235471], - arrival_coords: [59.95718670847085,30.308284464092527], + departure_coords: [59.984031419684804,30.35699900879305], + arrival_coords: [59.92646000495477,30.339519547126066], arrival_time: new Date('2025-11-20T20:30:00Z'), transport_type: 'Своя машина', trip_frequency: 'Каждую неделю', - comment: 'Готов подвезти' + comment: 'Готов подвезти', + firstAddr: 'Станция метро Лесная', + lastAddr: 'ИТМО на Ломоносова' }, { author: authors[4], - departure_coords: [59.92768158712746,30.36048332235471], - arrival_coords: [59.95718670847085,30.308284464092527], + departure_coords: [59.98401017798144,30.35052918892963], + arrival_coords: [59.92655346472663,30.33910851753994], arrival_time: new Date('2025-11-20T12:00:00Z'), transport_type: 'Общественный транспорт', trip_frequency: 'Каждую неделю', - comment: '' + comment: '', + firstAddr: 'Станция метро Лесная', + lastAddr: 'ИТМО на Ломоносова' }, { author: authors[2], @@ -106,6 +116,8 @@ export const trips: ITrip[] = [ arrival_time: new Date('2025-11-20T12:00:00Z'), transport_type: 'Каршеринг', trip_frequency: 'Каждую неделю', - comment: '' + comment: '', + firstAddr: 'Станция метро Лесная', + lastAddr: 'ИТМО на Ломоносова' }, ]; diff --git a/frontend/src/components/AuthForm.tsx b/frontend/src/components/AuthForm.tsx index ec34dde..2b8eab5 100644 --- a/frontend/src/components/AuthForm.tsx +++ b/frontend/src/components/AuthForm.tsx @@ -105,7 +105,7 @@ export function AuthForm({ onSuccess, onProfileComplete }: AuthFormProps) { {step === "login" ? ( <> - ITMO.TRIP ID + ITMO ID = (props) => { > {/* Трип */} - + {/* Заголовок маршрута */} - {'Площадь восстания'} → {'ИТМО на Кронверском'} + {props.tripData.firstAddr} {} → {props.tripData.lastAddr} @@ -173,6 +173,7 @@ const MyTrip: FC = (props) => { textAlign: 'left', mx: 0, alignSelf: 'flex-start', + mb: -1, '&::before': { content: '""', position: 'absolute', diff --git a/frontend/src/components/MyTripsTape.tsx b/frontend/src/components/MyTripsTape.tsx index 078f141..4ea4564 100644 --- a/frontend/src/components/MyTripsTape.tsx +++ b/frontend/src/components/MyTripsTape.tsx @@ -8,9 +8,20 @@ import {UserInfo} from "./UserInfo.tsx"; import Grid from "@mui/material/Grid"; import {Button} from "@mui/material"; import NewTripModal from "./NewTripModal.tsx"; +import type {ITrip} from "../models/ITrip.ts"; +import {MeService} from "../api/generated"; +import {SuccessLoginToast} from "./SuccessLoginToast.tsx"; -export default function MyTripsTape() { +interface MyTripsTapeProps { + trips: ITrip[] + userId: string + onNewTrip: () => Promise; +} + +export const MyTripsTape: React.FC = (props) => { const [newTripOpen, setNewTripOpen] = useState(false); + const [toastMessage, setToastMessage] = useState(''); + const [userId, setUserId] = useState(""); const {setAction, reset} = useAppBarAction(); useEffect(() => { @@ -22,6 +33,16 @@ export default function MyTripsTape() { return () => reset(); }, [setAction, reset]); + useEffect(() => { + fetchAndSetUserId() + }, []); + + const fetchAndSetUserId = async () => { + const meResponse = await MeService.getApiV1Me(); + setUserId(meResponse.id) + return; + } + return ( @@ -44,33 +65,35 @@ export default function MyTripsTape() { + {toastMessage && ( + setToastMessage('')} + /> + )} + -
- -
-
- -
-
- -
- {/*{trips.map((tr, index) => (*/} - {/*
*/} - {/* */} - {/*
*/} - {/*))}*/} + {props.trips.filter(tr => tr.author.id === props.userId || tr.author.id === userId).map((tr, index) => ( +
+ +
+ ))}
{newTripOpen && setNewTripOpen(false)} + close={() => { + setNewTripOpen(false) + setToastMessage("Новая поездка успешно добавлена!") + }} + onNewTrip={props.onNewTrip} />}
); diff --git a/frontend/src/components/NewTripModal.tsx b/frontend/src/components/NewTripModal.tsx index 0e34828..4bff9e6 100644 --- a/frontend/src/components/NewTripModal.tsx +++ b/frontend/src/components/NewTripModal.tsx @@ -23,6 +23,7 @@ import Utils from "../services/Utils.ts"; interface NewTripProps { isOpen: boolean; close: any; + onNewTrip: () => Promise; } interface NewTripState { @@ -54,14 +55,14 @@ const steps = ["Отправление", "Прибытие", "Дополните const NewTripModal: FC = (props) => { const [newTripState, setNewTripState] = useState({ - arrival_time: undefined, + arrival_time: new Date(), departure_time: new Date(), transport_type_id: undefined, comment: "", }) const [activeStep, setActiveStep] = useState(0); - const [errors, setErrors] = useState(); + const [_, setErrors] = useState(); const [transportTypes, setTransportTypes] = useState(); const [locationTypes, setLocationTypes] = useState(); @@ -142,6 +143,7 @@ const NewTripModal: FC = (props) => { } await TripsService.postApiV1Trips(tripRequest) + props.onNewTrip() clearModal() props.close() } catch (e) { @@ -167,9 +169,6 @@ const NewTripModal: FC = (props) => { Новая поездка - {errors && ( -

{errors}

- )} {steps.map((label) => ( @@ -246,7 +245,7 @@ const NewTripModal: FC = (props) => { label="Время прибытия" type="datetime-local" variant='standard' - value={DateTimeUtils.toISOString(newTripState.departure_time) ?? ''} + value={DateTimeUtils.toISOString(newTripState.arrival_time) ?? ''} onChange={(e) => { setNewTripState(prevState => ({ ...prevState, diff --git a/frontend/src/components/Trip.tsx b/frontend/src/components/Trip.tsx index 52540cf..002fda3 100644 --- a/frontend/src/components/Trip.tsx +++ b/frontend/src/components/Trip.tsx @@ -87,7 +87,7 @@ const Trip: FC = (props) => { - {'Площадь восстания'} {} → {'ИТМО на Кронверском'} + {props.tripData.firstAddr} {} → {props.tripData.lastAddr} diff --git a/frontend/src/components/TripsTape.tsx b/frontend/src/components/TripsTape.tsx index 644b842..3ef4122 100644 --- a/frontend/src/components/TripsTape.tsx +++ b/frontend/src/components/TripsTape.tsx @@ -1,66 +1,18 @@ -import { useEffect, useState } from "react"; import { Box } from "@mui/material"; import Masonry from "@mui/lab/Masonry"; -import Filter from "./Filter"; import Trip from "./Trip"; -import {type TripResponse, TripsService, type UserResponse} from "../api/generated"; import type {ITrip} from "../models/ITrip.ts"; -import type {IAuthor} from "../models/IAuhor.ts"; -// eslint-disable-next-line react-refresh/only-export-components -export const mapUserToAuthor = (u: UserResponse): IAuthor => ({ - id: Number(u.id), - name: `${u.last_name} ${u.first_name}${u.middle_name ? ' ' + u.middle_name : ''}`, - avatar: u.avatar_url ?? '', - courseNumber: Number(u.student_id), // <-- уточни, если иначе - facultyName: u.faculty, - tg_username: u.social_network_username ?? '', - bio: u.bio ?? '', -}); - -// eslint-disable-next-line react-refresh/only-export-components -export const mapTripResponseToITrip = (t: TripResponse): ITrip => { - return { - trip_frequency: "Всегда", - arrival_coords: [ - t.arrival_location.latitude, - t.arrival_location.longitude - ], - departure_coords: [ - t.departure_location.latitude, - t.departure_location.longitude - ], - arrival_time: new Date(t.arrival_time!), - transport_type: t.transport_type.name_ru ?? "Неизвестно", - comment: t.comment ?? "", - author: mapUserToAuthor(t.creator) - }; -}; - -export default function TripsTape() { - const [trips, setTrips] = useState([]); - - useEffect(() => { - getTrips(); - }, []); - - const getTrips = async () => { - try { - const response = await TripsService.getApiV1Trips(); - const mapped = response.map(mapTripResponseToITrip); - - setTrips(mapped); - } catch (e) { - console.error("Ошибка:", e); - } - }; +interface TripsTapeProps { + trips: ITrip[] +} +export const TripsTape: React.FC = (props) => { return ( - - {trips.map((tr, index) => ( + {props.trips.map((tr, index) => (
diff --git a/frontend/src/models/IAuhor.ts b/frontend/src/models/IAuhor.ts index 4ce82b9..8475d48 100644 --- a/frontend/src/models/IAuhor.ts +++ b/frontend/src/models/IAuhor.ts @@ -1,5 +1,5 @@ export interface IAuthor { - id: number, + id: string, name: string, avatar: string, courseNumber: number, diff --git a/frontend/src/models/ITrip.ts b/frontend/src/models/ITrip.ts index 65fd017..63cc9d6 100644 --- a/frontend/src/models/ITrip.ts +++ b/frontend/src/models/ITrip.ts @@ -7,5 +7,7 @@ export interface ITrip { arrival_time: Date, transport_type: string, trip_frequency: string, - comment: string + comment: string, + firstAddr: string, + lastAddr: string, } \ No newline at end of file diff --git a/frontend/src/services/Utils.ts b/frontend/src/services/Utils.ts index e5c25ed..c1a07c3 100644 --- a/frontend/src/services/Utils.ts +++ b/frontend/src/services/Utils.ts @@ -1,3 +1,7 @@ +import type {TripResponse, UserResponse} from "../api/generated"; +import type {IAuthor} from "../models/IAuhor.ts"; +import type {ITrip} from "../models/ITrip.ts"; + export default class Utils { static getLocationTypeName(locationTypeCode: string) { if (locationTypeCode === 'METRO_STATION') return 'Метро' @@ -6,4 +10,32 @@ export default class Utils { else if (locationTypeCode === 'CUSTOM') return 'Свой адрес' else return locationTypeCode } + + static mapUserToAuthor = (u: UserResponse): IAuthor => ({ + id: u.id, + name: `${u.last_name} ${u.first_name}${u.middle_name ? ' ' + u.middle_name : ''}`, + avatar: u.avatar_url ?? '', + courseNumber: 1, // Пока на бэкенде в БД не добавили это поле + facultyName: u.faculty, + tg_username: u.social_network_username ?? '', + bio: u.bio ?? '', + }); + + static mapTripResponseToITrip = (t: TripResponse): ITrip => ({ + arrival_coords: [ + t.arrival_location.latitude, + t.arrival_location.longitude + ], + departure_coords: [ + t.departure_location.latitude, + t.departure_location.longitude + ], + arrival_time: new Date(t.arrival_time!), + transport_type: t.transport_type.name_ru ?? "Неизвестно", + comment: t.comment ?? "", + author: Utils.mapUserToAuthor(t.creator), + trip_frequency: "", + firstAddr: t.departure_location.name, + lastAddr: t.arrival_location.name + }); }