From 89a307e9627f52e97941afbf28c97d0d16886c8a Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Fri, 1 Aug 2025 05:48:54 +0300 Subject: [PATCH] [frontend]: add create account page --- frontend/package.json | 7 +- frontend/public/favicon.ico | Bin 0 -> 986 bytes frontend/public/file.svg | 1 - frontend/public/globe.svg | 1 - frontend/public/languages/en.json | 618 ++++++++++++++++- frontend/public/languages/ru.json | 619 +++++++++++++++++- frontend/public/next.svg | 1 - frontend/public/vercel.svg | 1 - frontend/public/window.svg | 1 - frontend/src/app/account/create/page.tsx | 20 + frontend/src/app/layout.tsx | 2 + .../components/features/auth/AuthWrapper.tsx | 38 ++ .../features/auth/forms/CreateAccountForm.tsx | 136 ++++ frontend/src/components/images/LogoImage.tsx | 43 ++ frontend/src/components/ui/common/Alert.tsx | 62 ++ frontend/src/components/ui/common/Card.tsx | 74 +++ frontend/src/components/ui/common/Form.tsx | 191 ++++++ frontend/src/components/ui/common/Input.tsx | 22 + frontend/src/components/ui/common/Label.tsx | 30 + frontend/src/components/ui/common/Toast.tsx | 25 + frontend/src/graphql/generated/output.ts | 42 ++ .../graphql/metations/auth/CreateUser.graphql | 7 + frontend/src/providers/ToastProvider.tsx | 25 + .../src/schemas/auth/create-account.schema.ts | 12 + frontend/yarn.lock | 47 +- 25 files changed, 2014 insertions(+), 11 deletions(-) create mode 100644 frontend/public/favicon.ico delete mode 100644 frontend/public/file.svg delete mode 100644 frontend/public/globe.svg delete mode 100644 frontend/public/next.svg delete mode 100644 frontend/public/vercel.svg delete mode 100644 frontend/public/window.svg create mode 100644 frontend/src/app/account/create/page.tsx create mode 100644 frontend/src/components/features/auth/AuthWrapper.tsx create mode 100644 frontend/src/components/features/auth/forms/CreateAccountForm.tsx create mode 100644 frontend/src/components/images/LogoImage.tsx create mode 100644 frontend/src/components/ui/common/Alert.tsx create mode 100644 frontend/src/components/ui/common/Card.tsx create mode 100644 frontend/src/components/ui/common/Form.tsx create mode 100644 frontend/src/components/ui/common/Input.tsx create mode 100644 frontend/src/components/ui/common/Label.tsx create mode 100644 frontend/src/components/ui/common/Toast.tsx create mode 100644 frontend/src/graphql/metations/auth/CreateUser.graphql create mode 100644 frontend/src/providers/ToastProvider.tsx create mode 100644 frontend/src/schemas/auth/create-account.schema.ts diff --git a/frontend/package.json b/frontend/package.json index 4922303..9851d32 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,8 @@ "@graphql-codegen/typescript": "4.1.6", "@graphql-codegen/typescript-operations": "4.6.1", "@graphql-codegen/typescript-react-apollo": "4.3.3", + "@hookform/resolvers": "5.2.1", + "@radix-ui/react-label": "2.1.7", "@radix-ui/react-slot": "1.2.3", "class-variance-authority": "0.7.1", "clsx": "2.1.1", @@ -25,7 +27,10 @@ "next-themes": "0.4.6", "react": "19.1.1", "react-dom": "19.1.1", - "tailwind-merge": "3.3.1" + "react-hook-form": "7.61.1", + "sonner": "2.0.6", + "tailwind-merge": "3.3.1", + "zod": "4.0.14" }, "devDependencies": { "@eslint/eslintrc": "3.3.1", diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7136c802610cf73bafab7d8373809437ec0b132b GIT binary patch literal 986 zcmeAS@N?(olHy`uVBq!ia0vp^SwLLP!3HGftQK7fq&N#aB8wRqxP?KOkzv*x37{Zj zage(c!@6@aFM%AEbVpxD28NCO+TE!@5&MW>16jU9B5@VIfO4K3JRdMSO$A zY_;=GWB&awu8Th*U}359a{}|p&rfVxD>7>So%?Zb;zsw@6OoC2T1N_9K1~kRQ2KdV zQe@(vXDnSF|FU^pml&8fDFyAwau(Fg*}CABl5cO{$$twI6YuW5m2>j>{OH=_A#GWo zJgcf6PLjE6uC!+N%;(;po?Je@FZr8C>yA4|SDT1Td=$pj(}r2BH0<>rZiRCz{JNNLLQ7$qR{gej2uxCSU{c}5(_TwDVbwLD=iq!e=4Nnzmv_3dZYt~J@G zbbIB+!kspS9lKbx!8?)R;Th zfBY2Bz4_zY)*Z_PIYoswoNQ&!|Ch1z?uU2bzklu4HZE8Fn4@1iQ$cZefTW`0?0}hy ziq!#&00 OB7>)^pUXO@geCw9`lAN` literal 0 HcmV?d00001 diff --git a/frontend/public/file.svg b/frontend/public/file.svg deleted file mode 100644 index 004145c..0000000 --- a/frontend/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/public/globe.svg b/frontend/public/globe.svg deleted file mode 100644 index 567f17b..0000000 --- a/frontend/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/public/languages/en.json b/frontend/public/languages/en.json index e294ad1..0449edf 100644 --- a/frontend/public/languages/en.json +++ b/frontend/public/languages/en.json @@ -1,5 +1,621 @@ { + "layout": { + "header": { + "logo": { + "platform": "Streaming platform" + }, + "headerMenu": { + "login": "Sign In", + "register": "Sign Up", + "profileMenu": { + "notifications": { + "heading": "Notifications", + "loading": "Loading...", + "empty": "You have no notifications" + }, + "successMessage": "Successfully logged out", + "errorMessage": "Logout error", + "channel": "My channel", + "dashboard": "Dashboard", + "logout": "Sign out" + } + }, + "search": { + "placeholder": "Search" + } + }, + "sidebar": { + "header": { + "expand": "Expand", + "collapse": "Collapse", + "navigation": "Navigation" + }, + "userNav": { + "home": "Home", + "categories": "Categories", + "streams": "Streams", + "recommended": "Recommended" + }, + "recommended": { + "heading": "Recommendations" + }, + "dashboardNav": { + "settings": "Settings", + "streamSettings": "Stream Settings", + "keys": "Stream Keys", + "chatSettings": "Chat Settings", + "followers": "Followers", + "sponsors": "Sponsors", + "premium": "Premium Plans", + "transactions": "Transactions" + } + } + }, "home": { - "title": "Home page" + "streamsHeading": "Channels You Might Like", + "categoriesHeading": "Categories You Might Like" + }, + "categories": { + "heading": "Categories", + "overview": { + "heading": "Streams for this category" + } + }, + "streams": { + "heading": "Streams", + "searchHeading": "Search by query" + }, + "stream": { + "video": { + "offline": "is offline", + "loading": "Connecting...", + "player": { + "volume": "Volume", + "fullscreen": { + "open": "Fullscreen", + "exit": "Exit Fullscreen" + } + } + }, + "info": { + "viewers": "viewers", + "offline": "Offline" + }, + "actions": { + "follow": { + "confirmUnfollowHeading": "Unfollow", + "confirmUnfollowMessage": "You will no longer receive notifications from this channel and it will disappear from your followed list.", + "unfollowButton": "Unfollow", + "followButton": "Follow", + "successFollowMessage": "You have successfully followed", + "errorFollowMessage": "Error while following", + "successUnfollowMessage": "You have successfully unfollowed", + "errorUnfollowMessage": "Error while unfollowing" + }, + "support": { + "alreadySponsor": "You are already a sponsor", + "supportAuthor": "Support the author", + "perMonth": "per month", + "choose": "Choose", + "errorMessage": "Error while creating payment" + }, + "share": { + "heading": "Share" + } + }, + "aboutChannel": { + "heading": "About", + "followersCount": "followers", + "noDescription": "Description not provided." + }, + "sponsors": { + "heading": "Channel Sponsors" + }, + "chat": { + "heading": "Chat", + "unavailable": "Chat unavailable", + "unavailableMessage": "Chat is unavailable while there is no stream on the channel. Please check back later!", + "loading": "Connecting...", + "info": { + "authRequired": "Authorization required", + "chatDisabled": "Chat disabled", + "premiumFollowersOnly": "For sponsors only", + "followersOnly": "For followers only" + }, + "sendMessage": { + "placeholder": "Send a message", + "emojiPlaceholder": "Search", + "errorMessage": "Error while sending message." + } + }, + "settings": { + "heading": "Streaming Settings", + "thumbnail": { + "updateButton": "Update Image", + "confirmModal": { + "heading": "Stream Thumbnail Removal", + "message": "Are you sure you want to delete the stream thumbnail? This action cannot be undone." + }, + "info": "Supported formats: JPG, JPEG, PNG, or GIF. Max size: 10 MB.", + "successUpdateMessage": "Stream image successfully updated", + "errorUpdateMessage": "Error updating the stream image", + "successRemoveMessage": "Stream image successfully removed", + "errorRemoveMessage": "Error removing the stream image" + }, + "info": { + "titleLabel": "Title", + "titlePlaceholder": "GTA 5 Stream", + "titleDescription": "Enter the title of your stream. It should be short and memorable.", + "categoryLabel": "Category", + "categoryPlaceholder": "Select a category", + "categoryDescription": "Select the category your content belongs to. This will help viewers find your streams more easily.", + "submitButton": "Save Changes", + "successMessage": "Stream settings successfully updated", + "errorMessage": "Error updating stream settings" + } + } + }, + "success": { + "heading": "Payment Successful!", + "details": { + "heading": "Sponsorship Details:", + "price": "Price:", + "duration": "Duration:", + "channel": "Channel:" + }, + "congratulations": "You now have access to the channel's premium content!", + "backToHome": "Back to Home", + "backToChannel": "Back to Channel", + "support": "If you have any questions, contact our support." + }, + "notFound": { + "description": "Oops, something went wrong.", + "backToHome": "Back to Home" + }, + "auth": { + "register": { + "heading": "Register on TeaStream", + "backButtonLabel": "Already have an account? Sign in", + "usernameLabel": "Username", + "usernameDescription": "This is the name other users will know you by. You can change it later.", + "emailLabel": "Email", + "emailDescription": "We may use your email for sending account-related notifications.", + "passwordLabel": "Password", + "passwordDescription": "The password must contain at least 8 characters.", + "submitButton": "Sign up", + "successAlertTitle": "Check Your Email", + "successAlertDescription": "A verification email has been sent to your inbox. If you don't see the email, check your spam folder", + "errorMessage": "Error when creating an account" + }, + "verify": { + "heading": "Account verification", + "successMessage": "Account verified", + "errorMessage": "Verification error" + }, + "login": { + "heading": "Login to TeaStream", + "backButtonLabel": "Don't have an account? Sign up!", + "loginLabel": "Login", + "loginDescription": "Your username or email that you used during registration.", + "passwordLabel": "Password", + "passwordDescription": "The password you used during registration.", + "pinLabel": "6-digit code", + "pinDescription": "Enter the code from your authentication app.", + "forgotPassword": "Forgot password?", + "submitButton": "Login", + "successMessage": "You have successfully logged in", + "errorMessage": "Error when logging in" + }, + "resetPassword": { + "heading": "Reset password", + "backButtonLabel": "Already have an account? Sign in", + "emailLabel": "Email", + "emailDescription": "The email you used during registration", + "submitButton": "Reset password", + "successAlertTitle": "Link sent", + "successAlertDescription": "We have sent a password reset link to your email. If you have enabled Telegram notifications, the link was sent there as well", + "errorMessage": "Password reset error" + }, + "newPassword": { + "heading": "New password", + "backButtonLabel": "Already have an account? Sign in", + "passwordLabel": "New password", + "passwordDescription": "The password you used during registration", + "passwordRepeatLabel": "Repeat password", + "passwordRepeatDescription": "Repeat your new password for confirmation", + "submitButton": "Set new password", + "successMessage": "Password successfully changed", + "errorMessage": "Error when setting a new password" + }, + "deactivate": { + "heading": "Deactivate Account", + "backButtonLabel": "Go to Dashboard", + "emailLabel": "Email", + "emailDescription": "Your email that you used during registration", + "passwordLabel": "Password", + "passwordDescription": "The password you used during registration", + "pinLabel": "6-digit code", + "pinDescription": "A code has been sent to your email. If you enabled Telegram notifications, we sent the code there as well", + "submitButton": "Deactivate", + "successMessage": "You have successfully deactivated your account", + "errorMessage": "Deactivation error" + } + }, + "dashboard": { + "settings": { + "header": { + "heading": "Settings", + "description": "Here you can manage your settings", + "profile": "Profile", + "account": "Account", + "appearance": "Appearance", + "notifications": "Notifications", + "sessions": "Sessions" + }, + "profile": { + "header": { + "heading": "Profile", + "description": "Customize your profile by changing your avatar, personal information, and social media links. Update your information so that other users can learn more about you." + }, + "avatar": { + "heading": "Profile Image", + "updateButton": "Update Profile Image", + "confirmModal": { + "heading": "Remove Avatar", + "message": "Are you sure you want to remove your profile image? This action cannot be undone." + }, + "info": "Supported formats: JPG, JPEG, PNG, WEBP or GIF. Max size: 10 MB.", + "successUpdateMessage": "Profile image updated successfully.", + "errorUpdateMessage": "Error updating profile image.", + "successRemoveMessage": "Profile image removed successfully.", + "errorRemoveMessage": "Error removing profile image." + }, + "info": { + "heading": "Profile Settings", + "usernameLabel": "Username", + "usernamePlaceholder": "johndoe", + "usernameDescription": "This is how other users will know you.", + "displayNameLabel": "Display Name", + "displayNamePlaceholder": "John Doe", + "displayNameDescription": "You can use uppercase letters in your display name as you wish.", + "bioLabel": "About Me", + "bioPlaceholder": "I love programming", + "bioDescription": "The information about yourself should contain no more than 300 characters.", + "successMessage": "Profile successfully updated", + "errorMessage": "Error updating profile", + "submitButton": "Save Changes" + }, + "socialLinks": { + "createForm": { + "heading": "Social Media Links", + "titleLabel": "Link Title", + "titlePlaceholder": "Youtube", + "titleDescription": "Link text", + "urlLabel": "Link URL", + "urlPlaceholder": "https://github.com/TeaCoder52", + "urlDescription": "Where does this link lead? Please enter the full URL, e.g., https://github.com/TeaCoder52", + "submitButton": "Add", + "successMessage": "Social media link successfully added", + "errorMessage": "Error adding social media link" + }, + "editForm": { + "cancelButton": "Cancel", + "submitButton": "Save", + "successUpdateMessage": "Social media link successfully updated", + "errorUpdateMessage": "Error updating social media link", + "successRemoveMessage": "Social media link successfully removed", + "errorRemoveMessage": "Error removing social media link" + }, + "successReorderMessage": "Order of social media links changed", + "errorReorderMessage": "Error changing order of social media links" + } + }, + "account": { + "header": { + "heading": "Account", + "description": "Manage your account settings, including access changes, security, and deactivation options.", + "securityHeading": "Security", + "securityDescription": "Set up two-factor authentication and auto-renewals for enhanced protection of your data.", + "deactivationHeading": "Deactivation", + "deactivationDescription": "If you want to temporarily or permanently disable your account, use this option. Be aware of the consequences." + }, + "email": { + "heading": "Email Address", + "emailLabel": "Email", + "emailDescription": "Enter your new email address.", + "submitButton": "Save", + "successMessage": "Email successfully updated", + "errorMessage": "Error during mail update" + }, + "password": { + "heading": "Account Password", + "oldPasswordLabel": "Old Password", + "oldPasswordDescription": "Enter your old password to verify your identity before changing your password. This is necessary to ensure the security of your account.", + "newPasswordLabel": "New Password", + "newPasswordDescription": "Your new password must be at least 8 characters long. It is recommended to use special characters for added security.", + "submitButton": "Save", + "successMessage": "Password successfully updated", + "errorMessage": "Error when updating password" + }, + "twoFactor": { + "heading": "TOTP Authentication", + "description": "Enhance your account security by enabling TOTP authentication. This additional layer of protection requires entering a unique code, making your account less vulnerable to unauthorized access.", + "enable": { + "trigger": "Enable", + "heading": "Enabling TOTP", + "qrInstructions": "Scan the QR code to add TOTP", + "secretCodeLabel": "Secret Code: ", + "pinLabel": "PIN Code", + "pinDescription": "Please enter the 6-digit code provided by your authentication app.", + "submitButton": "Confirm", + "successMessage": "TOTP authentication enabled", + "errorMessage": "Error verifying the code" + }, + "disable": { + "trigger": "Disable", + "heading": "Are you sure you want to disable TOTP?", + "message": "Disabling TOTP authentication will make your account less secure. Are you sure you want to proceed?", + "successMessage": "TOTP authentication has been disabled", + "errorMessage": "Error disabling" + } + }, + "deactivation": { + "heading": "Account Deactivation", + "description": "After deactivating your account, you will not be able to log in. Your account will be completely deleted after 7 days. Until then, you can contact support to restore access and unlock your account.", + "confirmModal": { + "heading": "Deactivation Confirmation", + "message": "Are you sure you want to deactivate your account? Access can only be restored within 7 days." + }, + "button": "Deactivate" + } + }, + "appearance": { + "header": { + "heading": "Appearance", + "description": "Customize the look of the website interface to your liking. Choose a theme and language that make your experience more comfortable." + }, + "theme": { + "heading": "Change Theme", + "description": "You can choose a dark or light theme for the website interface.", + "successMessage": "The website theme has been successfully changed" + }, + "language": { + "heading": "Website Language", + "description": "Select the language that will be used on the website.", + "selectPlaceholder": "Select a language", + "successMessage": "The website language has been successfully changed" + }, + "color": { + "heading": "Accent Color", + "description": "Choose an accent color for the interface" + } + }, + "notifications": { + "header": { + "heading": "Notifications", + "description": "Configure how you want to receive notifications from TeaStream. You will receive notifications about the start of streams on the channels you are subscribed to, as well as about new followers." + }, + "siteNotifications": { + "heading": "Site Notifications", + "description": "If you turn off site notifications, you will not receive notifications about new subscribers to your channel and notifications about new streams." + }, + "telegramNotifications": { + "heading": "Telegram Notifications", + "description": "If you enable notifications via Telegram, your TeaStream account will be linked to your Telegram account, and you will receive messages about new subscriptions, new streams, password resets, and two-factor authentication codes." + }, + "successMessage": "Notification settings have been successfully updated", + "errorMessage": "Error when updating notification settings" + }, + "sessions": { + "header": { + "heading": "Sessions", + "description": "Sessions are devices that you are currently using or have used to access your TeaStream account. Here you can see the active sessions right now." + }, + "info": { + "current": "Current session", + "active": "Active sessions", + "notFound": "No active sessions found" + }, + "sessionItem": { + "deleteButton": "Delete", + "detailsButton": "Details", + "confirmModal": { + "heading": "Delete Session", + "message": "Are you sure you want to delete this session? This action will result in the irreversible deletion of the session on this device and will terminate all active actions associated with it." + }, + "successMessage": "Session deleted" + }, + "sessionModal": { + "heading": "Session Information", + "device": "Device:", + "location": "Location:", + "ipAddress": "IP Address:", + "createdAt": "Created At:" + } + } + }, + "keys": { + "header": { + "heading": "Stream Keys", + "description": "Generate a URL and key for streaming." + }, + "url": { + "heading": "URL" + }, + "key": { + "heading": "Stream Key" + }, + "instructionModal": { + "trigger": "Instruction", + "heading": "🛠️ OBS Setup Guide for Live Streaming 🛠️", + "description": "To start sharing your content with your audience, follow this step-by-step guide to connecting OBS Studio. Just a few steps, and you're ready to go live!", + "step1Title": "🔑 Step 1: Download OBS Studio and Generate Server URL and Stream Key", + "step1Description": "To start streaming, you need two key elements: Server URL and Stream Key. Follow these steps:", + "downloadObs": "Download OBS Studio", + "downloadObsDescription": "Before setting up your stream, download and install the program", + "obsLinkText": "OBS Studio", + "copyKeys": "Copy Server URL and Stream Key", + "copyKeysDescription": "Once generated, you will see two fields: Server URL and Stream Key. Click the copy icons next to each field to save them to your clipboard.", + "step2Title": "🎛️ Step 2: Set Up OBS Studio", + "step2Description": "Now that you have the required data, let’s set up OBS to work with your stream:", + "openObs": "Open OBS Studio", + "openObsDescription": "If OBS is not installed yet, download it from the official site and install it. Once launched, the main OBS page will open.", + "openStreamSettings": "Open Stream Settings", + "openStreamSettingsDescription": "In the top menu, click “File” → “Settings.” Go to the “Stream” tab.", + "enterDetails": "Enter Your Details", + "enterDetailsDescription": "In the 'Service' field, select 'Custom...'. Paste the previously copied Server URL into the 'URL' field. In the 'Stream Key' field, enter your Stream Key.", + "saveSettings": "Save Settings", + "saveSettingsDescription": "Click 'Apply' and then 'OK' to save the changes.", + "step3Title": "🚀 Step 3: Start Streaming", + "step3Description": "After successfully setting up OBS, you’re ready to start a live broadcast:", + "startStream": "Start Streaming", + "startStreamDescription": "Return to the main OBS screen and click the 'Start Streaming' button.", + "monitorStream": "Monitor Your Stream", + "monitorStreamDescription": "Your stream will begin in real-time, and you can monitor it both on the platform and in OBS.", + "congrats": "✨ Congratulations! You've successfully set up OBS for streaming on our platform. Your audience awaits new exciting moments and content! 🎉", + "close": "Close" + }, + "createModal": { + "trigger": "Generate Keys", + "heading": "Generate Keys", + "ingressTypeLabel": "Connection Type", + "ingressTypePlaceholder": "Select connection type", + "ingressTypeDescription": "Select RTMP for classic broadcasting or WHIP for WebRTC connections.", + "submitButton": "Generate", + "successMessage": "Ingress created", + "errorMessage": "Error creating ingress" + } + }, + "chat": { + "header": { + "heading": "Chat Settings", + "description": "Here you can manage your chat settings." + }, + "isChatEnabled": { + "heading": "Enable Chat", + "description": "If you enable chat, viewers will be able to send messages in the chat during your broadcast." + }, + "isChatFollowersOnly": { + "heading": "Followers-Only Chat", + "description": "If you enable this setting, only your followers will be able to send messages in the chat." + }, + "isChatPremiumFollowersOnly": { + "heading": "Premium Followers-Only Chat", + "description": "If you enable this setting, only premium followers will be able to send messages in the chat." + }, + "successMessage": "Chat settings updated successfully", + "errorMessage": "Error updating chat settings" + }, + "followers": { + "header": { + "heading": "Followers", + "description": "Here is a list of all your followers." + }, + "columns": { + "date": "Date", + "user": "User", + "actions": "Actions", + "viewChannel": "View channel" + } + }, + "sponsors": { + "header": { + "heading": "Sponsors", + "description": "Here is a list of all your sponsors." + }, + "columns": { + "date": "Expiration Date", + "user": "User", + "plan": "Plan", + "actions": "Actions", + "viewChannel": "View Channel" + } + }, + "plans": { + "header": { + "heading": "Premium Plans", + "description": "Here is the list of premium plans for your channel." + }, + "alert": { + "heading": "Channel verification required", + "description": "To create premium plans, your channel must be verified. You only need 10 subscribers to get the checkmark and unlock access to this feature." + }, + "createForm": { + "trigger": "Create Plan", + "heading": "Create Plan", + "titleLabel": "Title", + "titlePlaceholder": "Premium subscription for the channel", + "titleDescription": "Enter the name of your plan. This name will be displayed to your subscribers.", + "descriptionLabel": "Description", + "descriptionPlaceholder": "Describe what your plan includes", + "descriptionDescription": "Describe what your plan includes. This will help your subscribers understand the benefits of the subscription.", + "priceLabel": "Price", + "priceDescription": "Specify the price of your plan in currency. This is the amount users will pay for the subscription.", + "submitButton": "Create", + "successMessage": "Premium plan created", + "errorMessage": "Error while creating the plan" + }, + "columns": { + "date": "Creation Date", + "title": "Title", + "price": "Price", + "actions": "Actions", + "remove": "Remove", + "successMessage": "Plan successfully removed", + "removeMessage": "Error while removing the plan" + } + }, + "transactions": { + "header": { + "heading": "Transactions", + "description": "Here are the transactions related to channel sponsorship." + }, + "columns": { + "date": "Creation Date", + "status": "Status", + "success": "Successful", + "pending": "Pending", + "failed": "Failed", + "expired": "Expired", + "amount": "Amount" + } + } + }, + "components": { + "liveBadge": { + "text": "LIVE" + }, + "confirmModal": { + "cancel": "Cancel", + "continue": "Continue" + }, + "copyButton": { + "successMessage": "Copied" + }, + "dataTable": { + "notFound": "Nothing found" + }, + "emptyState": { + "heading": "Nothing found", + "text": "We've searched every corner of our site, but unfortunately, nothing was found for your request. Maybe you could try something else?" + } + }, + "utils": { + "formatDate": { + "months": { + "january": "january", + "february": "february", + "march": "march", + "april": "april", + "may": "may", + "june": "june", + "july": "july", + "august": "august", + "september": "september", + "october": "october", + "november": "november", + "december": "december" + } + } } } diff --git a/frontend/public/languages/ru.json b/frontend/public/languages/ru.json index 5313445..ab9b926 100644 --- a/frontend/public/languages/ru.json +++ b/frontend/public/languages/ru.json @@ -1,5 +1,622 @@ { + "layout": { + "header": { + "logo": { + "platform": "Платформа для стримов" + }, + "headerMenu": { + "login": "Войти", + "register": "Регистрация", + "profileMenu": { + "notifications": { + "heading": "Уведомления", + "loading": "Загрузка...", + "empty": "У вас нет уведомлений" + }, + "successMessage": "Вы успешно вышли из системы", + "errorMessage": "Ошибка при выходе", + "channel": "Мой канал", + "dashboard": "Панель управления", + "logout": "Выйти" + } + }, + "search": { + "placeholder": "Поиск" + } + }, + "sidebar": { + "header": { + "expand": "Развернуть", + "collapse": "Свернуть", + "navigation": "Навигация" + }, + "dashboardNav": { + "settings": "Настройки", + "streamSettings": "Настройки стрима", + "keys": "Ключи для стрима", + "chatSettings": "Настройки чата", + "followers": "Подписчики", + "sponsors": "Спонсоры", + "premium": "Премиум планы", + "transactions": "Транзакции" + }, + "userNav": { + "home": "Главная", + "categories": "Категории", + "streams": "Трансляции", + "recommended": "Рекомендации" + }, + "recommended": { + "heading": "Рекомендации" + } + } + }, "home": { - "title": "Домашняя страница" + "streamsHeading": "Каналы, которые могут вам понравиться", + "categoriesHeading": "Категории, которые могут вам понравиться" + }, + "categories": { + "heading": "Категории", + "overview": { + "heading": "Стримы к этой категории" + } + }, + "streams": { + "heading": "Трансляции", + "searchHeading": "Поиск по запросу" + }, + "stream": { + "video": { + "offline": "не в сети", + "loading": "Подключение...", + "player": { + "volume": "Громкость", + "fullscreen": { + "open": "Полноэкранный режим", + "exit": "Выйти из полноэкранного режима" + } + } + }, + "info": { + "viewers": "зрителей", + "offline": "Не в сети" + }, + "actions": { + "follow": { + "confirmUnfollowHeading": "Перестать отслеживать", + "confirmUnfollowMessage": "Вы больше не будете получать уведомления от этого канала и он пропадет из списка отслеживаемых", + "unfollowButton": "Перестать отслеживать", + "followButton": "Отслеживать", + "successFollowMessage": "Вы успешно подписались", + "errorFollowMessage": "Ошибка при подписке", + "successUnfollowMessage": "Вы успешно описались", + "errorUnfollowMessage": "Ошибка при описке" + }, + "support": { + "alreadySponsor": "Вы уже спонсор", + "supportAuthor": "Поддержать автора", + "perMonth": "в месяц", + "choose": "Выбрать", + "errorMessage": "Ошибка при создании платежа" + }, + "share": { + "heading": "Поделиться с помощью" + } + }, + "aboutChannel": { + "heading": "Информация про", + "followersCount": "фолловеров", + "noDescription": "Описание не указано" + }, + "sponsors": { + "heading": "Спонсоры канала" + }, + "chat": { + "heading": "Чат", + "unavailable": "Чат не доступен", + "unavailableMessage": "Чат недоступен, пока на канале нет трансляции. Пожалуйста, загляните позже!", + "loading": "Подключение...", + "info": { + "authRequired": "Требуется авторизация", + "chatDisabled": "Чат отключен", + "premiumFollowersOnly": "Только для спонсоров", + "followersOnly": "Только для подписчиков" + }, + "sendMessage": { + "placeholder": "Отправить сообщение", + "emojiPlaceholder": "Поиск", + "errorMessage": "Ошибка при отправке сообщения" + } + }, + "settings": { + "heading": "Параметры трансляции", + "thumbnail": { + "updateButton": "Обновить изображение", + "confirmModal": { + "heading": "Удаление превью для стрима", + "message": "Вы уверены, что хотите удалить изображение для стрима? Это действие нельзя будет отменить." + }, + "info": "Поддерживаемые форматы: JPG, JPEG, PNG или GIF. Макс. размер: 10 МБ.", + "successUpdateMessage": "Изображение стрима обновлено удалено", + "errorUpdateMessage": "Ошибка при обновлении изображения стрима", + "successRemoveMessage": "Изображение стрима обновлено удалено", + "errorRemoveMessage": "Ошибка при удалении изображения стрима" + }, + "info": { + "titleLabel": "Название", + "titlePlaceholder": "Стрим по ГТА 5", + "titleDescription": "Введите название вашего стрима. Оно должно быть коротким и запоминающимся.", + "categoryLabel": "Категория", + "categoryPlaceholder": "Выберете категорию", + "categoryDescription": "Выберите категорию, к которой относится ваш контент. Это поможет зрителям легче находить ваши трансляции.", + "submitButton": "Сохранить изменения", + "successMessage": "Настройки стрима успешно обновлены", + "errorMessage": "Ошибка при обновлении настроек стрима" + } + } + }, + "success": { + "heading": "Оплата прошла успешно!", + "details": { + "heading": "Детали спонсорства:", + "price": "Цена:", + "duration": "Длительность:", + "channel": "Канал:" + }, + "congratulations": "Теперь у вас есть доступ к премиум-контенту канала!", + "backToHome": "Вернуться на главную", + "backToChannel": "Вернуться на канал", + "support": "Если у вас возникли вопросы, свяжитесь с нашей поддержкой." + }, + "notFound": { + "description": "Упс, что-то пошло не так.", + "backToHome": "Перейти на главную" + }, + "auth": { + "register": { + "heading": "Регистрация в TeaStream", + "backButtonLabel": "Есть учетная запись? Войти", + "usernameLabel": "Имя пользователя", + "usernameDescription": "Под этим именем вас будут знать другие пользователи.", + "emailLabel": "Почта", + "emailDescription": "Мы можем использовать вашу почту для отправки сообщений касательно учетной записи.", + "passwordLabel": "Пароль", + "passwordDescription": "Пароль должен содержать не менее 8 символов.", + "submitButton": "Зарегистрироваться", + "successAlertTitle": "Проверьте свою почту", + "successAlertDescription": "На ваш адрес электронной почты было отправлено письмо для подтверждения. Если вы не видите письмо, проверьте папку «Спам»", + "errorMessage": "Ошибка при создании аккаунта" + }, + "verify": { + "heading": "Верификация аккаунта", + "successMessage": "Аккаунт верифицирован", + "errorMessage": "Ошибка при верификации" + }, + "login": { + "heading": "Войти в TeaStream", + "backButtonLabel": "У вас нет учетной записи? Зарегистрируйтесь!", + "loginLabel": "Логин", + "loginDescription": "Ваше имя или почта, которые вы вводили при регистрации.", + "passwordLabel": "Пароль", + "passwordDescription": "Пароль, который вы вводили при регистрации.", + "pinLabel": "6-значный код", + "pinDescription": "Введите код из вашего приложения для аутентификации.", + "forgotPassword": "Забыли пароль?", + "submitButton": "Войти", + "successMessage": "Вы успешно вошли в систему", + "errorMessage": "Ошибка при входе в систему" + }, + "resetPassword": { + "heading": "Сброс пароля", + "backButtonLabel": "Есть учётная запись? Войти", + "emailLabel": "Почта", + "emailDescription": "Ваша почта, которую вы вводили при регистрации", + "submitButton": "Сбросить пароль", + "successAlertTitle": "Ссылка отправлена", + "successAlertDescription": "Мы отправили ссылку для сброса пароля на вашу почту. Если у вас включены уведомления в Telegram, ссылка также была отправлена туда", + "errorMessage": "Ошибка при сбросе пароля" + }, + "newPassword": { + "heading": "Новый пароль", + "backButtonLabel": "Есть учётная запись? Войти", + "passwordLabel": "Новый пароль", + "passwordDescription": "Пароль, который вы вводили при регистрации", + "passwordRepeatLabel": "Повторите пароль", + "passwordRepeatDescription": "Повторите свой новый пароль для подтверждения", + "submitButton": "Продолжить", + "successMessage": "Пароль успешно изменён", + "errorMessage": "Ошибка при установлении нового пароля" + }, + "deactivate": { + "heading": "Деактивация аккаунта", + "backButtonLabel": "Перейти в панель управления", + "emailLabel": "Почта", + "emailDescription": "Ваша почта, которую вы вводили при регистрации", + "passwordLabel": "Пароль", + "passwordDescription": "Пароль, который вы вводили при регистрации", + "pinLabel": "6-значный код", + "pinDescription": "Вам на почту был выслан код. Если вы подключали уведомления в Telegram, мы отправили код и туда", + "submitButton": "Деактивировать", + "successMessage": "Вы успешно деактивировали аккаунт", + "errorMessage": "Ошибка при деактивации" + } + }, + "dashboard": { + "settings": { + "header": { + "heading": "Настройки", + "description": "Здесь вы можете управлять вашими настройками", + "profile": "Профиль", + "account": "Аккаунт", + "appearance": "Внешний вид", + "notifications": "Уведомления", + "sessions": "Сессии" + }, + "profile": { + "header": { + "heading": "Профиль", + "description": "Настройте ваш профиль, обновите аватар, измените информацию о себе и добавьте ссылки на социальные сети, чтобы сделать вашу страницу более привлекательной для других пользователей." + }, + "avatar": { + "heading": "Изображение профиля", + "updateButton": "Обновить изображение профиля", + "confirmModal": { + "heading": "Удаление аватара", + "message": "Вы уверены, что хотите удалить изображение профиля? Это действие нельзя будет отменить." + }, + "info": "Поддерживаемые форматы: JPG, JPEG, PNG, WEBP или GIF. Макс. размер: 10 МБ.", + "successUpdateMessage": "Изображение профиля успешно обновлено", + "errorUpdateMessage": "Ошибка при обновлении изображения профиля", + "successRemoveMessage": "Изображение профиля успешно удалено", + "errorRemoveMessage": "Ошибка при удалении изображения профиля" + }, + "info": { + "heading": "Настройки профиля", + "usernameLabel": "Имя пользователя", + "usernamePlaceholder": "johndoe", + "usernameDescription": "Под этим именем вас будут знать другие пользователи.", + "displayNameLabel": "Отображаемое имя", + "displayNamePlaceholder": "John Doe", + "displayNameDescription": "Проставьте прописные буквы в имени пользователя по своему желанию.", + "bioLabel": "О себе", + "bioPlaceholder": "Я люблю программировать", + "bioDescription": "Информация о себе должна содержать не более 300 символов.", + "submitButton": "Сохранить изменения", + "successMessage": "Профиль успешно обновлён", + "errorMessage": "Ошибка при обновлении профиля" + }, + "socialLinks": { + "createForm": { + "heading": "Ссылки на соцсети", + "titleLabel": "Название ссылки", + "titlePlaceholder": "Youtube", + "titleDescription": "Текст ссылки", + "urlLabel": "URL ссылки", + "urlPlaceholder": "https://github.com/TeaCoder52", + "urlDescription": "Куда ведет эта ссылка? Введите полный URL-адрес, например https://github.com/TeaCoder52", + "submitButton": "Добавить", + "successMessage": "Ссылка на соц. сеть успешно добавлена", + "errorMessage": "Ошибка при добавлении ссылки на соц. сеть" + }, + "editForm": { + "cancelButton": "Отмена", + "submitButton": "Сохранить", + "successUpdateMessage": "Ссылка на соц. сеть успешно изменена", + "errorUpdateMessage": "Ошибка при обновлении соц. сети", + "successRemoveMessage": "Ссылка на соц. сеть успешно удалена", + "errorRemoveMessage": "Ошибка при удалении соц. сети" + }, + "successReorderMessage": "Порядок ссылок на соц. сети изменен", + "errorReorderMessage": "Ошибка при изменении порядка соц. сетей" + } + }, + "account": { + "header": { + "heading": "Аккаунт", + "description": "Управляйте настройками вашего аккаунта, включая изменение доступа, безопасность и возможность деактивации.", + "securityHeading": "Безопасность", + "securityDescription": "Настройте двухфакторную аутентификацию для улучшенной защиты ваших данных.", + "deactivationHeading": "Деактивация", + "deactivationDescription": "Если вы хотите временно или постоянно отключить свой аккаунт, используйте эту опцию. Обратите внимание на последствия." + }, + "email": { + "heading": "Адрес электронной почты", + "emailLabel": "Почта", + "emailDescription": "Введите ваш новый адрес электронной почты.", + "submitButton": "Сохранить", + "successMessage": "Почта успешно обновлена", + "errorMessage": "Ошибка при обновлении почты" + }, + "password": { + "heading": "Пароль от аккаунта", + "oldPasswordLabel": "Старый пароль", + "oldPasswordDescription": "Введите свой старый пароль, чтобы подтвердить вашу личность перед изменением пароля. Это необходимо для обеспечения безопасности вашей учетной записи.", + "newPasswordLabel": "Новый пароль", + "newPasswordDescription": "Ваш новый пароль должен содержать не менее 8 символов. Рекомендуется использовать также специальные символы для повышения безопасности.", + "submitButton": "Сохранить", + "successMessage": "Пароль успешно обновлён", + "errorMessage": "Ошибка при обновлении пароля" + }, + "twoFactor": { + "heading": "Аутентификация с помощью TOTP", + "description": "Увеличьте безопасность вашего аккаунта, активировав аутентификацию с помощью TOTP. Этот дополнительный уровень защиты требует ввода уникального кода, что делает ваш аккаунт менее уязвимым к несанкционированному доступу.", + "enable": { + "trigger": "Включить", + "heading": "Включение TOTP", + "qrInstructions": "Сканируйте QR-код для добавления TOTP", + "secretCodeLabel": "Секретный код: ", + "pinLabel": "PIN-код", + "pinDescription": "Пожалуйста, введите 6-значный код, предоставленный вашим приложением для аутентификации.", + "submitButton": "Подтвердить", + "successMessage": "Аутентификация с помощью TOTP включена", + "errorMessage": "Ошибка при верификации кода" + }, + "disable": { + "trigger": "Выключить", + "heading": "Вы действительно хотите отключить TOTP?", + "message": "При отключении аутентификации с помощью TOTP ваш аккаунт станет менее защищенным. Вы уверены, что хотите продолжить?", + "successMessage": "Аутентификация через TOTP отключена", + "errorMessage": "Ошибка при отключении" + } + }, + "deactivation": { + "heading": "Деактивация аккаунта", + "description": "После деактивации аккаунта вы не сможете в него войти. Через 7 дней аккаунт будет полностью удалён. До этого времени вы можете обратиться в поддержку для восстановления доступа и разблокировки аккаунта.", + "confirmModal": { + "heading": "Подтверждение деактивации", + "message": "Вы уверены, что хотите деактивировать аккаунт? Восстановить доступ можно только в течение 7 дней." + }, + "button": "Деактивировать" + } + }, + "appearance": { + "header": { + "heading": "Внешний вид", + "description": "Настройте внешний вид интерфейса сайта по своему вкусу. Выберите тему и язык, которые сделают ваше взаимодействие более комфортным." + }, + "theme": { + "heading": "Смена темы", + "description": "Вы можете выбрать темную или светлую тему для интерфейса сайта", + "successMessage": "Тема сайта успешно изменена" + }, + "language": { + "heading": "Язык сайта", + "description": "Выберите язык, который будет использован на сайте", + "selectPlaceholder": "Выберите язык", + "successMessage": "Язык сайта успешно изменен" + }, + "color": { + "heading": "Цвет акцента", + "description": "Выберите цвет акцента для интерфейса" + } + }, + "notifications": { + "header": { + "heading": "Уведомления", + "description": "Настройте, как вы хотите получать уведомления от TeaStream. Вы будете получать уведомления о начале трансляций на каналах, на которые вы подписаны, а также о новых подписчиках." + }, + "siteNotifications": { + "heading": "Уведомления на сайте", + "description": "Если вы выключите уведомления на сайте, то не будете получать уведомления о новых подписчиках на ваш канал и уведомления о новых трансляциях." + }, + "telegramNotifications": { + "heading": "Уведомления в Telegram", + "description": "Если вы включите уведомления через Telegram, ваш аккаунт TeaStream свяжется с вашим Telegram-аккаунтом, и вы будете получать сообщения о новых подписках, новых стримах, сбросе пароля и коды двухфакторной аутентификации." + }, + "successMessage": "Настройки уведомлений успешно обновлены", + "errorMessage": "Ошибка при обновлении настроек уведомлений" + }, + "sessions": { + "header": { + "heading": "Сессии", + "description": "Сессии — это устройства, которые вы используете или которые использовали для входа в вашу учетную запись TeaStream. Здесь показаны активные сессии в данный момент." + }, + "info": { + "current": "Текущая сессия", + "active": "Активные сессии", + "notFound": "Активных сессий не найдено" + }, + "sessionItem": { + "deleteButton": "Удалить", + "detailsButton": "Подробнее", + "confirmModal": { + "heading": "Удаление сессии", + "message": "Вы уверены, что хотите удалить эту сессию? Это действие приведёт к необратимому удалению сессии на этом устройстве и завершит все активные действия, связанные с ним." + }, + "successMessage": "Сессия удалена", + "errorMessage": "Ошибка при удалении сессии" + }, + "sessionModal": { + "heading": "Информация о сессии", + "device": "Устройство:", + "location": "Местоположение:", + "ipAddress": "IP-адрес:", + "createdAt": "Дата создания:" + } + } + }, + "keys": { + "header": { + "heading": "Ключи для стрима", + "description": "Сгенерируйте URL и ключ для проведения трансляции." + }, + "url": { + "heading": "URL" + }, + "key": { + "heading": "Ключ" + }, + "instructionModal": { + "trigger": "Инструкция", + "heading": "🛠️ Инструкция по настройке OBS для прямых трансляций 🛠️", + "description": "Чтобы начать делиться своим контентом с аудиторией, следуйте подробной инструкции по подключению OBS Studio. Это всего несколько шагов, и вы готовы выходить в эфир!", + "step1Title": "🔑 Шаг 1: Скачивание OBS Studio и генерация Server URL и Stream Key", + "step1Description": "Чтобы начать трансляцию, вам необходимо получить два важных элемента: Server URL и Stream Key (ключ трансляции). Следуйте этим шагам:", + "downloadObs": "Скачайте OBS Studio", + "downloadObsDescription": "Прежде чем настраивать трансляцию, скачайте и установите программу", + "obsLinkText": "OBS Studio", + "copyKeys": "Скопируйте Server URL и Stream Key", + "copyKeysDescription": "После генерации на экране появятся два поля: Server URL и Stream Key. Нажмите на иконки копирования рядом с каждым из полей, чтобы сохранить данные в буфер обмена.", + "step2Title": "🎛️ Шаг 2: Настройка OBS Studio", + "step2Description": "Теперь, когда у вас есть необходимые данные, давайте настроим OBS для работы с вашей трансляцией:", + "openObs": "Откройте OBS Studio", + "openObsDescription": "Если у вас еще не установлен OBS, скачайте его с официального сайта и установите. После запуска откроется главная страница OBS.", + "openStreamSettings": "Откройте настройки трансляции", + "openStreamSettingsDescription": "В верхнем меню нажмите «Файл» → «Настройки». Перейдите во вкладку «Трансляция».", + "enterDetails": "Введите ваши данные", + "enterDetailsDescription": "В поле «Сервис» выберите опцию «Пользовательский...». Вставьте скопированный ранее Server URL в поле «URL». В поле «Ключ трансляции» вставьте ваш Stream Key.", + "saveSettings": "Сохраните настройки", + "saveSettingsDescription": "Нажмите «Применить», а затем «ОК», чтобы сохранить изменения.", + "step3Title": "🚀 Шаг 3: Начало трансляции", + "step3Description": "После успешной настройки OBS вы готовы начать прямую трансляцию:", + "startStream": "Запустите трансляцию", + "startStreamDescription": "Вернитесь на главный экран OBS и нажмите кнопку «Начать трансляцию».", + "monitorStream": "Следите за своей трансляцией", + "monitorStreamDescription": "Ваша трансляция начнется в режиме реального времени, и вы сможете наблюдать за ней как на платформе, так и в OBS.", + "congrats": "✨ Поздравляем! Теперь вы успешно настроили OBS для трансляций на нашей платформе. Ваша аудитория ждет новых ярких моментов и контента! 🎉", + "close": "Закрыть" + }, + "createModal": { + "trigger": "Сгенерировать ключи", + "heading": "Сгенерировать ключи", + "ingressTypeLabel": "Тип подключения", + "ingressTypePlaceholder": "Выберите тип подключения", + "ingressTypeDescription": "Выберите RTMP для классического вещания или WHIP для WebRTC подключений.", + "submitButton": "Сгенерировать", + "successMessage": "Входной поток создан", + "errorMessage": "Ошибка при создании входного потока" + } + }, + "chat": { + "header": { + "heading": "Настройки чата", + "description": "Здесь вы можете управлять настройками вашего чата." + }, + "isChatEnabled": { + "heading": "Включить чат", + "description": "Если вы включите чат, зрители смогут писать сообщения в чате во время вашей трансляции." + }, + "isChatFollowersOnly": { + "heading": "Чат только для подписчиков", + "description": "Если вы включите эту настройку, только ваши подписчики смогут писать сообщения в чате." + }, + "isChatPremiumFollowersOnly": { + "heading": "Чат только для премиум подписчиков", + "description": "Если вы включите эту настройку, только премиум подписчики смогут писать сообщения в чате." + }, + "successMessage": "Настройки чата успешно обновлены", + "errorMessage": "Ошибка при обновлении настроек чата " + }, + "followers": { + "header": { + "heading": "Подписчики", + "description": "Здесь отображается список всех ваших подписчиков." + }, + "columns": { + "date": "Дата подписи", + "user": "Пользователь", + "actions": "Действия", + "viewChannel": "Перейти на канал" + } + }, + "sponsors": { + "header": { + "heading": "Спонсоры", + "description": "Здесь отображается список всех ваших спонсоров." + }, + "columns": { + "date": "Дата окончания", + "user": "Пользователь", + "plan": "План", + "actions": "Действия", + "viewChannel": "Перейти на канал" + } + }, + "plans": { + "header": { + "heading": "Премиум планы", + "description": "Здесь отображается список премиум планов для вашего канала." + }, + "alert": { + "heading": "Требуется верификация канала", + "description": "Для создания премиум планов ваш канал должен быть верифицирован. Для этого вам нужно всего 10 подписчиков, чтобы получить галочку и разблокировать доступ к этой функции." + }, + "createForm": { + "trigger": "Создать план", + "heading": "Создать план", + "titleLabel": "Название", + "titlePlaceholder": "Премиум подписка на канал", + "titleDescription": "Введите название вашего плана. Это название будет отображаться вашим подписчикам.", + "descriptionLabel": "Описание", + "descriptionPlaceholder": "Опишите, что включает в себя ваш план", + "descriptionDescription": "Опишите, что включает в себя ваш план. Это поможет вашим подписчикам понять преимущества подписки.", + "priceLabel": "Цена", + "priceDescription": "Укажите цену вашего плана в валюте. Это сумма, которую пользователи будут платить за подписку.", + "submitButton": "Создать", + "successMessage": "Премиум план создан", + "errorMessage": "Ошибка при создании плана" + }, + "columns": { + "date": "Дата создания", + "title": "Название", + "price": "Цена", + "actions": "Действия", + "remove": "Удалить", + "successMessage": "План успешно удалён", + "removeMessage": "Ошибка при удалении плана" + } + }, + "transactions": { + "header": { + "heading": "Транзакции", + "description": "Здесь отображаются транзакции, связанные с оформлением спонсорства на каналы." + }, + "columns": { + "date": "Дата создания", + "status": "Статус", + "success": "Успешно", + "pending": "В ожидании", + "failed": "Ошибка", + "expired": "Истек", + "amount": "Сумма" + } + } + }, + "components": { + "liveBadge": { + "text": "В ЭФИРЕ" + }, + "confirmModal": { + "cancel": "Отмена", + "continue": "Продолжить" + }, + "copyButton": { + "successMessage": "Скопировано" + }, + "dataTable": { + "notFound": "Нечего не найдено" + }, + "emptyState": { + "heading": "Нечего не найдено", + "text": "Мы обшарили все уголки нашего сайта, но к сожалению по вашему запросу ничего не нашлось. Может попробуете что-то другое?" + } + }, + "utils": { + "formatDate": { + "months": { + "january": "января", + "february": "февраля", + "march": "марта", + "april": "апреля", + "may": "мая", + "june": "июня", + "july": "июля", + "august": "августа", + "september": "сентября", + "october": "октября", + "november": "ноября", + "december": "декабря" + } + } } } diff --git a/frontend/public/next.svg b/frontend/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/frontend/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg deleted file mode 100644 index 7705396..0000000 --- a/frontend/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/public/window.svg b/frontend/public/window.svg deleted file mode 100644 index b2b2a44..0000000 --- a/frontend/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/app/account/create/page.tsx b/frontend/src/app/account/create/page.tsx new file mode 100644 index 0000000..c69a3d3 --- /dev/null +++ b/frontend/src/app/account/create/page.tsx @@ -0,0 +1,20 @@ +import CreateAccountForm from '@/components/features/auth/forms/CreateAccountForm'; +import { Metadata } from 'next'; +import { getTranslations } from 'next-intl/server'; +import React from 'react'; + +export async function generateMetadata(): Promise { + const t = await getTranslations('auth.register'); + + return { + title: t('heading') + } +} + +const CreateAccountPage = () => { + return ( + + ); +}; + +export default CreateAccountPage; diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index f2cb1b7..2e6a234 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,5 +1,6 @@ import ApolloClientProvider from '@/providers/ApolloClientProvider'; import { ThemeProvider } from '@/providers/ThemeProvider'; +import { ToastProvider } from '@/providers/ToastProvider'; import type { Metadata } from "next"; import { NextIntlClientProvider } from 'next-intl'; import { getLocale, getMessages } from 'next-intl/server'; @@ -35,6 +36,7 @@ export default async function RootLayout({ enableSystem disableTransitionOnChange > + {children} diff --git a/frontend/src/components/features/auth/AuthWrapper.tsx b/frontend/src/components/features/auth/AuthWrapper.tsx new file mode 100644 index 0000000..7921ff6 --- /dev/null +++ b/frontend/src/components/features/auth/AuthWrapper.tsx @@ -0,0 +1,38 @@ +import { LogoImage } from '@/components/images/LogoImage'; +import { Button } from '@/components/ui/common/Button'; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/common/Card'; +import Link from 'next/link'; +import React, { FC, ReactNode } from 'react'; + +type AuthWrapperProps = { + heading: string + backButtonLabel?: string + backButtonHref?: string + children: ReactNode +} +const AuthWrapper: FC = (props) => { + const { children, backButtonHref, backButtonLabel, heading } = props; + + return ( +
+ + + + {heading} + + {children} + + {backButtonLabel && backButtonHref && ( + + + + )} + + +
+ ); +}; + +export default AuthWrapper; diff --git a/frontend/src/components/features/auth/forms/CreateAccountForm.tsx b/frontend/src/components/features/auth/forms/CreateAccountForm.tsx new file mode 100644 index 0000000..2521d5f --- /dev/null +++ b/frontend/src/components/features/auth/forms/CreateAccountForm.tsx @@ -0,0 +1,136 @@ +'use client' + +import { Button } from '@/components/ui/common/Button'; +import { createAccountSchema, TypeCreateAccountSchema } from '@/schemas/auth/create-account.schema'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { CircleCheck } from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import AuthWrapper from '../AuthWrapper'; +import React, { useState } from 'react'; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel } from '@/components/ui/common/Form'; +import { Input } from '@/components/ui/common/Input'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/common/Alert'; +import { useCreateUserMutation } from '@/graphql/generated/output'; + + +const CreateAccountForm = () => { + const [isSuccess, setIsSuccess] = useState(false) + const t = useTranslations('auth.register') + + const form = useForm({ + resolver: zodResolver(createAccountSchema), + defaultValues: { + name: '', + email: '', + password: '' + } + }) + const { isValid } = form.formState + + const [create, { loading: isLoadingCreate }] = useCreateUserMutation({ + onCompleted() { + setIsSuccess(true) + }, + onError() { + toast.error(t('errorMessage')) + } + }) + + function onSubmit(data: TypeCreateAccountSchema) { + create({ variables: { data } }) + } + + return ( + + {isSuccess ? ( + + + {t('successAlertTitle')} + + {t('successAlertDescription')} + + + ) : ( +
+ + ( + + {t('usernameLabel')} + + + + + {t('usernameDescription')} + + + )} + /> + ( + + {t('emailLabel')} + + + + + {t('emailDescription')} + + + )} + /> + ( + + {t('passwordLabel')} + + + + + {t('passwordDescription')} + + + )} + /> + + + + )} +
+ ); +}; + +export default CreateAccountForm; diff --git a/frontend/src/components/images/LogoImage.tsx b/frontend/src/components/images/LogoImage.tsx new file mode 100644 index 0000000..fc63fca --- /dev/null +++ b/frontend/src/components/images/LogoImage.tsx @@ -0,0 +1,43 @@ +export function LogoImage() { + return ( + + Logo + + + + + + + + + + + + ) +} diff --git a/frontend/src/components/ui/common/Alert.tsx b/frontend/src/components/ui/common/Alert.tsx new file mode 100644 index 0000000..de96dc3 --- /dev/null +++ b/frontend/src/components/ui/common/Alert.tsx @@ -0,0 +1,62 @@ +import { cn } from '@/utils/twMerge' +import { type VariantProps, cva } from 'class-variance-authority' +import { type HTMLAttributes, forwardRef } from 'react' + + +const alertVariants = cva( + 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', + { + variants: { + variant: { + default: 'bg-background text-foreground', + destructive: + 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive' + } + }, + defaultVariants: { + variant: 'default' + } + } +) + +const Alert = forwardRef< + HTMLDivElement, + HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = 'Alert' + +const AlertTitle = forwardRef< + HTMLParagraphElement, + HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = 'AlertTitle' + +const AlertDescription = forwardRef< + HTMLParagraphElement, + HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = 'AlertDescription' + +export { Alert, AlertDescription, AlertTitle } diff --git a/frontend/src/components/ui/common/Card.tsx b/frontend/src/components/ui/common/Card.tsx new file mode 100644 index 0000000..a9a3022 --- /dev/null +++ b/frontend/src/components/ui/common/Card.tsx @@ -0,0 +1,74 @@ +import { cn } from '@/utils/twMerge'; +import { type HTMLAttributes, forwardRef } from 'react' + + +const Card = forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +Card.displayName = 'Card' + +const CardHeader = forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardHeader.displayName = 'CardHeader' + +const CardTitle = forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardTitle.displayName = 'CardTitle' + +const CardDescription = forwardRef< + HTMLDivElement, + HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = 'CardDescription' + +const CardContent = forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardContent.displayName = 'CardContent' + +const CardFooter = forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardFooter.displayName = 'CardFooter' + +export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } diff --git a/frontend/src/components/ui/common/Form.tsx b/frontend/src/components/ui/common/Form.tsx new file mode 100644 index 0000000..a7d7a99 --- /dev/null +++ b/frontend/src/components/ui/common/Form.tsx @@ -0,0 +1,191 @@ +'use client' + +import { cn } from '@/utils/twMerge'; +import * as LabelPrimitive from '@radix-ui/react-label' +import { Slot } from '@radix-ui/react-slot' +import { + type ComponentPropsWithoutRef, + type ComponentRef, + type HTMLAttributes, + createContext, + forwardRef, + useContext, + useId +} from 'react' +import { + Controller, + type ControllerProps, + type FieldPath, + type FieldValues, + FormProvider, + useFormContext +} from 'react-hook-form' + + +import { Label } from './Label' + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props + }: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = useContext(FormFieldContext) + const itemContext = useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error('useFormField should be used within ') + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = createContext( + {} as FormItemContextValue +) + +const FormItem = forwardRef>( + ({ className, ...props }, ref) => { + const id = useId() + + return ( + +
+ + ) + } +) +FormItem.displayName = 'FormItem' + +const FormLabel = forwardRef< + ComponentRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +