Compare commits

..

5 Commits

27 changed files with 1109 additions and 666 deletions

4
.gitignore vendored
View File

@ -128,4 +128,6 @@ dist
.yarn/unplugged .yarn/unplugged
.yarn/build-state.yml .yarn/build-state.yml
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
ngrok.exe

1071
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,17 @@
import React from 'react'; import React, {useEffect} from 'react';
import Main from './container/main'; import Main from './container/main';
import useTelegram from './container/hooks/useTelegram';
const App = () => { const App = () => {
const { isScriptLoaded, onClose, onToggleButton, tg, user_id, username } = useTelegram();
useEffect(() => {
if (isScriptLoaded && tg) {
tg.ready(); // Инициализация WebApp после загрузки скрипта
console.log('Telegram WebApp готов:', tg);
}
}, [isScriptLoaded, tg]);
return <Main />; return <Main />;
}; };

View File

@ -1,36 +1,61 @@
import { useEffect, useState } from 'react';
declare global { declare global {
interface Window { interface Window {
Telegram: { Telegram: {
WebApp: any; WebApp: any;
}; };
}
} }
const useTelegram = () => {
const tg = window.Telegram.WebApp;
const onClose = () => {
tg.close()
}
const onToggleButton = () => {
if(tg.MainButton.isVisible) {
tg.MainButton.hide();
} else {
tg.MainButton.show();
}
}
return {
onClose,
onToggleButton,
tg,
user_id: tg.initDataUnsafe?.user?.id,
user: tg.initDataUnsafe?.user,
username: tg.initDataUnsafe?.user?.username,
queryId: tg.initDataUnsafe?.query_id
}
} }
export default useTelegram; const useTelegram = () => {
const [isScriptLoaded, setIsScriptLoaded] = useState(false);
useEffect(() => {
// Проверяем, был ли скрипт уже загружен
if (!window.Telegram) {
// Если Telegram API еще не доступен, загружаем скрипт
const script = document.createElement('script');
script.src = 'https://telegram.org/js/telegram-web-app.js';
script.async = true;
script.onload = () => {
setIsScriptLoaded(true); // Скрипт загружен
};
script.onerror = () => {
console.error('Ошибка при загрузке Telegram Web App SDK');
};
document.body.appendChild(script);
} else {
// Если Telegram Web App уже доступен, просто обновляем состояние
setIsScriptLoaded(true);
}
}, []);
// Логика, которая будет выполняться, когда скрипт загружен
const tg = window.Telegram?.WebApp;
const onClose = () => {
tg?.close();
};
const onToggleButton = () => {
if (tg?.MainButton?.isVisible) {
tg.MainButton.hide();
} else {
tg.MainButton.show();
}
};
return {
isScriptLoaded,
onClose,
onToggleButton,
tg,
user_id: tg?.initDataUnsafe?.user?.id,
user: tg?.initDataUnsafe?.user,
username: tg?.initDataUnsafe?.user?.username,
queryId: tg?.initDataUnsafe?.query_id,
};
};
export default useTelegram;

View File

@ -1,28 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { getNavigationsValue } from '@brojs/cli';
const navigations: Array<{ name: string; href: string }> = [
{
name: 'Регистрация',
href: getNavigationsValue('sberhubproject.signup')
}
];
const Header = (): React.ReactElement => {
return (
<header>
<ul>
{navigations.map((item) => {
return (
<li key={item.name}>
<Link to={item.href}>{item.name}</Link>
</li>
);
})}
</ul>
</header>
);
};
export default Header;

View File

@ -0,0 +1,7 @@
import styled from '@emotion/styled';
import AppBar from '@mui/material/AppBar';
export const AppBarStyled = styled(AppBar)`
background-color: var(--tg-theme-button-color);
`;

View File

@ -0,0 +1,18 @@
import React from 'react';
import Logo from './logo';
import { AppBarStyled } from './index.style';
import Toolbar from '@mui/material/Toolbar';
const Header = (): React.ReactElement => {
return (
<header>
<AppBarStyled position="static">
<Toolbar>
<Logo />
</Toolbar>
</AppBarStyled>
</header>
);
};
export default Header;

View File

@ -0,0 +1,14 @@
import React from 'react';
import styled from '@emotion/styled';
import logoPng from './logo.png';
const LogoStyled = styled.img`
height: 40px;
padding: 8px;
`;
const Logo = () => {
return <LogoStyled src={logoPng} alt={'logo'} />;
};
export default Logo;

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,60 @@
import styled from '@emotion/styled';
import TextField from '@mui/material/TextField';
export const AboutStyled = styled(TextField)`
& .MuiInputBase-input {
color: var(--tg-theme-text-color,rgba(0, 0, 0, 0.6));
}
& .MuiFormLabel-root {
color: var(--tg-theme-text-color, rgba(0, 0, 0, 0.6));
}
& .Mui-focused .MuiFormLabel-root {
color: var(--tg-theme-label-focus-color, #0066ff);
}
& .MuiOutlinedInput-root {
fieldset {
border-color: var(--tg-theme-border-color, #cccccc);
}
&:hover fieldset {
border-color: var(--tg-theme-border-hover, #888888);
}
&.Mui-focused fieldset {
border-color: var(--tg-theme-border-focused, #0066ff);
}
}
@media (prefers-color-scheme: dark) {
& .MuiInputBase-input {
color: var(--tg-theme-text-color-dark, #ffffff);
}
& .MuiFormLabel-root {
color: var(--tg-theme-label-color-dark, #ffffff);
}
& .Mui-focused .MuiFormLabel-root {
color: var(--tg-theme-label-focus-color-dark, #0066ff);
}
& .MuiOutlinedInput-root {
fieldset {
border-color: var(--tg-theme-border-color-dark, #ffffff);
}
&:hover fieldset {
border-color: var(--tg-theme-border-hover-dark, #777777);
}
&.Mui-focused fieldset {
border-color: var(--tg-theme-border-focused-dark, #0066ff);
}
}
}
`;

View File

@ -0,0 +1,19 @@
import * as React from 'react';
import { AboutStyled } from './index.style';
const About = ({rows, id, label, name, placeholder="Введите текст...", className = null}): React.ReactElement => {
return (
<AboutStyled
fullWidth
multiline
rows={rows}
id={id}
label={label}
name={name}
placeholder={placeholder}
className={className}
/>
);
}
export default About;

View File

@ -0,0 +1,7 @@
import styled from '@emotion/styled';
import Button from '@mui/material/Button';
export const RegisterButtonStyled = styled(Button)`
background-color: var(--tg-theme-button-color);
color: var(--tg-theme-button-text-color);
`;

View File

@ -0,0 +1,19 @@
import * as React from 'react';
import { RegisterButtonStyled } from './index.style';
const RegisterButton = ({children, id = null, name = null, className = null}): React.ReactElement => {
return (
<RegisterButtonStyled
id={id}
name={name}
className={className}
type="submit"
fullWidth
variant="contained"
>
{children}
</RegisterButtonStyled>
);
}
export default RegisterButton;

View File

@ -1,7 +1,3 @@
.upload-input {
display: none;
}
.basic-multi-select { .basic-multi-select {
width: 100%; width: 100%;
} }

View File

@ -0,0 +1,9 @@
import styled from '@emotion/styled';
import { Grid2 } from '@mui/material';
export const GridChildrenStyle = styled(Grid2)`
display: flex;
padding: 10px;
align-items: center;
justify-content: center;
`;

View File

@ -1,33 +1,24 @@
import * as React from 'react'; import * as React from 'react';
import Avatar from '@mui/material/Avatar'; import {Button, Grid2, Box, Container
import Button from '@mui/material/Button'; } from '@mui/material';
import CssBaseline from '@mui/material/CssBaseline'; import Title from './title';
import TextField from '@mui/material/TextField'; import Name from './name';
import Grid from '@mui/material/Grid'; import About from './about';
import Box from '@mui/material/Box'; import Photo from './photo';
import Typography from '@mui/material/Typography'; import Interests from './interests';
import Container from '@mui/material/Container'; import RegisterButton from './button';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import axios from 'axios'; import axios from 'axios';
import student_icon from '../../assets/images/student-icon.png'; import student_icon from './student-icon.png';
import "./index.css"; import "./index.css";
//import useTelegram from "../hooks/useTelegram"; //import useTelegram from "../hooks/useTelegram";
import Select from 'react-select'; import Select from 'react-select';
import makeAnimated from 'react-select/animated';
import { useState } from 'react'; import { useState } from 'react';
import { useConstant } from '../Constant'; import { useConstant } from '../Constant';
import { GridChildrenStyle } from './index.style';
const animatedComponents = makeAnimated();
const theme = createTheme();
const SingUpPage = (): React.ReactElement => { const SingUpPage = (): React.ReactElement => {
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
const [selectedOption, setSelectedOption] = useState<string | null>(null);
const handleChange = (selectedOption) => {
setSelectedOption(selectedOption);
};
const { interests } = useConstant(); const { interests } = useConstant();
//const { user_id, username, onClose } = useTelegram(); //const { user_id, username, onClose } = useTelegram();
/* /*
const handleSubmit = async (event) => { const handleSubmit = async (event) => {
@ -48,123 +39,66 @@ const SingUpPage = (): React.ReactElement => {
onClose(); onClose();
}; };
*/ */
const handleAvatarClick = () => {
const fileInput = document.getElementById('avatar-input') as HTMLInputElement;
fileInput.click();
};
const handleAvatarChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = () => {
setAvatarUrl(reader.result as string);
};
reader.readAsDataURL(file);
};
return ( return (
<ThemeProvider theme={theme}>
<Container component="main" maxWidth="xs"> <Container component="main" maxWidth="xs">
<CssBaseline />
<Box <Box
sx={{ sx={{
marginTop: 8, marginTop: 4,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
}} }}
> >
<Typography component="h1" variant="h5"> <Title>Регистрация</Title>
Регистрация
</Typography>
<Box component="form" noValidate sx={{ mt: 3 }}> <Box component="form" noValidate sx={{ mt: 3 }}>
<Grid container spacing={2}> <Grid2 container>
<Grid item xs={12} sm={6}> <GridChildrenStyle size={6}>
<TextField <Name
required id="lastname"
fullWidth label="Фамилия"
id="lname" name="lastname"
label="Фамилия"
name="lname"
autoComplete="family-name" autoComplete="family-name"
/> />
</Grid> </GridChildrenStyle>
<Grid item xs={12} sm={6}> <GridChildrenStyle size={6}>
<TextField <Name
autoComplete="given-name" id="firstname"
name="fname"
required
fullWidth
id="fname"
label="Имя" label="Имя"
autoFocus name="firstname"
autoComplete="given-name"
/> />
</Grid> </GridChildrenStyle>
<Grid item xs={12}> <GridChildrenStyle size={12}>
<TextField <About
fullWidth
multiline
rows={3} rows={3}
id="about" id="about"
label="Обо мне" label="Обо мне"
name="about" name="about"
variant="outlined"
placeholder="Напишите о себе" placeholder="Напишите о себе"
/> />
</Grid> </GridChildrenStyle>
<Grid <GridChildrenStyle size={12}>
container <Photo
spacing={0} id="photo"
direction="column" name="photo"
alignItems="center"
justifyContent="center"
sx={{ paddingTop: 7, paddingLeft: 3 }}
>
<Avatar src={avatarUrl || student_icon} onClick={handleAvatarClick} sx={{ width: 200, height: 200 }}/>
<input
type="file"
name="image"
accept="image/*"
onChange={handleAvatarChange}
style={{ display: 'none' }}
id="avatar-input"
/> />
</Grid> </GridChildrenStyle>
<Grid <GridChildrenStyle size={12}>
container <Interests
spacing={0}
direction="column"
alignItems="center"
justifyContent="center"
sx={{ paddingTop: 7, ml: 3 }}
>
<Select
onChange={handleChange}
closeMenuOnSelect={false}
components={animatedComponents}
isMulti
options={interests} options={interests}
className="basic-multi-select" placeholder='Выберите интересы...'
classNamePrefix="select"
/> />
</Grid> </GridChildrenStyle>
<Button <GridChildrenStyle size={12}>
type="submit" <RegisterButton>
fullWidth
variant="contained"
sx={{ mt: 3, mb: 2, ml: 3 }}
>
Регистрация Регистрация
</Button> </RegisterButton>
</Grid> </GridChildrenStyle>
</Grid2>
</Box> </Box>
</Box> </Box>
</Container> </Container>
</ThemeProvider>
); );
}; };

View File

@ -0,0 +1,68 @@
import styled from '@emotion/styled';
import Select from 'react-select';
export const InterestsStyled = styled(Select)`
.select__control {
background-color: var(--tg-theme-bg-color, #ffffff);
border: 1px solid var(--tg-theme-border-color, #cccccc);
color: var(--tg-theme-text-color, #000000);
border-radius: 8px;
font-size: 14px;
box-shadow: none;
&:hover {
border-color: var(--tg-theme-border-hover, #888888);
}
}
.select__menu {
background-color: var(--tg-theme-bg-color, #ffffff);
border-radius: 8px;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
z-index: 10;
}
.select__option {
background-color: transparent;
color: var(--tg-theme-text-color, #000000);
padding: 10px;
cursor: pointer;
&:hover {
background-color: var(--tg-theme-hover-color, #f2f2f2);
}
&.select__option--is-selected {
background-color: var(--tg-theme-button-color);
color: #ffffff;
}
&.select__option--is-focused {
background-color: var(--tg-theme-hover-color, #e0e0e0);
}
}
.select__placeholder {
color: var(--tg-theme-placeholder-color, #888888);
font-size: 14px;
}
.select__multi-value {
background-color: var(--tg-theme-button-color);
color: #ffffff;
border-radius: 4px;
.select__multi-value__label {
color: #ffffff;
}
.select__multi-value__remove {
color: #ffffff;
&:hover {
background-color: rgba(255, 255, 255, 0.2);
color: #ffffff;
}
}
}
`;

View File

@ -0,0 +1,27 @@
import * as React from 'react';
import { useState } from 'react';
import { InterestsStyled } from './index.style';
const Interests = ({options, placeholder = "Выберите..."}): React.ReactElement => {
const [selectedInterests, setSelectedInterests] = useState<string | null>(null);
const handleChange = (selectedInterests) => {
setSelectedInterests(selectedInterests);
};
return (
<InterestsStyled
onChange={handleChange}
closeMenuOnSelect={false}
isMulti
options={options}
className="basic-multi-select"
classNamePrefix="select"
placeholder={placeholder}
value={selectedInterests}
/>
);
}
export default Interests;

View File

@ -0,0 +1,60 @@
import styled from '@emotion/styled';
import TextField from '@mui/material/TextField';
export const TextFieldStyled = styled(TextField)`
& .MuiInputBase-input {
color: var(--tg-theme-text-color,rgba(0, 0, 0, 0.6));
}
& .MuiFormLabel-root {
color: var(--tg-theme-text-color, rgba(0, 0, 0, 0.6));
}
& .Mui-focused .MuiFormLabel-root {
color: var(--tg-theme-label-focus-color, #0066ff);
}
& .MuiOutlinedInput-root {
fieldset {
border-color: var(--tg-theme-border-color, #cccccc);
}
&:hover fieldset {
border-color: var(--tg-theme-border-hover, #888888);
}
&.Mui-focused fieldset {
border-color: var(--tg-theme-border-focused, #0066ff);
}
}
@media (prefers-color-scheme: dark) {
& .MuiInputBase-input {
color: var(--tg-theme-text-color-dark, #ffffff);
}
& .MuiFormLabel-root {
color: var(--tg-theme-label-color-dark, #ffffff);
}
& .Mui-focused .MuiFormLabel-root {
color: var(--tg-theme-label-focus-color-dark, #0066ff);
}
& .MuiOutlinedInput-root {
fieldset {
border-color: var(--tg-theme-border-color-dark, #ffffff);
}
&:hover fieldset {
border-color: var(--tg-theme-border-hover-dark, #777777);
}
&.Mui-focused fieldset {
border-color: var(--tg-theme-border-focused-dark, #0066ff);
}
}
}
`;

View File

@ -0,0 +1,18 @@
import * as React from 'react';
import { TextFieldStyled } from './index.style';
const Name = ({id, label, name, autoComplete, className = null}): React.ReactElement => {
return (
<TextFieldStyled
required
fullWidth
id={id}
label={label}
name={name}
autoComplete={autoComplete}
className={className}
/>
);
}
export default Name;

View File

@ -0,0 +1,17 @@
import styled from '@emotion/styled';
import Avatar from "@mui/material/Avatar";
export const PhotoStyled = styled.div<{ id: string, name: string, value: string | null }>`
position: relative;
display: inline-block;
cursor: pointer;
`;
export const AvatarStyled = styled(Avatar)`
width: 200px;
height: 200px;
`;
export const InputStyled = styled.input`
display: none;
`;

View File

@ -0,0 +1,50 @@
import * as React from 'react';
import { useState } from 'react';
import { AvatarStyled, PhotoStyled, InputStyled } from './index.style';
const Photo = ({ id, name, defaultPhoto = null}): React.ReactElement => {
const [photo, setPhoto] = useState(defaultPhoto);
const handleFileChange = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const newPhoto = e.target.result;
setPhoto(newPhoto);
};
reader.readAsDataURL(file);
}
};
const handleAvatarClick = () => {
const fileInput = document.getElementById('fileInput') as HTMLInputElement;
fileInput.click();
}
return (
<PhotoStyled
id = {id}
name = {name}
value = {photo}
>
<AvatarStyled
src={photo}
alt="Выберите фотографию"
onClick={handleAvatarClick}
/>
<InputStyled
id="fileInput"
type="file"
accept="image/*"
onChange={handleFileChange}
/>
</PhotoStyled>
);
}
export default Photo;

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -0,0 +1,6 @@
import styled from '@emotion/styled';
export const TitleStyled = styled.h1`
font-size: 28px;
color: var(--tg-theme-text-color);
`;

View File

@ -0,0 +1,12 @@
import * as React from 'react';;
import { TitleStyled } from './index.style';
const Title = ({children}): React.ReactElement => {
return (
<TitleStyled>
{children}
</TitleStyled>
);
}
export default Title;

View File

@ -12,7 +12,7 @@
"moduleResolution": "node", "moduleResolution": "node",
"target": "es6", "target": "es6",
"jsx": "react", "jsx": "react",
"typeRoots": ["node_modules/@types", "src/typings"], "typeRoots": ["node_modules/@types", "src/@types"],
"types" : ["webpack-env", "node"], "types" : ["webpack-env", "node"],
"resolveJsonModule": true "resolveJsonModule": true
}, },