Add info if linked acc
This commit is contained in:
41
.dockerignore
Normal file
41
.dockerignore
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
*/node_modules
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
*.pyc
|
||||||
|
__pycache__
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Environment files (keep .env.example)
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Test & coverage
|
||||||
|
coverage
|
||||||
|
.pytest_cache
|
||||||
|
.coverage
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
30
backend/alembic/versions/010_add_telegram_profile.py
Normal file
30
backend/alembic/versions/010_add_telegram_profile.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
"""Add telegram profile fields to users
|
||||||
|
|
||||||
|
Revision ID: 010_add_telegram_profile
|
||||||
|
Revises: 009_add_disputes
|
||||||
|
Create Date: 2024-12-16
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '010_add_telegram_profile'
|
||||||
|
down_revision: Union[str, None] = '009_add_disputes'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column('users', sa.Column('telegram_first_name', sa.String(100), nullable=True))
|
||||||
|
op.add_column('users', sa.Column('telegram_last_name', sa.String(100), nullable=True))
|
||||||
|
op.add_column('users', sa.Column('telegram_avatar_url', sa.String(500), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('users', 'telegram_avatar_url')
|
||||||
|
op.drop_column('users', 'telegram_last_name')
|
||||||
|
op.drop_column('users', 'telegram_first_name')
|
||||||
@@ -25,6 +25,9 @@ class TelegramConfirmLink(BaseModel):
|
|||||||
token: str
|
token: str
|
||||||
telegram_id: int
|
telegram_id: int
|
||||||
telegram_username: str | None = None
|
telegram_username: str | None = None
|
||||||
|
telegram_first_name: str | None = None
|
||||||
|
telegram_last_name: str | None = None
|
||||||
|
telegram_avatar_url: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class TelegramLinkResponse(BaseModel):
|
class TelegramLinkResponse(BaseModel):
|
||||||
@@ -131,6 +134,9 @@ async def confirm_telegram_link(data: TelegramConfirmLink, db: DbSession):
|
|||||||
logger.info(f"[TG_CONFIRM] Linking telegram_id={data.telegram_id} to user_id={user_id}")
|
logger.info(f"[TG_CONFIRM] Linking telegram_id={data.telegram_id} to user_id={user_id}")
|
||||||
user.telegram_id = data.telegram_id
|
user.telegram_id = data.telegram_id
|
||||||
user.telegram_username = data.telegram_username
|
user.telegram_username = data.telegram_username
|
||||||
|
user.telegram_first_name = data.telegram_first_name
|
||||||
|
user.telegram_last_name = data.telegram_last_name
|
||||||
|
user.telegram_avatar_url = data.telegram_avatar_url
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
logger.info(f"[TG_CONFIRM] SUCCESS! User {user.nickname} linked to Telegram {data.telegram_id}")
|
logger.info(f"[TG_CONFIRM] SUCCESS! User {user.nickname} linked to Telegram {data.telegram_id}")
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ class User(Base):
|
|||||||
avatar_path: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
avatar_path: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||||
telegram_id: Mapped[int | None] = mapped_column(BigInteger, unique=True, nullable=True)
|
telegram_id: Mapped[int | None] = mapped_column(BigInteger, unique=True, nullable=True)
|
||||||
telegram_username: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
telegram_username: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||||
|
telegram_first_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||||
|
telegram_last_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||||
|
telegram_avatar_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||||
role: Mapped[str] = mapped_column(String(20), default=UserRole.USER.value)
|
role: Mapped[str] = mapped_column(String(20), default=UserRole.USER.value)
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ class UserPublic(UserBase):
|
|||||||
role: str = "user"
|
role: str = "user"
|
||||||
telegram_id: int | None = None
|
telegram_id: int | None = None
|
||||||
telegram_username: str | None = None
|
telegram_username: str | None = None
|
||||||
|
telegram_first_name: str | None = None
|
||||||
|
telegram_last_name: str | None = None
|
||||||
|
telegram_avatar_url: str | None = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiogram import Router, F
|
from aiogram import Router, F, Bot
|
||||||
from aiogram.filters import CommandStart, Command, CommandObject
|
from aiogram.filters import CommandStart, Command, CommandObject
|
||||||
from aiogram.types import Message
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from config import settings
|
||||||
from keyboards.main_menu import get_main_menu
|
from keyboards.main_menu import get_main_menu
|
||||||
from services.api_client import api_client
|
from services.api_client import api_client
|
||||||
|
|
||||||
@@ -11,6 +12,21 @@ logger = logging.getLogger(__name__)
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_avatar_url(bot: Bot, user_id: int) -> str | None:
|
||||||
|
"""Get user's Telegram profile photo URL."""
|
||||||
|
try:
|
||||||
|
photos = await bot.get_user_profile_photos(user_id, limit=1)
|
||||||
|
if photos.total_count > 0 and photos.photos:
|
||||||
|
# Get the largest photo (last in the list)
|
||||||
|
photo = photos.photos[0][-1]
|
||||||
|
file = await bot.get_file(photo.file_id)
|
||||||
|
if file.file_path:
|
||||||
|
return f"https://api.telegram.org/file/bot{settings.TELEGRAM_BOT_TOKEN}/{file.file_path}"
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[START] Could not get user avatar: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@router.message(CommandStart())
|
@router.message(CommandStart())
|
||||||
async def cmd_start(message: Message, command: CommandObject):
|
async def cmd_start(message: Message, command: CommandObject):
|
||||||
"""Handle /start command with or without deep link."""
|
"""Handle /start command with or without deep link."""
|
||||||
@@ -26,16 +42,26 @@ async def cmd_start(message: Message, command: CommandObject):
|
|||||||
logger.info(f"[START] Token: {token}")
|
logger.info(f"[START] Token: {token}")
|
||||||
logger.info(f"[START] Token length: {len(token)} chars")
|
logger.info(f"[START] Token length: {len(token)} chars")
|
||||||
|
|
||||||
|
# Get user's avatar
|
||||||
|
avatar_url = await get_user_avatar_url(message.bot, message.from_user.id)
|
||||||
|
logger.info(f"[START] User avatar URL: {avatar_url}")
|
||||||
|
|
||||||
logger.info(f"[START] -------- CALLING API --------")
|
logger.info(f"[START] -------- CALLING API --------")
|
||||||
logger.info(f"[START] Sending to /telegram/confirm-link:")
|
logger.info(f"[START] Sending to /telegram/confirm-link:")
|
||||||
logger.info(f"[START] - token: {token}")
|
logger.info(f"[START] - token: {token}")
|
||||||
logger.info(f"[START] - telegram_id: {message.from_user.id}")
|
logger.info(f"[START] - telegram_id: {message.from_user.id}")
|
||||||
logger.info(f"[START] - telegram_username: {message.from_user.username}")
|
logger.info(f"[START] - telegram_username: {message.from_user.username}")
|
||||||
|
logger.info(f"[START] - telegram_first_name: {message.from_user.first_name}")
|
||||||
|
logger.info(f"[START] - telegram_last_name: {message.from_user.last_name}")
|
||||||
|
logger.info(f"[START] - telegram_avatar_url: {avatar_url}")
|
||||||
|
|
||||||
result = await api_client.confirm_telegram_link(
|
result = await api_client.confirm_telegram_link(
|
||||||
token=token,
|
token=token,
|
||||||
telegram_id=message.from_user.id,
|
telegram_id=message.from_user.id,
|
||||||
telegram_username=message.from_user.username
|
telegram_username=message.from_user.username,
|
||||||
|
telegram_first_name=message.from_user.first_name,
|
||||||
|
telegram_last_name=message.from_user.last_name,
|
||||||
|
telegram_avatar_url=avatar_url
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"[START] -------- API RESPONSE --------")
|
logger.info(f"[START] -------- API RESPONSE --------")
|
||||||
|
|||||||
@@ -64,7 +64,10 @@ class APIClient:
|
|||||||
self,
|
self,
|
||||||
token: str,
|
token: str,
|
||||||
telegram_id: int,
|
telegram_id: int,
|
||||||
telegram_username: str | None
|
telegram_username: str | None,
|
||||||
|
telegram_first_name: str | None = None,
|
||||||
|
telegram_last_name: str | None = None,
|
||||||
|
telegram_avatar_url: str | None = None
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Confirm Telegram account linking."""
|
"""Confirm Telegram account linking."""
|
||||||
result = await self._request(
|
result = await self._request(
|
||||||
@@ -73,7 +76,10 @@ class APIClient:
|
|||||||
json={
|
json={
|
||||||
"token": token,
|
"token": token,
|
||||||
"telegram_id": telegram_id,
|
"telegram_id": telegram_id,
|
||||||
"telegram_username": telegram_username
|
"telegram_username": telegram_username,
|
||||||
|
"telegram_first_name": telegram_first_name,
|
||||||
|
"telegram_last_name": telegram_last_name,
|
||||||
|
"telegram_avatar_url": telegram_avatar_url
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return result or {"error": "Не удалось связаться с сервером"}
|
return result or {"error": "Не удалось связаться с сервером"}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { MessageCircle, ExternalLink, X, Loader2 } from 'lucide-react'
|
import { MessageCircle, ExternalLink, X, Loader2, RefreshCw, CheckCircle, User, Link2, Link2Off } from 'lucide-react'
|
||||||
import { telegramApi } from '@/api/telegram'
|
import { telegramApi } from '@/api/telegram'
|
||||||
|
import { authApi } from '@/api/auth'
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
|
|
||||||
export function TelegramLink() {
|
export function TelegramLink() {
|
||||||
@@ -9,16 +10,74 @@ export function TelegramLink() {
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [botUrl, setBotUrl] = useState<string | null>(null)
|
const [botUrl, setBotUrl] = useState<string | null>(null)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [isPolling, setIsPolling] = useState(false)
|
||||||
|
const [linkSuccess, setLinkSuccess] = useState(false)
|
||||||
|
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||||
|
|
||||||
const isLinked = !!user?.telegram_id
|
const isLinked = !!user?.telegram_id
|
||||||
|
|
||||||
|
// Cleanup polling on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (pollingRef.current) {
|
||||||
|
clearInterval(pollingRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const startPolling = () => {
|
||||||
|
setIsPolling(true)
|
||||||
|
let attempts = 0
|
||||||
|
const maxAttempts = 60 // 5 minutes (5 sec intervals)
|
||||||
|
|
||||||
|
pollingRef.current = setInterval(async () => {
|
||||||
|
attempts++
|
||||||
|
try {
|
||||||
|
const userData = await authApi.me()
|
||||||
|
if (userData.telegram_id) {
|
||||||
|
// Success! User linked their account
|
||||||
|
updateUser({
|
||||||
|
telegram_id: userData.telegram_id,
|
||||||
|
telegram_username: userData.telegram_username,
|
||||||
|
telegram_first_name: userData.telegram_first_name,
|
||||||
|
telegram_last_name: userData.telegram_last_name,
|
||||||
|
telegram_avatar_url: userData.telegram_avatar_url
|
||||||
|
})
|
||||||
|
setLinkSuccess(true)
|
||||||
|
setIsPolling(false)
|
||||||
|
setBotUrl(null)
|
||||||
|
if (pollingRef.current) {
|
||||||
|
clearInterval(pollingRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors, continue polling
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempts >= maxAttempts) {
|
||||||
|
setIsPolling(false)
|
||||||
|
if (pollingRef.current) {
|
||||||
|
clearInterval(pollingRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopPolling = () => {
|
||||||
|
setIsPolling(false)
|
||||||
|
if (pollingRef.current) {
|
||||||
|
clearInterval(pollingRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleGenerateLink = async () => {
|
const handleGenerateLink = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
setLinkSuccess(false)
|
||||||
try {
|
try {
|
||||||
const { bot_url } = await telegramApi.generateLinkToken()
|
const { bot_url } = await telegramApi.generateLinkToken()
|
||||||
setBotUrl(bot_url)
|
setBotUrl(bot_url)
|
||||||
} catch (err) {
|
} catch {
|
||||||
setError('Не удалось сгенерировать ссылку')
|
setError('Не удалось сгенерировать ссылку')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -30,9 +89,15 @@ export function TelegramLink() {
|
|||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
await telegramApi.unlinkTelegram()
|
await telegramApi.unlinkTelegram()
|
||||||
updateUser({ telegram_id: null, telegram_username: null })
|
updateUser({
|
||||||
|
telegram_id: null,
|
||||||
|
telegram_username: null,
|
||||||
|
telegram_first_name: null,
|
||||||
|
telegram_last_name: null,
|
||||||
|
telegram_avatar_url: null
|
||||||
|
})
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
} catch (err) {
|
} catch {
|
||||||
setError('Не удалось отвязать аккаунт')
|
setError('Не удалось отвязать аккаунт')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -42,9 +107,16 @@ export function TelegramLink() {
|
|||||||
const handleOpenBot = () => {
|
const handleOpenBot = () => {
|
||||||
if (botUrl) {
|
if (botUrl) {
|
||||||
window.open(botUrl, '_blank')
|
window.open(botUrl, '_blank')
|
||||||
|
startPolling()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
setBotUrl(null)
|
setBotUrl(null)
|
||||||
}
|
setError(null)
|
||||||
|
setLinkSuccess(false)
|
||||||
|
stopPolling()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -65,11 +137,7 @@ export function TelegramLink() {
|
|||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
<div className="bg-gray-800 rounded-xl max-w-md w-full p-6 relative">
|
<div className="bg-gray-800 rounded-xl max-w-md w-full p-6 relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={handleClose}
|
||||||
setIsOpen(false)
|
|
||||||
setBotUrl(null)
|
|
||||||
setError(null)
|
|
||||||
}}
|
|
||||||
className="absolute top-4 right-4 text-gray-400 hover:text-white"
|
className="absolute top-4 right-4 text-gray-400 hover:text-white"
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
@@ -93,38 +161,107 @@ export function TelegramLink() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLinked ? (
|
{isLinked || linkSuccess ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="p-4 bg-gray-700/50 rounded-lg">
|
{linkSuccess && (
|
||||||
<p className="text-sm text-gray-400 mb-1">Привязан к:</p>
|
<div className="p-4 bg-green-500/20 border border-green-500/50 rounded-lg flex items-center gap-3">
|
||||||
<p className="text-white font-medium">
|
<CheckCircle className="w-6 h-6 text-green-400 flex-shrink-0" />
|
||||||
{user?.telegram_username ? `@${user.telegram_username}` : `ID: ${user?.telegram_id}`}
|
<p className="text-green-400 font-medium">Аккаунт успешно привязан!</p>
|
||||||
</p>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* User Profile Card */}
|
||||||
|
<div className="p-4 bg-gradient-to-br from-gray-700/50 to-gray-800/50 rounded-xl border border-gray-600/50">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{/* Avatar - prefer Telegram avatar */}
|
||||||
|
<div className="relative">
|
||||||
|
{user?.telegram_avatar_url || user?.avatar_url ? (
|
||||||
|
<img
|
||||||
|
src={user.telegram_avatar_url || user.avatar_url || ''}
|
||||||
|
alt={user.nickname}
|
||||||
|
className="w-16 h-16 rounded-full object-cover border-2 border-blue-500/50"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center border-2 border-blue-500/50">
|
||||||
|
<User className="w-8 h-8 text-white" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Link indicator */}
|
||||||
|
<div className="absolute -bottom-1 -right-1 w-6 h-6 bg-green-500 rounded-full flex items-center justify-center border-2 border-gray-800">
|
||||||
|
<Link2 className="w-3 h-3 text-white" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-sm text-gray-400">
|
{/* User Info */}
|
||||||
<p className="mb-2">Ты будешь получать уведомления о:</p>
|
<div className="flex-1 min-w-0">
|
||||||
<ul className="list-disc list-inside space-y-1">
|
<p className="text-lg font-bold text-white truncate">
|
||||||
<li>Начале и окончании событий</li>
|
{[user?.telegram_first_name, user?.telegram_last_name].filter(Boolean).join(' ') || user?.nickname}
|
||||||
<li>Старте и завершении марафонов</li>
|
</p>
|
||||||
<li>Спорах по заданиям</li>
|
{user?.telegram_username && (
|
||||||
</ul>
|
<p className="text-blue-400 font-medium truncate">@{user.telegram_username}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Notifications Info */}
|
||||||
|
<div className="p-4 bg-gray-700/30 rounded-lg">
|
||||||
|
<p className="text-sm text-gray-300 font-medium mb-3">Уведомления включены:</p>
|
||||||
|
<div className="grid grid-cols-1 gap-2">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||||
|
<span className="text-yellow-400">🌟</span>
|
||||||
|
<span>События (Golden Hour, Jackpot)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||||
|
<span className="text-green-400">🚀</span>
|
||||||
|
<span>Старт и финиш марафонов</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||||
|
<span className="text-red-400">⚠️</span>
|
||||||
|
<span>Споры по заданиям</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleUnlink}
|
onClick={handleUnlink}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full py-3 px-4 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-lg font-medium transition-colors disabled:opacity-50"
|
className="w-full py-3 px-4 bg-red-500/10 hover:bg-red-500/20 text-red-400 rounded-lg font-medium transition-colors disabled:opacity-50 flex items-center justify-center gap-2 border border-red-500/30"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Loader2 className="w-5 h-5 animate-spin mx-auto" />
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
'Отвязать аккаунт'
|
<>
|
||||||
|
<Link2Off className="w-4 h-4" />
|
||||||
|
Отвязать аккаунт
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : botUrl ? (
|
) : botUrl ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{isPolling ? (
|
||||||
|
<>
|
||||||
|
<div className="p-4 bg-blue-500/20 border border-blue-500/50 rounded-lg">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<RefreshCw className="w-5 h-5 text-blue-400 animate-spin" />
|
||||||
|
<p className="text-blue-400 font-medium">Ожидание привязки...</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
Открой бота в Telegram и нажми Start. Статус обновится автоматически.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleOpenBot}
|
||||||
|
className="w-full py-3 px-4 bg-blue-500 hover:bg-blue-600 text-white rounded-lg font-medium transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-5 h-5" />
|
||||||
|
Открыть Telegram снова
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<p className="text-gray-300">
|
<p className="text-gray-300">
|
||||||
Нажми кнопку ниже, чтобы открыть бота и завершить привязку:
|
Нажми кнопку ниже, чтобы открыть бота и завершить привязку:
|
||||||
</p>
|
</p>
|
||||||
@@ -140,6 +277,8 @@ export function TelegramLink() {
|
|||||||
<p className="text-sm text-gray-500 text-center">
|
<p className="text-sm text-gray-500 text-center">
|
||||||
Ссылка действительна 10 минут
|
Ссылка действительна 10 минут
|
||||||
</p>
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ export interface User {
|
|||||||
role: UserRole
|
role: UserRole
|
||||||
telegram_id: number | null
|
telegram_id: number | null
|
||||||
telegram_username: string | null
|
telegram_username: string | null
|
||||||
|
telegram_first_name: string | null
|
||||||
|
telegram_last_name: string | null
|
||||||
|
telegram_avatar_url: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user