From fc9eb0baea73cc3ef9c1331d8e2991f259f4849a Mon Sep 17 00:00:00 2001 From: kosyachniy Date: Fri, 28 Nov 2025 18:35:49 +0300 Subject: [PATCH] Demo Components --- .../features/demo/components/CounterDemo.tsx | 70 ++++++ .../demo/components/MultiFileUploadDemo.tsx | 97 ++++++++ .../features/demo/components/PopupDemo.tsx | 175 ++++++++++++++ .../features/demo/components/ToastDemo.tsx | 226 ++++++++++++++++++ web/src/features/demo/components/UserDemo.tsx | 97 ++++++++ web/src/features/demo/index.ts | 7 + 6 files changed, 672 insertions(+) create mode 100644 web/src/features/demo/components/CounterDemo.tsx create mode 100644 web/src/features/demo/components/MultiFileUploadDemo.tsx create mode 100644 web/src/features/demo/components/PopupDemo.tsx create mode 100644 web/src/features/demo/components/ToastDemo.tsx create mode 100644 web/src/features/demo/components/UserDemo.tsx create mode 100644 web/src/features/demo/index.ts diff --git a/web/src/features/demo/components/CounterDemo.tsx b/web/src/features/demo/components/CounterDemo.tsx new file mode 100644 index 00000000..1463211b --- /dev/null +++ b/web/src/features/demo/components/CounterDemo.tsx @@ -0,0 +1,70 @@ +'use client' + +import { Button } from '@/shared/ui/button' +import { Box } from '@/shared/ui/box' +import { PageHeader } from '@/shared/ui/page-header' +import { CalculatorIcon } from '@/shared/ui/icons' +import { useAppDispatch, useAppSelector } from '@/shared/stores/store' +import { increment, decrement, incrementByAmount, reset } from '../stores/counterSlice' +import { useTranslations } from 'next-intl' + +export function CounterDemo() { + const t = useTranslations('counter') + const count = useAppSelector((state) => state.counter.value) + const dispatch = useAppDispatch() + + return ( +
+ } + iconClassName="bg-indigo-500/15 text-indigo-600 dark:bg-indigo-500/20 dark:text-indigo-400" + title={t('title')} + description={t('description')} + /> + + +
+
+
+ {count} +
+
+ +
+ + + + + + + +
+
+
+
+ ) +} diff --git a/web/src/features/demo/components/MultiFileUploadDemo.tsx b/web/src/features/demo/components/MultiFileUploadDemo.tsx new file mode 100644 index 00000000..3e06612d --- /dev/null +++ b/web/src/features/demo/components/MultiFileUploadDemo.tsx @@ -0,0 +1,97 @@ +'use client'; + +import React, { useState } from 'react'; +import { useTranslations } from 'next-intl'; +import { MultiFileUpload, FileData } from '@/shared/ui/multi-file-upload'; +import { Box } from '@/shared/ui/box'; +import { Button } from '@/shared/ui/button'; +import { PageHeader } from '@/shared/ui/page-header'; +import { ImageIcon } from '@/shared/ui/icons'; + + +export function MultiFileUploadDemo() { + const t = useTranslations('multiFileUploadDemo'); + const tMultiUpload = useTranslations('multiFileUpload'); + const [files, setFiles] = useState([]); + + const handleFilesChange = (newFiles: FileData[]) => { + setFiles(newFiles); + console.log('Files changed:', newFiles); + }; + + const handleClearAll = () => { + setFiles([]); + }; + + + const getSelectedFilesText = () => { + const plural = files.length === 1 ? '' : 's'; + return t('selectedFiles', { count: files.length, plural }); + }; + + + return ( +
+ } + iconClassName="bg-purple-500/15 text-purple-600 dark:bg-purple-500/20 dark:text-purple-400" + title={t('title')} + description={t('description')} + /> + +
+ + + {/* Multi File Upload Example */} + +
+ + + {files.length > 0 && ( +
+
+

+ {getSelectedFilesText()} +

+ +
+
+ )} + + {/* Debug Section */} + {files.length > 0 && ( + <> +
+
+

{t('debugTitle')}

+
+                    {JSON.stringify(
+                      files.map(file => ({
+                        fileName: file.file?.name,
+                        fileSize: file.file?.size,
+                        fileType: file.type,
+                        hasPreview: !!file.preview,
+                      })),
+                      null,
+                      2
+                    )}
+                  
+
+ + )} +
+
+
+
+ ); +} diff --git a/web/src/features/demo/components/PopupDemo.tsx b/web/src/features/demo/components/PopupDemo.tsx new file mode 100644 index 00000000..86e7c807 --- /dev/null +++ b/web/src/features/demo/components/PopupDemo.tsx @@ -0,0 +1,175 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/shared/ui/button'; +import { Input } from '@/shared/ui/input'; +import { Box } from '@/shared/ui/box'; +import { PageHeader } from '@/shared/ui/page-header'; +import { WindowIcon } from '@/shared/ui/icons'; +import { usePopupActions } from '@/widgets/feedback-system'; + +export default function PopupDemo() { + const [customInput, setCustomInput] = useState(''); + const { alert, confirm, confirmDelete, success, error, show, close } = usePopupActions(); + + const handleAlert = async () => { + await alert({ + title: 'Information', + message: 'This is a simple alert popup with an OK button.' + }); + console.log('Alert closed'); + }; + + const handleConfirm = async () => { + const result = await confirm({ + title: 'Confirmation Required', + message: 'Do you want to proceed with this action?', + confirmText: 'Yes, Proceed', + cancelText: 'Cancel' + }); + + if (result) { + success('Action confirmed successfully!'); + } else { + console.log('Action cancelled'); + } + }; + + const handleDelete = async () => { + const result = await confirmDelete('This action cannot be undone. Are you sure?'); + + if (result) { + success('Item deleted successfully!'); + } + }; + + const handleCustomPopup = () => { + show({ + title: 'Custom Popup', + size: 'lg', + children: ( +
+

+ This is a custom popup with interactive content. +

+ setCustomInput(e.target.value)} + /> +
+ + +
+
+ ) + }); + }; + + const handleTransparentPopup = () => { + show({ + title: 'Transparent Background', + overlay: 'transparent', + children: ( +
+

+ This popup has a transparent background. Click outside to close. +

+ +
+ ) + }); + }; + + const handleFullScreenPopup = () => { + show({ + title: 'Full Screen Content', + size: 'full', + children: ( +
+

+ This popup takes up most of the screen space, perfect for forms or detailed content. +

+
+ + + + +
+
+ + +
+
+ ) + }); + }; + + return ( +
+ } + iconClassName="bg-orange-500/15 text-orange-600 dark:bg-orange-500/20 dark:text-orange-400" + title="Popup System Demo" + description="Interactive modal dialogs and popup components" + /> + + +
+ +
+ + + + + + + + + + + + + + + +
+
+
+
+ ); +} diff --git a/web/src/features/demo/components/ToastDemo.tsx b/web/src/features/demo/components/ToastDemo.tsx new file mode 100644 index 00000000..da51bfc0 --- /dev/null +++ b/web/src/features/demo/components/ToastDemo.tsx @@ -0,0 +1,226 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/shared/ui/button'; +import { Input } from '@/shared/ui/input'; +import { Box } from '@/shared/ui/box'; +import { PageHeader } from '@/shared/ui/page-header'; +import { BellIcon } from '@/shared/ui/icons'; +import { useToast, useToastActions } from '@/shared/hooks/useToast'; + +export default function ToastDemo() { + const [customMessage, setCustomMessage] = useState(''); + const [loadingToastId, setLoadingToastId] = useState(null); + + const toast = useToast(); + const { + success, + error, + warning, + info, + loading, + promise, + saveSuccess, + deleteSuccess + } = useToastActions(); + + const handleBasicToasts = () => { + toast.toast('This is a basic toast message'); + }; + + const handleSuccessToast = () => { + success('Operation completed successfully!'); + }; + + const handleErrorToast = () => { + error('Something went wrong. Please try again.'); + }; + + const handleWarningToast = () => { + warning('This action cannot be undone.'); + }; + + const handleInfoToast = () => { + info('Here is some helpful information.'); + }; + + const handleLoadingToast = () => { + const id = loading('Processing your request...'); + setLoadingToastId(id); + + // Simulate dismissing after 3 seconds + setTimeout(() => { + toast.dismiss(id); + success('Process completed!'); + setLoadingToastId(null); + }, 3000); + }; + + const handleCustomToast = () => { + if (!customMessage.trim()) { + error('Please enter a message first!'); + return; + } + + toast.toast(customMessage, { + title: 'Custom Toast', + description: 'This toast has a custom message and description', + duration: 6000, + action: { + label: 'Undo', + onClick: () => { + info('Undo action clicked!'); + } + } + }); + + setCustomMessage(''); + }; + + const handlePromiseToast = async () => { + // Simulate an API call + const apiCall = new Promise((resolve, reject) => { + setTimeout(() => { + if (Math.random() > 0.5) { + resolve('API call successful!'); + } else { + reject(new Error('API call failed')); + } + }, 2000); + }); + + try { + await promise(apiCall, { + loading: 'Making API call...', + success: (data) => `Success: ${data}`, + error: (err) => `Error: ${err instanceof Error ? err.message : String(err)}` + }); + } catch { + // Error is handled by the promise toast + } + }; + + const handleMultipleToasts = () => { + success('First toast'); + setTimeout(() => info('Second toast'), 500); + setTimeout(() => warning('Third toast'), 1000); + setTimeout(() => error('Fourth toast'), 1500); + setTimeout(() => success('Fifth toast'), 2000); + }; + + const handleConvenienceToasts = () => { + saveSuccess('User Profile'); + setTimeout(() => deleteSuccess('Old File'), 1000); + }; + + const handlePositionChange = () => { + const positions: Array<'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'> = [ + 'top-left', 'top-center', 'top-right', + 'bottom-left', 'bottom-center', 'bottom-right' + ]; + + const currentIndex = positions.indexOf(toast.position || 'bottom-right'); + const nextPosition = positions[(currentIndex + 1) % positions.length]; + + toast.setPosition(nextPosition); + info(`Toast position changed to: ${nextPosition}`); + }; + + const handleDismissAll = () => { + toast.dismissAll(); + setTimeout(() => { + info('All previous toasts were dismissed!'); + }, 100); + }; + + return ( +
+ } + iconClassName="bg-green-500/15 text-green-600 dark:bg-green-500/20 dark:text-green-400" + title="Toast System Demo" + description="Notification system with multiple variants and positioning" + /> + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ + +
+ setCustomMessage(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleCustomToast()} + /> + +
+ +
+
+ Current Position: {toast.position} +
+
+ Max Toasts: {toast.maxToasts} +
+
+ Default Duration: {toast.defaultDuration}ms +
+
+ Active Toasts: {toast.toasts.length} +
+
+
+
+
+
+ ); +} diff --git a/web/src/features/demo/components/UserDemo.tsx b/web/src/features/demo/components/UserDemo.tsx new file mode 100644 index 00000000..1247dc2c --- /dev/null +++ b/web/src/features/demo/components/UserDemo.tsx @@ -0,0 +1,97 @@ +'use client'; + +import { Button } from '@/shared/ui/button'; +import { Box } from '@/shared/ui/box'; +import { PageHeader } from '@/shared/ui/page-header'; +import { UserIcon, ComputerIcon, SunIcon, MoonIcon, SaveIcon } from '@/shared/ui/icons'; +import { useAppDispatch, useAppSelector } from '@/shared/stores/store'; +import { setLanguage, setTheme } from '../../../features/user/stores/userSettingsSlice'; +import { useTheme } from '@/providers/ThemeProvider'; +import { useTranslations } from 'next-intl'; +import { useRouter, usePathname } from '@/i18n/routing'; +import type { Locale } from '@/i18n/routing'; + +export function UserDemo() { + const t = useTranslations('userSettings'); + const userSettings = useAppSelector((state) => state.userSettings); + const dispatch = useAppDispatch(); + const { resolvedTheme } = useTheme(); + const router = useRouter(); + const pathname = usePathname(); + + const languages: Array<{ code: Locale; name: string; flag: string }> = [ + { code: 'en', name: 'English', flag: '🇺🇸' }, + { code: 'ru', name: 'Русский', flag: '🇷🇺' }, + { code: 'zh', name: '中文', flag: '🇨🇳' }, + { code: 'es', name: 'Español', flag: '🇪🇸' }, + { code: 'ar', name: 'العربية', flag: '🇸🇦' } + ]; + + const themes = [ + { value: 'system', name: 'System', icon: ComputerIcon }, + { value: 'light', name: 'Light', icon: SunIcon }, + { value: 'dark', name: 'Dark', icon: MoonIcon } + ]; + + const getCurrentLanguage = () => languages.find(lang => lang.code === userSettings.language); + const getCurrentTheme = () => themes.find(theme => theme.value === userSettings.theme); + + const handleLanguageChange = (newLocale: Locale) => { + // Update Redux store + dispatch(setLanguage(newLocale)); + + // Update URL and next-intl routing + router.replace(pathname, { locale: newLocale }); + }; + + return ( +
+ } + iconClassName="bg-purple-500/15 text-purple-600 dark:bg-purple-500/20 dark:text-purple-400" + title={t('title')} + description={t('description')} + /> + + +
+ {/* Language Controls */} +
+

Change Language:

+
+ {languages.map((lang) => ( + + ))} +
+
+ + {/* Theme Controls */} +
+

Change Theme:

+
+ {themes.map((theme) => ( + + ))} +
+
+
+
+
+ ); +} diff --git a/web/src/features/demo/index.ts b/web/src/features/demo/index.ts new file mode 100644 index 00000000..34c1d975 --- /dev/null +++ b/web/src/features/demo/index.ts @@ -0,0 +1,7 @@ +// Demo feature public API +export { CounterDemo } from './components/CounterDemo'; +export { UserDemo } from './components/UserDemo'; +export { default as ToastDemo } from './components/ToastDemo'; +export { default as PopupDemo } from './components/PopupDemo'; +export { MultiFileUploadDemo } from './components/MultiFileUploadDemo'; +export { counterSlice } from './stores/counterSlice'; \ No newline at end of file