telegram fullscreen /viewContent

This commit is contained in:
Verticool 2025-03-21 13:28:16 +06:00
parent 1b96d049c7
commit 9dcc6860f4
9 changed files with 638 additions and 143 deletions

155
package-lock.json generated
View File

@ -10,13 +10,13 @@
"dependencies": {
"@hookform/resolvers": "^3.3.4",
"@sentry/react": "^9.1.0",
"@telegram-apps/sdk": "^3.5.1",
"@ton/core": "^0.59.1",
"@tonconnect/ui-react": "^2.0.2",
"@vkruglikov/react-telegram-web-app": "^2.1.9",
"axios": "^1.6.7",
"buffer": "^6.0.3",
"clsx": "^2.1.0",
"jssha": "^3.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.0",
@ -1404,6 +1404,113 @@
"react": "^16.14.0 || 17.x || 18.x || 19.x"
}
},
"node_modules/@telegram-apps/bridge": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@telegram-apps/bridge/-/bridge-2.4.0.tgz",
"integrity": "sha512-Lp/vhspF7okK8zXvSWWirunKXOPE6Gr11o9VBne4VmKG/yHRhEW7Pf07ncPtXLLzI6wW8+VYc3khsHPABJymEw==",
"license": "MIT",
"dependencies": {
"@telegram-apps/signals": "^1.1.1",
"@telegram-apps/toolkit": "^2.0.0",
"@telegram-apps/transformers": "^2.2.0",
"@telegram-apps/types": "^2.0.0",
"better-promises": "^0.4.0",
"error-kid": "^0.0.4",
"mitt": "^3.0.1",
"valibot": "1.0.0-beta.14"
}
},
"node_modules/@telegram-apps/navigation": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/@telegram-apps/navigation/-/navigation-1.0.13.tgz",
"integrity": "sha512-TsUueB5LQp77GQHoMa93nq26Uw7GJjrFCPbyseMVU7aBBxAc+8CV2IYytRwcVp5sv/q7ThK5X4JaKn2V1yBHDQ==",
"license": "MIT",
"dependencies": {
"@telegram-apps/bridge": "^1.9.2",
"@telegram-apps/signals": "^1.1.1",
"@telegram-apps/toolkit": "^1.1.1"
}
},
"node_modules/@telegram-apps/navigation/node_modules/@telegram-apps/bridge": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@telegram-apps/bridge/-/bridge-1.9.2.tgz",
"integrity": "sha512-SJLcNWLXhbbZr9MiqFH/g2ceuitSJKMxUIZysK4zUNyTUNuonrQG80Q/yrO+XiNbKUj8WdDNM86NBARhuyyinQ==",
"license": "MIT",
"dependencies": {
"@telegram-apps/signals": "^1.1.1",
"@telegram-apps/toolkit": "^1.1.1",
"@telegram-apps/transformers": "^1.2.2",
"@telegram-apps/types": "^1.2.1"
}
},
"node_modules/@telegram-apps/navigation/node_modules/@telegram-apps/toolkit": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@telegram-apps/toolkit/-/toolkit-1.1.1.tgz",
"integrity": "sha512-+vhKx6ngfvjyTE6Xagl3z1TPVbfx5s7xAkcYzCdHYUo6T60jLIqLgyZMcI1UPoIAMuMu1pHoO+p8QNCj/+tFmw==",
"license": "MIT"
},
"node_modules/@telegram-apps/navigation/node_modules/@telegram-apps/transformers": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@telegram-apps/transformers/-/transformers-1.2.2.tgz",
"integrity": "sha512-vvMwXckd1D7Ozc0h66PSUwF5QLrRV9HlGJFFeBuUex8QEk5mSPtsJkLiqB8aBbwuFDa91+TUSM/CxqPZO/e9YQ==",
"license": "MIT",
"dependencies": {
"@telegram-apps/toolkit": "^1.1.1",
"@telegram-apps/types": "^1.2.1"
}
},
"node_modules/@telegram-apps/navigation/node_modules/@telegram-apps/types": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@telegram-apps/types/-/types-1.2.1.tgz",
"integrity": "sha512-so4HLh7clur0YyMthi9KVIgWoGpZdXlFOuQjk3+Q5NAvJZ11nAheBSwPlGw/Ko92+zwvrSBE/lQyN2+p17RP+w==",
"license": "MIT"
},
"node_modules/@telegram-apps/sdk": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/@telegram-apps/sdk/-/sdk-3.5.1.tgz",
"integrity": "sha512-m/ynpSozXsqq6Kfb6M9fm8SD6x/+jqvTFT59FuuOBvC+G8eMd0F+fvZdr6Pj0I6IL9M67nrfm92JpTu/aVNEjw==",
"license": "MIT",
"dependencies": {
"@telegram-apps/bridge": "^2.4.0",
"@telegram-apps/navigation": "^1.0.13",
"@telegram-apps/signals": "^1.1.1",
"@telegram-apps/toolkit": "^2.0.0",
"@telegram-apps/transformers": "^2.2.0",
"@telegram-apps/types": "^2.0.0",
"better-promises": "^0.4.0",
"error-kid": "^0.0.4",
"valibot": "1.0.0-beta.14"
}
},
"node_modules/@telegram-apps/signals": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@telegram-apps/signals/-/signals-1.1.1.tgz",
"integrity": "sha512-vz37r8lemGpPzDiBRfqpXYBynzmy3SFnY6zfHsTZABTYYt0b0WQZyU5mFDqqqugGhka78Gy11xmr9csgy4YgGA==",
"license": "MIT"
},
"node_modules/@telegram-apps/toolkit": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@telegram-apps/toolkit/-/toolkit-2.0.0.tgz",
"integrity": "sha512-1GKTLBNme1Phu/gFvgS9NWPq+LhPfzSIfnwhcF9I/6tCdufrLRcVaSMRiK9R4VDYD6iZUyj+a2l250qWAxxjQQ==",
"license": "MIT"
},
"node_modules/@telegram-apps/transformers": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@telegram-apps/transformers/-/transformers-2.2.0.tgz",
"integrity": "sha512-wqXXOukhEjZKhzdq5vG1LkxWL11DApbmUKzk+3nA/ki3TLyD3awVTOXbpoNdOwFl2xliIooYcsUOEl4WCyyLGw==",
"license": "MIT",
"dependencies": {
"@telegram-apps/toolkit": "^2.0.0",
"@telegram-apps/types": "^2.0.0",
"valibot": "1.0.0-beta.14"
}
},
"node_modules/@telegram-apps/types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@telegram-apps/types/-/types-2.0.0.tgz",
"integrity": "sha512-yF499FJK82a2IDNDAQdrmVH3sgFZl/QFNdVZKgWpgtunIVJ1fres5wi9+4aUBRVIdQwZOZZqB/AOvYYuYXsq3Q==",
"license": "MIT"
},
"node_modules/@ton/core": {
"version": "0.59.1",
"resolved": "https://registry.npmjs.org/@ton/core/-/core-0.59.1.tgz",
@ -2189,6 +2296,15 @@
],
"license": "MIT"
},
"node_modules/better-promises": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/better-promises/-/better-promises-0.4.0.tgz",
"integrity": "sha512-AcKkTUSd4o1vMf41eBbHW1NkY7vrXeNI6etitGdQE54WFXsF2wkfonrKA06Za7lViRNyT/cMvj5z+DScqhYW8A==",
"license": "MIT",
"dependencies": {
"error-kid": "^0.0.4"
}
},
"node_modules/big-integer": {
"version": "1.6.52",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
@ -2640,6 +2756,12 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/error-kid": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/error-kid/-/error-kid-0.0.4.tgz",
"integrity": "sha512-x+yQhY56SorLMnX6kOf+z3JCv2QBurcWEDcIjgxYtVr4fGeCfAtOdZOCyWttkHHDFPtL2PqnaRUmphbmALJd9w==",
"license": "MIT"
},
"node_modules/es-abstract": {
"version": "1.22.5",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.5.tgz",
@ -4222,15 +4344,6 @@
"node": ">=6"
}
},
"node_modules/jssha": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz",
"integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==",
"license": "BSD-3-Clause",
"engines": {
"node": "*"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -4413,6 +4526,12 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -6153,7 +6272,7 @@
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true,
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -6271,6 +6390,20 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
},
"node_modules/valibot": {
"version": "1.0.0-beta.14",
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.0.0-beta.14.tgz",
"integrity": "sha512-tLyV2rE5QL6U29MFy3xt4AqMrn+/HErcp2ZThASnQvPMwfSozjV1uBGKIGiegtZIGjinJqn0SlBdannf18wENA==",
"license": "MIT",
"peerDependencies": {
"typescript": ">=5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/vite": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz",

View File

@ -12,6 +12,7 @@
"dependencies": {
"@hookform/resolvers": "^3.3.4",
"@sentry/react": "^9.1.0",
"@telegram-apps/sdk": "^3.5.1",
"@ton/core": "^0.59.1",
"@tonconnect/ui-react": "^2.0.2",
"@vkruglikov/react-telegram-web-app": "^2.1.9",

View File

@ -1,27 +1,30 @@
import "~/app/styles/globals.css";
import '~/app/styles/globals.css';
import '~/shared/libs/buffer';
import { useEffect } from "react";
import { useExpand, useWebApp } from "@vkruglikov/react-telegram-web-app";
import { Providers } from "~/app/providers";
import { AppRouter } from "~/app/router";
import { useEffect } from 'react';
import { useExpand, useWebApp } from '@vkruglikov/react-telegram-web-app';
import { Providers } from '~/app/providers';
import { AppRouter } from '~/app/router';
import { Notification } from '~/shared/ui/notification';
import { init } from '@telegram-apps/sdk';
export const App = () => {
const WebApp = useWebApp();
const [, expand] = useExpand();
const WebApp = useWebApp();
const [, expand] = useExpand();
useEffect(() => {
WebApp.enableClosingConfirmation();
expand();
useEffect(() => {
init();
WebApp.enableClosingConfirmation();
expand();
WebApp.setHeaderColor("#1d1d1b");
WebApp.setBackgroundColor("#1d1d1b");
}, []);
WebApp.setHeaderColor('#1d1d1b');
WebApp.setBackgroundColor('#1d1d1b');
}, []);
return (
<Providers>
<AppRouter />
</Providers>
);
return (
<Providers>
<AppRouter />
<Notification />
</Providers>
);
};

View File

@ -1,7 +1,6 @@
import ReactPlayer from 'react-player/lazy';
import { useTonConnectUI } from '@tonconnect/ui-react';
import { useWebApp } from '@vkruglikov/react-telegram-web-app';
import { Button } from '~/shared/ui/button';
import { usePurchaseContent, useViewContent } from '~/shared/services/content';
import { fromNanoTON } from '~/shared/utils';
@ -10,7 +9,8 @@ import { AudioPlayer } from '~/shared/ui/audio-player';
import { useAuth } from '~/shared/services/auth';
import { CongratsModal } from './components/congrats-modal';
import { ErrorModal } from './components/error-modal';
import { useRootStore } from '~/shared/stores/root';
import { useTgFullscreen } from '~/shared/hooks/use-tgfullscreen';
type InvoiceStatus = 'paid' | 'failed' | 'cancelled' | 'pending';
// Add type for invoice event
@ -21,7 +21,7 @@ interface InvoiceEvent {
export const ViewContentPage = () => {
const WebApp = useWebApp();
const { addNotification } = useRootStore();
const { data: content, refetch: refetchContent } = useViewContent(
WebApp.initDataUnsafe?.start_param
);
@ -30,6 +30,8 @@ export const ViewContentPage = () => {
const [tonConnectUI] = useTonConnectUI();
const { isFullscreen, toggleFullscreen } = useTgFullscreen();
const auth = useAuth();
const [isCongratsModal, setIsCongratsModal] = useState(false);
const [isErrorModal, setIsErrorModal] = useState(false);
@ -42,11 +44,12 @@ export const ViewContentPage = () => {
while (Date.now() - startTime < timeoutMs) {
if (tonConnectUI.connected) {
addNotification('Кошелек подключен', 'success');
return true;
}
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
addNotification('Время ожидания подключения кошелька истекло', 'warning');
return false; // Timed out
};
@ -185,38 +188,68 @@ export const ViewContentPage = () => {
<main className={'min-h-screen flex w-full flex-col gap-[50px] px-4 '}>
{isCongratsModal && <CongratsModal onConfirm={handleConfirmCongrats} />}
{isErrorModal && <ErrorModal onConfirm={handleErrorModal} />}
{content?.data?.content_type.startsWith('audio') &&
content?.data?.display_options?.metadata?.image && (
<div className={'mt-[30px] h-[314px] w-full'}>
<img
alt={'content_image'}
className={'h-full w-full object-cover object-center'}
src={content?.data?.display_options?.metadata?.image}
<div
className={`${isFullscreen ? 'left-[50%] translate-x-[-50%] fixed top-11 lg:top-2' : 'left-4 absolute top-2'} z-50`}
>
<button
onClick={toggleFullscreen}
className="bg-primary bg-opacity-80 w-8 h-8 flex items-center justify-center rounded-full text-white"
>
<svg
width="20"
height="20"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2 6V2H6M8 2H12V6M12 8V12H8M6 12H2V8"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</div>
<div className="mt-[50px]">
{content?.data?.content_type.startsWith('audio') &&
content?.data?.display_options?.metadata?.image && (
<div className={'mt-[30px] h-[314px] w-full'}>
<img
alt={'content_image'}
className={'h-full w-full object-cover object-center'}
src={content?.data?.display_options?.metadata?.image}
/>
</div>
)}
{content?.data?.content_type.startsWith('audio') ? (
<AudioPlayer src={content?.data?.display_options?.content_url} />
) : (
<div className="w-full video-container">
<ReactPlayer
playsinline={true}
controls={true}
width="100%"
height="auto"
className="react-player"
allowFullScreen={true}
url={content?.data?.display_options?.content_url}
config={{
file: {
attributes: {
playsInline: true,
autoPlay: true,
poster:
content?.data?.display_options?.metadata?.image ||
undefined,
},
},
}}
/>
</div>
)}
{content?.data?.content_type.startsWith('audio') ? (
<AudioPlayer src={content?.data?.display_options?.content_url} />
) : (
<ReactPlayer
playsinline={true}
controls={true}
width="100%"
config={{
file: {
attributes: {
playsInline: true,
autoPlay: true,
poster:
content?.data?.display_options?.metadata?.image || undefined,
},
},
}}
url={content?.data?.display_options?.content_url}
/>
)}
</div>
<section className={'flex flex-col'}>
<h1 className={'text-[20px] font-bold'}>
{content?.data?.display_options?.metadata?.name}
@ -227,7 +260,6 @@ export const ViewContentPage = () => {
{content?.data?.display_options?.metadata?.description}
</p>
</section>
<div className="mt-auto pb-2">
{content?.data?.downloadable && (
<Button

View File

@ -0,0 +1,87 @@
import { useState, useCallback, useEffect } from 'react';
import { useWebApp } from '@vkruglikov/react-telegram-web-app';
import { viewport } from '@telegram-apps/sdk';
/**
* Hook for managing Telegram WebApp fullscreen state
* @returns {Object} Object containing isFullscreen state and toggleFullscreen function
*/
export const useTgFullscreen = () => {
const WebApp = useWebApp();
const [isFullscreen, setIsFullscreen] = useState(false);
// Initialize viewport once on mount
useEffect(() => {
// Mount viewport if available
if (viewport.mount.isAvailable() && !viewport.isMounted()) {
viewport.mount();
}
// Check current fullscreen state
if (viewport.isFullscreen && typeof viewport.isFullscreen === 'function') {
setIsFullscreen(viewport.isFullscreen());
}
// Use the WebApp event system for tracking changes
const handleViewportChanged = () => {
if (WebApp.isExpanded) {
setIsFullscreen(true);
}
};
try {
WebApp.onEvent('viewportChanged', handleViewportChanged);
} catch (error) {
console.warn('Could not add viewportChanged event listener:', error);
}
return () => {
try {
WebApp.offEvent('viewportChanged', handleViewportChanged);
} catch (error) {
console.warn('Could not remove viewportChanged event listener:', error);
}
};
}, [WebApp]);
const toggleFullscreen = useCallback(async () => {
try {
if (!isFullscreen) {
// Try to enter fullscreen
if (viewport.requestFullscreen && viewport.requestFullscreen.isAvailable()) {
await viewport.requestFullscreen();
setIsFullscreen(true);
} else if (viewport.expand && viewport.expand.isAvailable()) {
viewport.expand();
setIsFullscreen(true);
} else {
WebApp.expand();
setIsFullscreen(true);
}
} else {
// Try to exit fullscreen
if (viewport.exitFullscreen && viewport.exitFullscreen.isAvailable()) {
await viewport.exitFullscreen();
setIsFullscreen(false);
} else {
setIsFullscreen(false);
}
}
} catch (error) {
console.error('Error toggling fullscreen:', error);
if (!isFullscreen) {
try {
WebApp.expand();
setIsFullscreen(true);
} catch {
console.error('All fullscreen methods failed');
}
} else {
setIsFullscreen(false);
}
}
}, [WebApp, isFullscreen]);
return { isFullscreen, toggleFullscreen };
};

View File

@ -1,100 +1,135 @@
import { create } from "zustand";
import { create } from 'zustand';
export type Royalty = {
address: string;
value: number;
address: string;
value: number;
};
type NotificationType = 'success' | 'danger' | 'warning' | 'info';
interface Notification {
id: string;
message: string;
type: NotificationType;
createdAt: number;
}
type RootStore = {
name: string;
setName: (name: string) => void;
name: string;
setName: (name: string) => void;
author: string;
setAuthor: (author: string) => void;
author: string;
setAuthor: (author: string) => void;
file: File | null;
setFile: (file: File | null) => void;
file: File | null;
setFile: (file: File | null) => void;
fileType: string;
setFileType: (type: string) => void;
fileType: string;
setFileType: (type: string) => void;
fileSrc: string;
setFileSrc: (fileSrc: string) => void;
fileSrc: string;
setFileSrc: (fileSrc: string) => void;
allowCover: boolean;
setAllowCover: (allowCover: boolean) => void;
allowCover: boolean;
setAllowCover: (allowCover: boolean) => void;
allowDwnld: boolean;
setAllowDwnld: (allowDwnld: boolean) => void;
allowDwnld: boolean;
setAllowDwnld: (allowDwnld: boolean) => void;
cover: File | null;
setCover: (cover: File | null) => void;
cover: File | null;
setCover: (cover: File | null) => void;
isPercentHintOpen: boolean;
setPercentHintOpen: (isPercentHintOpen: boolean) => void;
isPercentHintOpen: boolean;
setPercentHintOpen: (isPercentHintOpen: boolean) => void;
authors: string[];
setAuthors: (authors: string[]) => void;
authors: string[];
setAuthors: (authors: string[]) => void;
royalty: Royalty[];
setRoyalty: (authors: Royalty[]) => void;
royalty: Royalty[];
setRoyalty: (authors: Royalty[]) => void;
price: number;
setPrice: (price: number) => void;
price: number;
setPrice: (price: number) => void;
allowResale: boolean;
setAllowResale: (allowResale: boolean) => void;
allowResale: boolean;
setAllowResale: (allowResale: boolean) => void;
licenseResalePrice: number;
setLicenseResalePrice: (licenseResalePrice: number) => void;
licenseResalePrice: number;
setLicenseResalePrice: (licenseResalePrice: number) => void;
hashtags: string[];
setHashtags: (hashtags: string[]) => void;
hashtags: string[];
setHashtags: (hashtags: string[]) => void;
notifications: Notification[];
addNotification: (message: string, type: NotificationType) => void;
removeNotification: (id: string) => void;
};
export const useRootStore = create<RootStore>((set) => ({
name: "",
setName: (name) => set({ name }),
name: '',
setName: (name) => set({ name }),
author: "",
setAuthor: (author) => set({ author }),
author: '',
setAuthor: (author) => set({ author }),
file: null,
setFile: (file) => set({ file }),
file: null,
setFile: (file) => set({ file }),
fileType: "",
setFileType: (fileType) => set({ fileType }),
fileType: '',
setFileType: (fileType) => set({ fileType }),
fileSrc: "",
setFileSrc: (fileSrc) => set({ fileSrc }),
fileSrc: '',
setFileSrc: (fileSrc) => set({ fileSrc }),
allowCover: false,
setAllowCover: (allowCover) => set({ allowCover }),
allowCover: false,
setAllowCover: (allowCover) => set({ allowCover }),
allowDwnld: false,
setAllowDwnld: (allowDwnld) => set({ allowDwnld }),
allowDwnld: false,
setAllowDwnld: (allowDwnld) => set({ allowDwnld }),
cover: null,
setCover: (cover) => set({ cover }),
cover: null,
setCover: (cover) => set({ cover }),
isPercentHintOpen: true,
setPercentHintOpen: (isPercentHintOpen) => set({ isPercentHintOpen }),
isPercentHintOpen: true,
setPercentHintOpen: (isPercentHintOpen) => set({ isPercentHintOpen }),
authors: [],
setAuthors: (authors) => set({ authors }),
authors: [],
setAuthors: (authors) => set({ authors }),
royalty: [],
setRoyalty: (royalty) => set({ royalty }),
royalty: [],
setRoyalty: (royalty) => set({ royalty }),
price: 0.15,
setPrice: (price: number) => set({ price }),
price: 0.15,
setPrice: (price: number) => set({ price }),
allowResale: false,
setAllowResale: (allowResale) => set({ allowResale }),
allowResale: false,
setAllowResale: (allowResale) => set({ allowResale }),
licenseResalePrice: 0,
setLicenseResalePrice: (licenseResalePrice) => set({ licenseResalePrice }),
licenseResalePrice: 0,
setLicenseResalePrice: (licenseResalePrice) => set({ licenseResalePrice }),
hashtags: [],
setHashtags: (hashtags: string[]) => set({ hashtags }),
hashtags: [],
setHashtags: (hashtags: string[]) => set({ hashtags }),
notifications: [],
addNotification: (message: string, type: NotificationType = 'info') => {
const id = Date.now().toString();
const notification = {
id,
message,
type,
createdAt: Date.now(),
};
set((state) => ({
notifications: [...state.notifications, notification],
}));
},
removeNotification: (id: string) => {
set((state) => ({
notifications: state.notifications.filter((notification) => notification.id !== id),
}));
},
}));

View File

@ -0,0 +1,77 @@
import { useEffect, useState } from 'react';
import { useRootStore } from '~/shared/stores/root';
interface AnimatedNotification {
id: string;
message: string;
type: 'success' | 'danger' | 'warning' | 'info';
isExiting: boolean;
}
export const Notification = () => {
const { notifications, removeNotification } = useRootStore();
const [animatedNotifications, setAnimatedNotifications] = useState<AnimatedNotification[]>([]);
useEffect(() => {
const currentIds = animatedNotifications.map((n) => n.id);
const newNotifications = notifications
.filter((n) => !currentIds.includes(n.id))
.map((n) => ({ ...n, isExiting: false }));
if (newNotifications.length > 0) {
newNotifications.forEach((notification) => {
setTimeout(() => {
handleRemove(notification.id);
}, 4000);
});
setAnimatedNotifications((prev) => [...prev, ...newNotifications]);
}
}, [notifications]);
// Функция для начала анимации удаления
const handleRemove = (id: string) => {
setAnimatedNotifications((prev) =>
prev.map((n) => (n.id === id ? { ...n, isExiting: true } : n))
);
setTimeout(() => {
setAnimatedNotifications((prev) => prev.filter((n) => n.id !== id));
removeNotification(id);
}, 300);
};
return (
<div className="fixed inset-x-0 top-2 z-50 flex flex-col items-center gap-2 pointer-events-none">
{animatedNotifications.map((notification) => (
<div
key={notification.id}
onClick={() => handleRemove(notification.id)}
className={`
p-2 rounded shadow-md relative transition-all duration-300 max-w-sm
transform origin-top pointer-events-auto cursor-pointer
${notification.isExiting ? 'animate-fade-out' : 'animate-slide-in-top'}
${getNotificationClass(notification.type)}
`}
>
<div className="text-center">{notification.message}</div>
</div>
))}
</div>
);
};
const getNotificationClass = (type: 'success' | 'danger' | 'warning' | 'info'): string => {
const baseStyle = 'border border-white bg-opacity-70 backdrop-blur-sm';
switch (type) {
case 'success':
return `${baseStyle} bg-primary text-white`;
case 'danger':
return `${baseStyle} bg-primary text-white`;
case 'warning':
return `${baseStyle} bg-primary text-white`;
case 'info':
default:
return `${baseStyle} bg-primary text-white`;
}
};

View File

@ -1,13 +1,33 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
gray: "#1d1d1b",
primary: "#e40615",
},
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
gray: '#1d1d1b',
primary: '#e40615',
},
keyframes: {
'slide-in-top': {
'0%': {
transform: 'translateY(-1rem)',
opacity: '0',
},
'100%': {
transform: 'translateY(0)',
opacity: '1',
},
},
'fade-out': {
'0%': { opacity: '1' },
'100%': { opacity: '0' },
},
},
animation: {
'slide-in-top': 'slide-in-top 0.3s ease-out forwards',
'fade-out': 'fade-out 0.3s ease-out forwards',
},
},
},
},
plugins: [],
plugins: [],
};

119
yarn.lock
View File

@ -441,6 +441,96 @@
"@sentry/core" "9.1.0"
hoist-non-react-statics "^3.3.2"
"@telegram-apps/bridge@^1.9.2":
version "1.9.2"
resolved "https://registry.npmjs.org/@telegram-apps/bridge/-/bridge-1.9.2.tgz"
integrity sha512-SJLcNWLXhbbZr9MiqFH/g2ceuitSJKMxUIZysK4zUNyTUNuonrQG80Q/yrO+XiNbKUj8WdDNM86NBARhuyyinQ==
dependencies:
"@telegram-apps/signals" "^1.1.1"
"@telegram-apps/toolkit" "^1.1.1"
"@telegram-apps/transformers" "^1.2.2"
"@telegram-apps/types" "^1.2.1"
"@telegram-apps/bridge@^2.4.0":
version "2.4.0"
resolved "https://registry.npmjs.org/@telegram-apps/bridge/-/bridge-2.4.0.tgz"
integrity sha512-Lp/vhspF7okK8zXvSWWirunKXOPE6Gr11o9VBne4VmKG/yHRhEW7Pf07ncPtXLLzI6wW8+VYc3khsHPABJymEw==
dependencies:
"@telegram-apps/signals" "^1.1.1"
"@telegram-apps/toolkit" "^2.0.0"
"@telegram-apps/transformers" "^2.2.0"
"@telegram-apps/types" "^2.0.0"
better-promises "^0.4.0"
error-kid "^0.0.4"
mitt "^3.0.1"
valibot "1.0.0-beta.14"
"@telegram-apps/navigation@^1.0.13":
version "1.0.13"
resolved "https://registry.npmjs.org/@telegram-apps/navigation/-/navigation-1.0.13.tgz"
integrity sha512-TsUueB5LQp77GQHoMa93nq26Uw7GJjrFCPbyseMVU7aBBxAc+8CV2IYytRwcVp5sv/q7ThK5X4JaKn2V1yBHDQ==
dependencies:
"@telegram-apps/bridge" "^1.9.2"
"@telegram-apps/signals" "^1.1.1"
"@telegram-apps/toolkit" "^1.1.1"
"@telegram-apps/sdk@^3.5.1":
version "3.5.1"
resolved "https://registry.npmjs.org/@telegram-apps/sdk/-/sdk-3.5.1.tgz"
integrity sha512-m/ynpSozXsqq6Kfb6M9fm8SD6x/+jqvTFT59FuuOBvC+G8eMd0F+fvZdr6Pj0I6IL9M67nrfm92JpTu/aVNEjw==
dependencies:
"@telegram-apps/bridge" "^2.4.0"
"@telegram-apps/navigation" "^1.0.13"
"@telegram-apps/signals" "^1.1.1"
"@telegram-apps/toolkit" "^2.0.0"
"@telegram-apps/transformers" "^2.2.0"
"@telegram-apps/types" "^2.0.0"
better-promises "^0.4.0"
error-kid "^0.0.4"
valibot "1.0.0-beta.14"
"@telegram-apps/signals@^1.1.1":
version "1.1.1"
resolved "https://registry.npmjs.org/@telegram-apps/signals/-/signals-1.1.1.tgz"
integrity sha512-vz37r8lemGpPzDiBRfqpXYBynzmy3SFnY6zfHsTZABTYYt0b0WQZyU5mFDqqqugGhka78Gy11xmr9csgy4YgGA==
"@telegram-apps/toolkit@^1.1.1":
version "1.1.1"
resolved "https://registry.npmjs.org/@telegram-apps/toolkit/-/toolkit-1.1.1.tgz"
integrity sha512-+vhKx6ngfvjyTE6Xagl3z1TPVbfx5s7xAkcYzCdHYUo6T60jLIqLgyZMcI1UPoIAMuMu1pHoO+p8QNCj/+tFmw==
"@telegram-apps/toolkit@^2.0.0":
version "2.0.0"
resolved "https://registry.npmjs.org/@telegram-apps/toolkit/-/toolkit-2.0.0.tgz"
integrity sha512-1GKTLBNme1Phu/gFvgS9NWPq+LhPfzSIfnwhcF9I/6tCdufrLRcVaSMRiK9R4VDYD6iZUyj+a2l250qWAxxjQQ==
"@telegram-apps/transformers@^1.2.2":
version "1.2.2"
resolved "https://registry.npmjs.org/@telegram-apps/transformers/-/transformers-1.2.2.tgz"
integrity sha512-vvMwXckd1D7Ozc0h66PSUwF5QLrRV9HlGJFFeBuUex8QEk5mSPtsJkLiqB8aBbwuFDa91+TUSM/CxqPZO/e9YQ==
dependencies:
"@telegram-apps/toolkit" "^1.1.1"
"@telegram-apps/types" "^1.2.1"
"@telegram-apps/transformers@^2.2.0":
version "2.2.0"
resolved "https://registry.npmjs.org/@telegram-apps/transformers/-/transformers-2.2.0.tgz"
integrity sha512-wqXXOukhEjZKhzdq5vG1LkxWL11DApbmUKzk+3nA/ki3TLyD3awVTOXbpoNdOwFl2xliIooYcsUOEl4WCyyLGw==
dependencies:
"@telegram-apps/toolkit" "^2.0.0"
"@telegram-apps/types" "^2.0.0"
valibot "1.0.0-beta.14"
"@telegram-apps/types@^1.2.1":
version "1.2.1"
resolved "https://registry.npmjs.org/@telegram-apps/types/-/types-1.2.1.tgz"
integrity sha512-so4HLh7clur0YyMthi9KVIgWoGpZdXlFOuQjk3+Q5NAvJZ11nAheBSwPlGw/Ko92+zwvrSBE/lQyN2+p17RP+w==
"@telegram-apps/types@^2.0.0":
version "2.0.0"
resolved "https://registry.npmjs.org/@telegram-apps/types/-/types-2.0.0.tgz"
integrity sha512-yF499FJK82a2IDNDAQdrmVH3sgFZl/QFNdVZKgWpgtunIVJ1fres5wi9+4aUBRVIdQwZOZZqB/AOvYYuYXsq3Q==
"@ton/core@^0.59.1":
version "0.59.1"
resolved "https://registry.npmjs.org/@ton/core/-/core-0.59.1.tgz"
@ -920,6 +1010,13 @@ base64-js@^1.3.1:
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
better-promises@^0.4.0:
version "0.4.0"
resolved "https://registry.npmjs.org/better-promises/-/better-promises-0.4.0.tgz"
integrity sha512-AcKkTUSd4o1vMf41eBbHW1NkY7vrXeNI6etitGdQE54WFXsF2wkfonrKA06Za7lViRNyT/cMvj5z+DScqhYW8A==
dependencies:
error-kid "^0.0.4"
big-integer@^1.6.16:
version "1.6.52"
resolved "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz"
@ -1234,6 +1331,11 @@ emoji-regex@^9.2.2:
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz"
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
error-kid@^0.0.4:
version "0.0.4"
resolved "https://registry.npmjs.org/error-kid/-/error-kid-0.0.4.tgz"
integrity sha512-x+yQhY56SorLMnX6kOf+z3JCv2QBurcWEDcIjgxYtVr4fGeCfAtOdZOCyWttkHHDFPtL2PqnaRUmphbmALJd9w==
es-abstract@^1.22.1, es-abstract@^1.22.3:
version "1.22.5"
resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.5.tgz"
@ -2076,11 +2178,6 @@ json5@^2.2.3:
resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
jssha@^3.3.1:
version "3.3.1"
resolved "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz"
integrity sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==
jssha@3.2.0:
version "3.2.0"
resolved "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz"
@ -2245,6 +2342,11 @@ minimist@^1.2.0, minimist@^1.2.6:
resolved "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz"
integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==
mitt@^3.0.1:
version "3.0.1"
resolved "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz"
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
ms@^2.1.1, ms@2.1.2:
version "2.1.2"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
@ -3146,7 +3248,7 @@ typed-array-length@^1.0.5:
is-typed-array "^1.1.13"
possible-typed-array-names "^1.0.0"
typescript@^5.0.0, typescript@^5.2.2, typescript@>=4.2.0:
typescript@^5.0.0, typescript@^5.2.2, typescript@>=4.2.0, typescript@>=5:
version "5.3.3"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
@ -3204,6 +3306,11 @@ util-deprecate@^1.0.2:
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
valibot@1.0.0-beta.14:
version "1.0.0-beta.14"
resolved "https://registry.npmjs.org/valibot/-/valibot-1.0.0-beta.14.tgz"
integrity sha512-tLyV2rE5QL6U29MFy3xt4AqMrn+/HErcp2ZThASnQvPMwfSozjV1uBGKIGiegtZIGjinJqn0SlBdannf18wENA==
vite-tsconfig-paths@^4.3.1:
version "4.3.1"
resolved "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.1.tgz"