diff --git a/public/icons/apart.svg b/public/icons/apart.svg new file mode 100644 index 0000000..47a41f2 --- /dev/null +++ b/public/icons/apart.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/bar.svg b/public/icons/bar.svg new file mode 100644 index 0000000..05d07c8 --- /dev/null +++ b/public/icons/bar.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/bus.svg b/public/icons/bus.svg new file mode 100644 index 0000000..5ea2b56 --- /dev/null +++ b/public/icons/bus.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/bycicle.svg b/public/icons/bycicle.svg new file mode 100644 index 0000000..402a91c --- /dev/null +++ b/public/icons/bycicle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/cafe.svg b/public/icons/cafe.svg new file mode 100644 index 0000000..8b31e4c --- /dev/null +++ b/public/icons/cafe.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/car.svg b/public/icons/car.svg new file mode 100644 index 0000000..2aeee53 --- /dev/null +++ b/public/icons/car.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/exhibition.png b/public/icons/exhibition.png new file mode 100644 index 0000000..356084c Binary files /dev/null and b/public/icons/exhibition.png differ diff --git a/public/icons/hostel.svg b/public/icons/hostel.svg new file mode 100644 index 0000000..d3f2742 --- /dev/null +++ b/public/icons/hostel.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/hotel.svg b/public/icons/hotel.svg new file mode 100644 index 0000000..4c49316 --- /dev/null +++ b/public/icons/hotel.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/liked.svg b/public/icons/liked.svg new file mode 100644 index 0000000..8950bdb --- /dev/null +++ b/public/icons/liked.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/monument.png b/public/icons/monument.png new file mode 100644 index 0000000..93459e0 Binary files /dev/null and b/public/icons/monument.png differ diff --git a/public/icons/museum.png b/public/icons/museum.png new file mode 100644 index 0000000..c738f0f Binary files /dev/null and b/public/icons/museum.png differ diff --git a/public/icons/not_found.jpeg b/public/icons/not_found.jpeg new file mode 100644 index 0000000..2f5d192 Binary files /dev/null and b/public/icons/not_found.jpeg differ diff --git a/public/icons/park.png b/public/icons/park.png new file mode 100644 index 0000000..2e5ba8a Binary files /dev/null and b/public/icons/park.png differ diff --git a/public/icons/plane.svg b/public/icons/plane.svg new file mode 100644 index 0000000..605b99c --- /dev/null +++ b/public/icons/plane.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/rest.svg b/public/icons/rest.svg new file mode 100644 index 0000000..a786657 --- /dev/null +++ b/public/icons/rest.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/scooter.svg b/public/icons/scooter.svg new file mode 100644 index 0000000..0386b02 --- /dev/null +++ b/public/icons/scooter.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/stars/1.svg b/public/icons/stars/1.svg new file mode 100644 index 0000000..3b49f51 --- /dev/null +++ b/public/icons/stars/1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/stars/2.svg b/public/icons/stars/2.svg new file mode 100644 index 0000000..1d27c67 --- /dev/null +++ b/public/icons/stars/2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/stars/3.svg b/public/icons/stars/3.svg new file mode 100644 index 0000000..a597297 --- /dev/null +++ b/public/icons/stars/3.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/stars/4.svg b/public/icons/stars/4.svg new file mode 100644 index 0000000..8feaede --- /dev/null +++ b/public/icons/stars/4.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/stars/5.svg b/public/icons/stars/5.svg new file mode 100644 index 0000000..95621b4 --- /dev/null +++ b/public/icons/stars/5.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/train.svg b/public/icons/train.svg new file mode 100644 index 0000000..6047bcd --- /dev/null +++ b/public/icons/train.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/unesco.png b/public/icons/unesco.png new file mode 100644 index 0000000..c285401 Binary files /dev/null and b/public/icons/unesco.png differ diff --git a/public/icons/unliked.svg b/public/icons/unliked.svg new file mode 100644 index 0000000..0c73921 --- /dev/null +++ b/public/icons/unliked.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/walk.svg b/public/icons/walk.svg new file mode 100644 index 0000000..9ecd56d --- /dev/null +++ b/public/icons/walk.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/consts.tsx b/src/consts.tsx index 883d42b..3b3b252 100644 --- a/src/consts.tsx +++ b/src/consts.tsx @@ -4,4 +4,6 @@ export const backend = axios.create({ baseURL: 'https://dev2.akarpov.ru/api/', timeout: 10000, headers: {'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjg3Mjk0NTExLCJpYXQiOjE2ODQ3MDI1MTEsImp0aSI6ImUwNGNjZGViMzA0NzQxYTlhYzJhODRhNzc1YWFkZTIxIiwidXNlcl9pZCI6N30.M-F08v6Wit5Bbm668m84JThyDX5yZhzsh3_GFh3nzXM'} - }); \ No newline at end of file + }); + + \ No newline at end of file diff --git a/src/elements/AttractionCard/index.tsx b/src/elements/AttractionCard/index.tsx new file mode 100644 index 0000000..c021361 --- /dev/null +++ b/src/elements/AttractionCard/index.tsx @@ -0,0 +1,39 @@ +import React, { useState } from "react"; +import { backend } from "../../consts"; +import { Button } from "../Button"; +import './style.css' + +export interface AttractionCardIE{ + city:string, + description: string, + oid: string, + title:string, + type:string +} + +export const AttractionCard:React.FC = (props) =>{ + const [liked, setLiked] = useState(false) + + const onLiked = ()=>{ + backend.get('/onboarding/' + props.oid + '/add_to_favorites/') + setLiked(!liked) + } + + return( + + + + + onLiked()} src={liked? 'icons/liked.svg':'icons/unliked.svg'}> + + + + {props.title} + + {props.description} + + + + + ); +} \ No newline at end of file diff --git a/src/elements/AttractionCard/style.css b/src/elements/AttractionCard/style.css new file mode 100644 index 0000000..f6386d4 --- /dev/null +++ b/src/elements/AttractionCard/style.css @@ -0,0 +1,93 @@ +.hotelCard{ + width: 250px; + padding: 0px 15px 15px 15px; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: space-between; + background: #F5F5F5; + border-radius: 20px; + height: 350px; + +} +h3{ + margin: 0px; +} + +.likeHotelBtn{ + width: 40px; + height: 40px; + cursor: pointer; +} + +.likeHotelBtn:hover{ + opacity: 0.5; +} + +.ratingTile{ + width: 40px; + height: 40px; + background: #007470; + border-radius: 8px; + display: flex; + justify-content: center; + align-items: center; + font-size: 18px; + color:white; + font-weight: 500; +} + +.hotelCardTools{ + position: relative; + top:15px; + display: flex; + flex-direction: row; + width: 250px; + justify-content: space-between; +} + +.hotelCardRow{ + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap:15px; + font-size: 12px; +} + +.hotelBtn{ + margin-top: 15px; +} + +.openHotelCardBG{ + position: fixed; + display: flex; + justify-content: center; + align-items: center; + background-color: rgba(0,0,0, 0.2); + left: 0px; + top:0px; + width: 100%; + height: 100vh; + z-index: 10000; + +} + + +.openHotelCard{ + width: 75%; + height: 75vh; + overflow-y: scroll; + padding: 50px; + background-color: white; + z-index: 10000; + border-radius: 20px; +} + + +.hotelImg{ + margin-top: -65px; + margin-left: -15px; + width: 280px; + height: 240px; +} \ No newline at end of file diff --git a/src/elements/ChoiceIcon/index.tsx b/src/elements/ChoiceIcon/index.tsx new file mode 100644 index 0000000..eab3a21 --- /dev/null +++ b/src/elements/ChoiceIcon/index.tsx @@ -0,0 +1,73 @@ +import React, { useState } from "react"; +import './style.css' +export interface ChoiceIconIE{ + onChange: (key:string[]) => void + options:{ + icon:string, + name: string, + key:string + }[] +} + +export const ChoiceIcon:React.FC = (props) => { + let startActives = new Array() + + for (let i=0; i < props.options.length; i++){ + startActives.push(false) + } + + const [state, setState] = useState({ + actives: startActives, + keys: new Array() + }) + + + const onChange = (key:string, index:number) =>{ + let localActives = state.actives + localActives[index] = !localActives[index] + + if (localActives[index]){ + props.onChange([...state.keys, key]) + setState( + { + keys: [...state.keys, key], + actives: localActives + } + ) + + } else{ + let localKeys = state.keys + if (localKeys.indexOf(key) != -1){ + localKeys.splice(localKeys.indexOf(key), 1) + if (localKeys.length > 0){ + props.onChange(localKeys) + } + setState( + { + keys: localKeys, + actives: localActives + } + ) + + } + } + } + + let icons = new Array() + + props.options.forEach((item, index)=>{ + icons.push( + onChange(item.key, index)}> + + {item.name} + + ) + }) + return( + + { + icons + } + + ); +} \ No newline at end of file diff --git a/src/elements/ChoiceIcon/style.css b/src/elements/ChoiceIcon/style.css new file mode 100644 index 0000000..72c150c --- /dev/null +++ b/src/elements/ChoiceIcon/style.css @@ -0,0 +1,44 @@ +.choiceIconWrapper{ + display: flex; + flex-direction: row; + gap:30px; + align-items: center; +} + +.choiceBtn{ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 30px; + border: 2px solid #FAEFDB; + background: #FAEFDB; + padding: 15px; + border-radius: 12px; + cursor: pointer; + transition: 0.3s; +} + +.choiceBtn:hover{ + opacity: 0.5; +} +.choiceBtnActive{ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 30px; + background: #FAEFDB; + border: 2px solid #FFCF08; + padding: 15px; + border-radius: 12px; + cursor: pointer; + transition: 0.3s; +} +.choiceBtnWrapper{ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap:10px +} \ No newline at end of file diff --git a/src/elements/FavoriteCard/style.css b/src/elements/FavoriteCard/style.css index 3ebc72c..ad2314a 100644 --- a/src/elements/FavoriteCard/style.css +++ b/src/elements/FavoriteCard/style.css @@ -11,8 +11,8 @@ transform: translateY(-60px) translateX(10px); } .likeButton:hover{ - transform: scale(1.1); -} +opacity: 0.5 +;} .titleFavoriteCard{ margin-top: 10px; font-size: 18px; diff --git a/src/elements/HotelCard/index.tsx b/src/elements/HotelCard/index.tsx new file mode 100644 index 0000000..79c0b04 --- /dev/null +++ b/src/elements/HotelCard/index.tsx @@ -0,0 +1,91 @@ +import React, { useState } from "react"; +import { Button } from "../Button"; +import './style.css' + +export interface HotelCardIE{ + address:string, + can_buy:boolean, + city:string, + description:string, + email:string, + lat:number, + lon:number, + phones: { + id:number, + name:string, + number: string + }[], + place?:any, + polymorphic_ctype:number, + priority: boolean, + region: string, + rooms:{ + amenities:string[], + description:string, + images: { + source:{ + id:string + } + }[], + integration_id:string, + name:string, + rate_plans:{ + description:string, + integration_id:string, + name:string + }[] + }[], + sort:number, + source: any, + stars:number, + title: string +} + +export const HotelCard:React.FC = (props) =>{ + const [liked, setLiked] = useState(false) + const [opened, setOpened] = useState(false) + console.log(props) + return( + + { + opened? + + {props.title} + + {props.address} + + {props.phones.map((phone)=>{phone.name} {phone.number})} + + + + {props.rooms.map((room)=> + {room.name} + {room.description} + + )} + + setOpened(!opened)}>Закрыть + + :null + } + + + {props.stars}* + setLiked(!liked)} src={liked? 'icons/liked.svg':'icons/unliked.svg'}> + + + + {props.title} + + {props.address} + { + props.rooms == null? null:{props.rooms.length} видов номеров + + } + + setOpened(!opened)}>Посмотреть + + + + ); +} \ No newline at end of file diff --git a/src/elements/HotelCard/style.css b/src/elements/HotelCard/style.css new file mode 100644 index 0000000..7c5caf1 --- /dev/null +++ b/src/elements/HotelCard/style.css @@ -0,0 +1,93 @@ +.hotelCard{ + width: 250px; + padding: 0px 15px 15px 15px; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: space-between; + background: #F5F5F5; + border-radius: 20px; + height: 380px; + +} +h3{ + margin: 0px; +} + +.likeHotelBtn{ + width: 40px; + height: 40px; + cursor: pointer; +} + +.likeHotelBtn:hover{ + opacity: 0.5; +} + +.ratingTile{ + width: 40px; + height: 40px; + background: #007470; + border-radius: 8px; + display: flex; + justify-content: center; + align-items: center; + font-size: 18px; + color:white; + font-weight: 500; +} + +.hotelCardTools{ + position: relative; + top:15px; + display: flex; + flex-direction: row; + width: 250px; + justify-content: space-between; +} + +.hotelCardRow{ + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap:15px; + font-size: 12px; +} + +.hotelBtn{ + margin-top: 15px; +} + +.openHotelCardBG{ + position: fixed; + display: flex; + justify-content: center; + align-items: center; + background-color: rgba(0,0,0, 0.2); + left: 0px; + top:0px; + width: 100%; + height: 100vh; + z-index: 10000; + +} + + +.openHotelCard{ + width: 75%; + height: 75vh; + overflow-y: scroll; + padding: 50px; + background-color: white; + z-index: 10000; + border-radius: 20px; +} + + +.hotelImg{ + margin-top: -65px; + margin-left: -15px; + width: 280px; + height: 240px; +} \ No newline at end of file diff --git a/src/elements/Prefernces/index.tsx b/src/elements/Prefernces/index.tsx new file mode 100644 index 0000000..cb77894 --- /dev/null +++ b/src/elements/Prefernces/index.tsx @@ -0,0 +1,327 @@ +import React, { useEffect, useState } from "react"; +import { Carousel, Tabs } from 'antd'; +import type { TabsProps } from 'antd'; +import { Button } from "../Button"; +import './style.css' +import { ChoiceIcon, ChoiceIconIE } from "../ChoiceIcon"; +import { syncBuiltinESMExports } from "module"; +import axios from "axios"; +import { backend } from "../../consts"; +import { HotelCard, HotelCardIE } from "../HotelCard"; +import { AttractionCard, AttractionCardIE } from "../AttractionCard"; +export const Prefernces = () =>{ + const [activeTab, setActiveTab] = useState(1) + const [cities, setCities] = useState([]) + const [regions, setRegions] = useState([]) + const [hotels, setHotels] = useState([]) + const [events, setEvents] = useState([]) + + useEffect(()=>{ + if (cities.length == 0){ + backend.get('/data/cities').then((response)=>setCities(response.data)) + backend.get('/data/regions').then((response)=>setRegions(response.data)) + } + + }) + + + const transportOptions = { + options:[ + { + name: 'Автобус', + icon: 'icons/bus.svg', + key: 'bus' + }, + { + name: 'Машина', + icon: 'icons/car.svg', + key: 'car' + }, + { + name: 'Самолет', + icon: 'icons/plane.svg', + key: 'plane' + }, + { + name: 'Поезд', + icon: 'icons/train.svg', + key: 'train' + } + ], + onChange: (e)=>console.log(e) + } as ChoiceIconIE + + + const transportCityOptions = { + options:[ + { + name: 'Транспорт', + icon: 'icons/bus.svg', + key: 'bus' + }, + { + name: 'Машина', + icon: 'icons/car.svg', + key: 'car' + }, + { + name: 'Велосипед', + icon: 'icons/bycicle.svg', + key: 'bycicle' + }, + { + name: 'Самокат', + icon: 'icons/scooter.svg', + key: 'scooter' + }, + { + name: 'Пешком', + icon: 'icons/walk.svg', + key: 'walk' + } + ], + onChange: (e)=>console.log(e) + } as ChoiceIconIE + + const hotelOptions = { + options:[ + { + name: 'Отель', + icon: 'icons/hotel.svg', + key: 'hotel' + }, + { + name: 'Хостел', + icon: 'icons/hostel.svg', + key: 'hostel' + }, + { + name: 'Апартаменты', + icon: 'icons/apart.svg', + key: 'apart' + }, + ], + onChange: (e)=>console.log(e) + } as ChoiceIconIE + + + + const ratingOptions = { + options:[ + { + name: '1', + icon: 'icons/stars/1.svg', + key: '1' + }, + { + name: '2', + icon: 'icons/stars/2.svg', + key: '2' + }, + { + name: '3', + icon: 'icons/stars/3.svg', + key: '3' + }, + { + name: '4', + icon: 'icons/stars/4.svg', + key: '4' + }, + { + name: '5', + icon: 'icons/stars/5.svg', + key: '5' + }, + ], + onChange: (e)=>backend.post('/onboarding/hotels/', { + stars: e.map((value)=>Number(value)) + }).then((response)=>setHotels(response.data.hotels)) + } as ChoiceIconIE + + + const eatOptions = { + options:[ + { + name: 'Кафе', + icon: 'icons/cafe.svg', + key: 'cafe' + }, + { + name: 'Ресторан', + icon: 'icons/rest.svg', + key: 'rest' + }, + { + name: 'Бар', + icon: 'icons/bar.svg', + key: 'bar' + }, + ], + onChange: (e)=>console.log(e) + } as ChoiceIconIE + + const eatRatingOptions = { + options:[ + { + name: '1', + icon: 'icons/stars/1.svg', + key: '1' + }, + { + name: '2', + icon: 'icons/stars/2.svg', + key: '2' + }, + { + name: '3', + icon: 'icons/stars/3.svg', + key: '3' + }, + { + name: '4', + icon: 'icons/stars/4.svg', + key: '4' + }, + { + name: '5', + icon: 'icons/stars/5.svg', + key: '5' + }, + ], + onChange: (e)=>console.log(e) + } as ChoiceIconIE + + const attractionOptions = { + options:[ + { + name: 'Park', + icon: 'icons/park.png', + key: 'park' + }, + { + name: 'Monument', + icon: 'icons/monument.png', + key: 'monument' + }, + { + name: 'Museum', + icon: 'icons/museum.png', + key: 'museum' + }, + { + name: 'Unesco', + icon: 'icons/unesco.png', + key: 'unesco' + }, + { + name: 'Exhibition', + icon: 'icons/exhibition.png', + key: 'exhibition' + }, + ], + onChange: (e)=>backend.post('/onboarding/event/', { + stars: e.map((value)=>Number(value)) + }).then((response)=>setEvents(response.data.events)) + + } as ChoiceIconIE + + + + + + + + + const items: TabsProps['items'] = [ + { + key: '1', + label: `Транспорт`, + children: + + + Как вы предпочитаете добираться + + + + Как вы предпочитаете передвигаться на месте + + + + , + }, + { + key: '2', + label: `Проживание`, + children: + + Где вы предпочитаете остановится + + + + Рейтинг + + + + Выберите понравившеися вам отели + + { + hotels.map((hotel:HotelCardIE)=>) + } + + + , + }, + { + key: '3', + label: `Питание`, + children: + + Где вы предпочитаете есть + + + + Рейтинг + + + + }, + { + key: '4', + label: `Развлечения`, + children: + + Что вы хотите увидеть + + + + Выберите самое интересное + + { + events.map((event:AttractionCardIE)=>{ + return + }) + } + + + , + }, + ]; + + + + + return( + + + + Мои предпочтения + Отметьте свои предпочтения, чтобы мы смогли предоставить лучшие рекомендации туров + + setActiveTab(Number(e))} /> + setActiveTab((activeTab+1)%5)}>Далее + + + + ); +} \ No newline at end of file diff --git a/src/elements/Prefernces/style.css b/src/elements/Prefernces/style.css new file mode 100644 index 0000000..8def387 --- /dev/null +++ b/src/elements/Prefernces/style.css @@ -0,0 +1,38 @@ +.prefs{ + width: 60%; + position: fixed; + top:0px; + right:0px; + height: 100vh; + display: flex; + flex-direction: column; + justify-content: flex-start; + padding: 25px; + background-color: white; +} + +.prefsHeadWrapper{ + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.prefsbg{ + width: 100%; + height: 100vh; + background-color: rgba(0,0,0,0.2); +} + +.hotelsCardWrapper{ + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap:30px; + height: 400px; + justify-content: center; + align-items: center; + overflow-y: scroll; + margin-bottom: 15px; +} \ No newline at end of file diff --git a/src/pages/Main/index.tsx b/src/pages/Main/index.tsx index f982d32..e1f7d95 100644 --- a/src/pages/Main/index.tsx +++ b/src/pages/Main/index.tsx @@ -1,5 +1,6 @@ -import { AutoComplete, DatePicker, Input, Checkbox } from 'antd'; -import react, { useState } from 'react' +import { AutoComplete, DatePicker, Input, Checkbox, Select } from 'antd'; +import react, { useEffect, useState } from 'react' +import { backend } from '../../consts'; import { Button } from '../../elements/Button'; import { FavoriteCard, FavoriteCardIE } from '../../elements/FavoriteCard'; import { GenerateCard } from '../../elements/GenerateCard'; @@ -9,6 +10,14 @@ import './style.css' export const Main: react.FC = () => { const { RangePicker } = DatePicker; + const [cities, setCities] = useState([]) + + useEffect(()=>{ + if (cities.length == 0){ + backend.get('/data/cities').then((response)=>setCities(response.data)) + } + }) + const TourPropsCard = { name: 'Я покажу тебе Москву', @@ -77,9 +86,25 @@ export const Main: react.FC = () => { Фильтры - - - + + (option?.label ?? '').toLowerCase().includes(input.toLowerCase()) + } + options={cities.map((city:any)=>{ + return { + value:city.oid, + label: city.title + } + } + )} + /> + setDates(e as any)} diff --git a/src/pages/Main/style.css b/src/pages/Main/style.css index 78c9d9b..35245d2 100644 --- a/src/pages/Main/style.css +++ b/src/pages/Main/style.css @@ -113,6 +113,10 @@ display: flex; flex-direction: column; gap:10px + } +.ant-select-selector, .ant-picker{ + border: 0px !important; +} diff --git a/src/router.tsx b/src/router.tsx index ed5248d..3144868 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -7,6 +7,7 @@ import { Login } from './pages/Login'; import { Main } from './pages/Main'; import { EventMatch } from './pages/EventMatch'; import { GenerateTour } from './pages/GenerateTour'; +import { Prefernces } from './elements/Prefernces'; const routes = [ @@ -33,6 +34,10 @@ const routes = [ { path: '/generate', element: + }, + { + path: '/prefs', + element: } ]