feat: first pages#398
Conversation
|
""" WalkthroughThis update introduces a comprehensive suite of features and foundational infrastructure for the Essence application. Key changes include the addition of navigation and layout components, onboarding and sign-in flows, a Pinia store for essence data, utility helpers, expanded localization, new API endpoints, and schema/type refinements. UI themes and configuration are enhanced, and toast notification logic is added for user feedback. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant App
participant EssenceStore
participant API
User->>App: Loads app
App->>EssenceStore: essence.update()
EssenceStore->>API: GET /api/option
API-->>EssenceStore: { id, masterAccountExists, channels }
App->>App: If not initialized and not on /welcome
App->>User: Redirect to /welcome
User->>App: Fills onboarding form (option/master/channel)
App->>API: PUT /api/option or /api/user/master or POST /api/channel
API-->>App: Success/Error
App->>EssenceStore: essence.update()
App->>User: Show toast (success/error)
App->>User: Redirect or update UI
Possibly related PRs
Suggested labels
Poem
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 17
🧹 Nitpick comments (28)
apps/essence/public/schema.json (1)
1-5: Basic package metadata provided.The schema.json file provides essential metadata for the package. Consider expanding it with additional fields like
author,license,repository, andkeywordsto improve discoverability and provide more context about the package.{ "version": "0.1.0", "title": "@nextorders/essence", "description": "Business data collection and data management software." + "author": "NextOrders", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/nextorders/essence" + }, + "keywords": ["business", "data", "management"] }apps/essence/app/utils/helpers.ts (3)
15-22: Consider simplifying the locale value retrieval logic.The ternary expression in the return statement could be clearer with a more explicit structure.
- return data.values.find((v) => hasCurrentLocale ? v.locale === data.locale : v.locale === data.defaultLocale)?.value ?? '' + const localeToUse = hasCurrentLocale ? data.locale : data.defaultLocale + return data.values.find((v) => v.locale === localeToUse)?.value ?? ''
24-43: Simplify type parameter in getWeightLocalizedUnit function.The type parameter
<WeightUnitLiteral = string & object>seems unnecessarily complex.-export function getWeightLocalizedUnit<WeightUnitLiteral = string & object>(unit?: WeightUnit | WeightUnitLiteral): string { +export function getWeightLocalizedUnit(unit?: WeightUnit | string): string {
162-217: Consider moving icon lists to a separate file.The category icons list is quite extensive. Consider moving it to a separate file to improve maintainability.
You could create a new file like
iconSelectors.tsand import the functions from there to keep this file more focused on core helpers.apps/essence/server/services/db/repository/channel.ts (1)
13-15: Implement pagination for channel listingThe new
list()method retrieves all records from the channels table without any pagination. While this works for small datasets, it could lead to performance issues as the number of channels grows.Consider implementing pagination to make the method more scalable:
- static async list() { - return useDatabase().query.channels.findMany() + static async list(options?: { limit?: number; offset?: number }) { + return useDatabase().query.channels.findMany({ + limit: options?.limit, + offset: options?.offset, + })This would allow API consumers to request a specific page of results, improving performance for large datasets.
apps/essence/app/components/modal/CreateChannel.vue (2)
9-17: Missing explicit import for useOverlay composableThe
useOverlaycomposable is used but not explicitly imported. While auto-imports might handle this, explicit imports improve code readability and maintainability.<script setup lang="ts"> +import { useOverlay } from '~/composables/useOverlay' // adjust path as needed const overlay = useOverlay() function closeAll() { for (const o of overlay.overlays) { overlay.close(o.id) } } </script>
12-16: Consider more targeted overlay closingThe
closeAllfunction closes all active overlays in the system. Consider whether it's more appropriate to close just this modal rather than all overlays, which could affect unrelated UI elements.function closeAll() { - for (const o of overlay.overlays) { - overlay.close(o.id) - } + // Close only the current modal + overlay.close() }apps/essence/server/api/option/index.get.ts (1)
5-13: Improve error message specificityThe error message "Not found" is generic. Consider providing more context about what specifically wasn't found to help with debugging.
if (!options?.length || !options[0]) { throw createError({ statusCode: 404, - message: 'Not found', + message: 'No application options found', }) }apps/essence/app/layouts/default.vue (2)
36-38: Missing explicit import for useApp composableThe
useAppcomposable is used but not explicitly imported. While auto-imports might handle this, explicit imports improve code readability and maintainability.<script setup lang="ts"> +import { useApp } from '~/composables/useApp' // adjust path as needed const { isNavbarOpened } = useApp() </script>
13-33: Enhance accessibility for the slideover navigationThe mobile navigation slideover lacks accessibility attributes. Consider adding ARIA attributes to improve the experience for users with assistive technologies.
<USlideover v-model:open="isNavbarOpened" side="left" + aria-label="Navigation menu" + :aria-hidden="!isNavbarOpened" > <template #header> <div class="flex flex-row items-center"> <UButton icon="i-lucide-x" color="neutral" variant="ghost" + aria-label="Close navigation" @click="isNavbarOpened = false" /> <NavigationHeader /> </div> </template> <template #body> <Navigation /> </template> </USlideover>apps/essence/app/pages/welcome/index.vue (3)
3-3: Consider setting a minimum particle count for smaller screens.The confetti animation uses a fixed particle count of 200 regardless of screen size. On smaller devices, this might affect performance.
- <div v-confetti="{ stageHeight: height, stageWidth: width, particleCount: 200, duration: 5000 }" /> + <div v-confetti="{ stageHeight: height, stageWidth: width, particleCount: width < 640 ? 100 : 200, duration: 5000 }" />
14-32: Add accessibility attributes to the form container.The form container lacks accessibility attributes like
roleandaria-labelledbywhich would improve screen reader navigation.- <div class="mt-12 px-6 py-6 max-w-sm mx-auto bg-elevated/50 ring ring-default rounded-xl"> + <div + class="mt-12 px-6 py-6 max-w-sm mx-auto bg-elevated/50 ring ring-default rounded-xl" + role="region" + aria-labelledby="welcome-title" + >And update the h1 element:
- <h1 class="mb-2 text-4xl font-semibold text-center"> + <h1 id="welcome-title" class="mb-2 text-4xl font-semibold text-center">
49-54: Add error handling for the navigation operation.The navigation logic lacks error handling, which could silently fail if there are routing issues.
if (essence.isInitialized) { - await navigateTo('/') + try { + await navigateTo('/') + } catch (error) { + console.error('Navigation failed:', error) + // Consider implementing a fallback or showing a toast notification + } }apps/essence/app/app.config.ts (1)
26-26: Consider breaking down the gradient button styles for better readability.The gradient button variant contains a long string of utility classes, which could be difficult to maintain.
- gradient: 'text-neutral-950 bg-linear-to-br from-lime-300 to-green-400 hover:opacity-90 disabled:bg-none disabled:bg-(--ui-bg-accented) disabled:text-neutral-400 aria-disabled:bg-(--ui-bg-accented) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ui-primary)', + gradient: [ + 'text-neutral-950', + 'bg-linear-to-br from-lime-300 to-green-400', + 'hover:opacity-90', + 'disabled:bg-none disabled:bg-(--ui-bg-accented) disabled:text-neutral-400', + 'aria-disabled:bg-(--ui-bg-accented)', + 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ui-primary)', + ].join(' '),apps/essence/app/components/form/InitMaster.vue (2)
9-10: Password visibility toggle is well implemented but could use aria improvements.The password visibility toggle button has good accessibility features with aria-label and aria-pressed attributes. However, the aria-controls attribute refers to "password" but there is no matching id on the password input field.
To fix this, add an id to the password input:
- v-model="state.password" + v-model="state.password" + id="password" :type="isPasswordVisible ? 'text' : 'password'"
91-91: Validation logic could be improved.The form validation only checks if login and password exist but doesn't verify they meet any quality requirements.
Consider using the validation from UserCreateSchema instead of a custom isFormValid computed property. This would ensure consistency with server-side validation:
-const isFormValid = computed<boolean>(() => !!state.value.login && !!state.value.password) +const isFormValid = computed<boolean>(() => { + try { + UserCreateSchema.parse({ + login: state.value.login, + password: state.value.password + }); + return true; + } catch (error) { + return false; + } +})apps/essence/i18n/locales/en-US.json (1)
17-27: Missing theme switch confirmation messages.The locale file has labels for theme switching but lacks confirmation messages that would be useful for toast notifications after switching themes.
Consider adding confirmation messages for theme switching:
"switch": { "system": "Switch to System Theme", "light": "Switch to Light Mode", "dark": "Switch to Dark Mode" - } + }, + "switched": { + "system": "Switched to System Theme", + "light": "Switched to Light Mode", + "dark": "Switched to Dark Mode" + } },apps/essence/app/pages/sign-in/index.vue (2)
87-92: Error toast uses hardcoded title instead of localized string.The error toast uses a hardcoded "Error" title instead of using a localized string.
Use localized string for the error toast title:
onResponseError: async ({ response }) => { toast.add({ - title: 'Error', + title: t('common.error'), description: response.statusText, }) },This requires adding a new entry to the localization file:
"common": { ... "error": "Error", ... }
76-93: Missing loading state indication during form submission.While the button shows a loading state, there's no visual indication if the form is submitted via Enter key.
Consider adding a loading state indicator for the entire form:
+const isSubmitting = ref(false) const { status, execute: signIn } = await useFetch('/api/auth/sign-in', { method: 'POST', body: state, immediate: false, watch: false, + onRequest() { + isSubmitting.value = true + }, onResponse: async ({ response }) => { + isSubmitting.value = false if (response.ok) { await refreshSession() await navigateTo('/') } }, onResponseError: async ({ response }) => { + isSubmitting.value = false toast.add({ title: 'Error', description: response.statusText, }) }, })Then add a loading overlay to the form:
<form class="space-y-4" @submit.prevent="signIn"> + <div v-if="isSubmitting" class="absolute inset-0 bg-gray-100/50 dark:bg-gray-800/50 flex items-center justify-center z-10"> + <UIcon name="i-lucide-loader-2" class="animate-spin text-primary h-8 w-8" /> + </div> <div class="w-full flex flex-col gap-3">apps/essence/app/components/Navigation.vue (1)
29-34: Enhance channel items with additional information.The channel list items only display the name without any additional information or visual cues that might help distinguish between channels.
Consider enhancing channel items with more information, like an icon or active status:
const channelItems = computed(() => essence.channels.map((c) => { return { label: c.name, to: `/channel/${c.id}`, + icon: 'i-lucide-layout', + trailing: c.active + ? { icon: 'i-lucide-check', color: 'success' } + : { icon: 'i-lucide-x', color: 'danger' }, + description: c.description ? c.description.substring(0, 20) + '...' : '', } }))apps/essence/app/components/UserMenu.vue (2)
34-43: Extract type definition for locale itemsThe
as consttype assertion is repeated throughout the code. Consider extracting the checkbox type to improve code maintainability.+const CheckboxType = 'checkbox' as const + const localeItems = ref(locales.value.map((l) => ({ label: l.name, - type: 'checkbox' as const, + type: CheckboxType, icon: `circle-flags:${l.code}`, onUpdateChecked(checked: boolean) { if (checked) { setLocale(l.code) } }, })))
59-69: Consider refactoring duplicate onUpdateChecked handlersThe pattern for handling checkbox updates is very similar across different checkboxes. Consider extracting a factory function to reduce duplication.
+// Add at the beginning of the script +function createPreferenceHandler(value: string) { + return function onUpdateChecked(checked: boolean) { + if (checked) { + colorMode.preference = value + } + } +} // Then use it in the items definition { label: t('common.color-mode.light'), type: 'checkbox' as const, icon: 'i-lucide-sun-dim', - onUpdateChecked(checked: boolean) { - if (checked) { - colorMode.preference = 'light' - } - }, + onUpdateChecked: createPreferenceHandler('light'), },packages/ui/app/composables/useActionToast.ts (3)
7-18: Consider configurable toast durationThe loading toast has a fixed duration of 2 minutes (120000ms), which might be too long for some operations and too short for others. Consider making the duration configurable.
-function start() { +function start(duration = 120000) { toast.add({ id: id.value, title: t('toast.in-process'), description: t('toast.updating-data'), icon: 'food:loader', - duration: 120000, + duration, ui: { icon: 'animate-spin', }, }) }
20-31: Allow customizing success toast durationThe success toast has a fixed duration of 3 seconds, which might not be appropriate for all messages. Consider making this configurable.
-function success(title: string) { +function success(title: string, duration = 3000) { toast.update(id.value, { title, description: undefined, icon: 'food:check', color: 'success', - duration: 3000, + duration, ui: { icon: '', }, }) }
33-44: Allow customizing error toast titleThe error toast always uses a fixed title from i18n. Consider making this customizable to support different error scenarios.
-function error(description: string = t('error.default')) { +function error(description: string = t('error.default'), customTitle?: string) { toast.update(id.value, { - title: t('error.title'), + title: customTitle || t('error.title'), icon: 'food:close', color: 'error', description, duration: 5000, ui: { icon: '', }, }) }apps/essence/app/components/form/CreateChannel.vue (3)
99-116: Enhance error handling and user feedback.The current error handling only logs errors to the console and shows a generic error toast. Consider providing more specific error messages to the user based on the error type.
async function onSubmit(event: FormSubmitEvent<ChannelCreate>) { actionToast.start() emit('submitted') try { await $fetch('/api/channel', { method: 'POST', body: event.data, }) await essence.update() actionToast.success(t('toast.channel-created')) emit('success') } catch (error) { console.error(error) - actionToast.error() + // Get specific error message if available + const errorMessage = error.response?.data?.message || t('common.error.default') + actionToast.error(errorMessage) } }
90-97: Consider default values for form fields.The initial state values are all undefined, which may lead to validation issues or inconsistent UX. Consider providing default values for fields where it makes sense (e.g., defaultLocale based on user's browser).
const state = ref<Partial<ChannelCreate>>({ name: undefined, description: undefined, - defaultLocale: undefined, + defaultLocale: useI18n().locale.value || 'en-US', currencyCode: undefined, countryCode: undefined, timeZone: undefined, })
78-98: Reset form after successful submission.The form doesn't reset after successful submission. If the user wants to create multiple channels in succession, they would need to manually clear all fields.
Add a function to reset the form and call it after successful submission:
const state = ref<Partial<ChannelCreate>>({ name: undefined, description: undefined, defaultLocale: undefined, currencyCode: undefined, countryCode: undefined, timeZone: undefined, }) +function resetForm() { + state.value = { + name: undefined, + description: undefined, + defaultLocale: undefined, + currencyCode: undefined, + countryCode: undefined, + timeZone: undefined, + } +} async function onSubmit(event: FormSubmitEvent<ChannelCreate>) { // ...existing code try { // ...existing code actionToast.success(t('toast.channel-created')) + resetForm() emit('success') } catch (error) { // ...existing code } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (4)
apps/essence/public/favicon.icois excluded by!**/*.icoapps/essence/public/img/admin-avatar.svgis excluded by!**/*.svgapps/essence/public/img/bg-pattern-1.jpgis excluded by!**/*.jpgpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (34)
apps/essence/app/app.config.ts(1 hunks)apps/essence/app/app.vue(2 hunks)apps/essence/app/components/Navigation.vue(1 hunks)apps/essence/app/components/NavigationHeader.vue(1 hunks)apps/essence/app/components/UserMenu.vue(1 hunks)apps/essence/app/components/form/CreateChannel.vue(1 hunks)apps/essence/app/components/form/InitMaster.vue(1 hunks)apps/essence/app/components/form/InitOption.vue(1 hunks)apps/essence/app/components/modal/CreateChannel.vue(1 hunks)apps/essence/app/composables/useApp.ts(1 hunks)apps/essence/app/error.vue(1 hunks)apps/essence/app/layouts/default.vue(1 hunks)apps/essence/app/layouts/empty.vue(1 hunks)apps/essence/app/pages/index.vue(1 hunks)apps/essence/app/pages/sign-in/index.vue(1 hunks)apps/essence/app/pages/welcome/index.vue(1 hunks)apps/essence/app/utils/helpers.ts(1 hunks)apps/essence/i18n/locales/en-US.json(1 hunks)apps/essence/i18n/locales/ru-RU.json(1 hunks)apps/essence/package.json(1 hunks)apps/essence/public/robots.txt(1 hunks)apps/essence/public/schema.json(1 hunks)apps/essence/server/api/auth/sign-in.post.ts(1 hunks)apps/essence/server/api/option/index.get.ts(1 hunks)apps/essence/server/api/user/master.put.ts(2 hunks)apps/essence/server/services/db/repository/channel.ts(1 hunks)apps/essence/stores/essence.ts(1 hunks)packages/schema/src/services/channel.ts(2 hunks)packages/schema/src/services/option.ts(1 hunks)packages/schema/src/services/user.ts(1 hunks)packages/ui/app/assets/css/main.css(1 hunks)packages/ui/app/composables/useActionToast.ts(1 hunks)packages/ui/i18n/locales/en-US.json(2 hunks)packages/ui/i18n/locales/ru-RU.json(2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (6)
apps/essence/server/services/db/repository/channel.ts (1)
apps/essence/server/services/db/database.ts (1)
useDatabase(22-28)
apps/essence/server/api/user/master.put.ts (1)
packages/schema/src/services/user.ts (1)
UserCreateSchema(8-12)
apps/essence/server/api/auth/sign-in.post.ts (1)
packages/schema/src/services/user.ts (1)
UserSignInSchema(3-6)
apps/essence/stores/essence.ts (3)
packages/schema/src/types/food.ts (1)
LocaleValue(10-10)apps/essence/server/services/db/tables.ts (1)
channels(51-68)apps/essence/server/services/db/repository/channel.ts (2)
Channel(6-39)update(26-34)
apps/essence/server/api/option/index.get.ts (2)
apps/essence/server/services/db/tables.ts (2)
options(6-16)channels(51-68)apps/essence/server/services/db/repository/index.ts (1)
repository(23-23)
apps/essence/app/utils/helpers.ts (2)
packages/schema/src/types/channel.ts (1)
CurrencyCode(6-6)packages/schema/src/types/food.ts (3)
LocaleValue(10-10)Locale(4-4)WeightUnit(13-13)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: build
🔇 Additional comments (39)
packages/ui/app/assets/css/main.css (1)
1-1: Tailwind CSS import simplified appropriately.The simplified import syntax
@import "tailwindcss";aligns with modern Tailwind usage patterns and maintains compatibility with the rest of the styling configuration.apps/essence/public/robots.txt (1)
1-4: Robots.txt configuration follows standard protocol.The robots.txt file correctly follows the standard format with proper directives. Disallowing the
/welcomepath is appropriate if this is an onboarding or intro page that shouldn't be indexed by search engines.apps/essence/app/composables/useApp.ts (1)
1-18: Well-structured composable for navigation state management.The
useAppcomposable provides a clean approach to managing navigation state across the application. UsingcreateSharedComposableensures consistent state between components, and the route watcher automatically closes the navbar on navigation, which improves user experience.packages/ui/i18n/locales/en-US.json (2)
3-3: Translation key added appropriately.The new "to-home" key has been correctly added to the "app" namespace, which is referenced in the error.vue file.
15-22: New namespaces added for toast notifications and error handling.The additions of "toast" and "error" namespaces provide necessary translation support for app-wide notifications and error handling. These namespaces will help maintain consistency across the application.
apps/essence/app/utils/helpers.ts (1)
1-13: Currency sign mapping looks good.The CURRENCY_SIGNS constant provides a clean mapping between currency codes and their respective symbols, which will be helpful for formatting currency values throughout the app.
apps/essence/app/error.vue (1)
10-10: Button styling and translation key updated consistently.The button has been styled with a neutral color and now uses the "app.to-home" translation key that was added to the localization file.
Also applies to: 15-15
apps/essence/app/pages/index.vue (1)
1-3: Homepage contains only placeholder content.The current implementation only displays "123" and lacks proper content structure. This appears to be a placeholder that needs to be replaced with actual homepage content.
Is this intended to be a placeholder for future implementation, or should it be expanded to include actual content for the initial release?
apps/essence/app/layouts/empty.vue (1)
1-3: LGTM! Clean minimal layout implementationThis empty layout provides a clean container for pages that don't need navigation or other UI elements, perfect for onboarding and welcome screens.
packages/schema/src/services/option.ts (1)
14-14: LGTM! Good type definition approachExporting a type derived from the schema's inferred type ensures consistency between validation and usage throughout the application.
apps/essence/server/api/auth/sign-in.post.ts (1)
2-2: LGTM! Improved schema naming consistencyThe update from
UserSignIntoUserSignInSchemaimproves naming consistency across the codebase, making it clearer that this is a validation schema.Also applies to: 9-9
apps/essence/server/api/user/master.put.ts (1)
1-1: Schema naming convention improvementThe update from
UserCreatetoUserCreateSchemaimproves naming consistency by clearly indicating this is a validation schema. This aligns well with the schema definition inpackages/schema/src/services/user.ts.Also applies to: 18-18
apps/essence/package.json (1)
20-20: Good addition of UI enhancement packageAdding the
@neoconfetti/vuepackage is appropriate for enhancing the user experience with confetti animations, likely for celebratory moments such as on the welcome page or after completing important actions.packages/schema/src/services/channel.ts (2)
14-14: Improved type safety with inferred typesGood addition of the
ChannelCreatetype that leverages TypeScript's type inference. This provides strong typing for channel creation operations throughout the application, improving developer experience with IDE autocompletion and catching type errors at compile time.
28-28: Improved type safety with inferred typesExcellent addition of the
ChannelUpdatetype that maintains consistency between runtime validation and compile-time typing. This follows the DRY principle by deriving types from validation schemas rather than creating separate type definitions.apps/essence/app/components/NavigationHeader.vue (1)
1-7: Well-structured navigation header componentThis is a clean, simple component that properly uses internationalization for the app title. The component follows good practices with semantic HTML and appropriate spacing/styling classes.
apps/essence/app/app.vue (2)
7-7: UI consistency improvementChanged from CSS variable syntax
bg-(--ui-primary)to the direct utility classbg-primary, which is more consistent with standard Tailwind/UI framework conventions.
29-39: Well-implemented app initialization flowThis addition creates a robust initialization flow that:
- Updates the essence store on app startup
- Redirects users to the welcome page if the app isn't initialized
- Ensures users complete the onboarding process before accessing other app features
The async/await handling is also implemented correctly.
packages/schema/src/services/user.ts (1)
3-3: Schema naming convention standardization - Good improvement!Excellent job renaming the schema constants to include the "Schema" suffix and adding the type inference. This makes the schema naming consistent across files and improves type safety by separating the schema definition from the inferred TypeScript type.
Also applies to: 8-8, 13-13
apps/essence/server/api/option/index.get.ts (1)
18-22: Well-structured response objectThe response object neatly combines the option data with additional properties. This approach provides a comprehensive dataset for frontend components in a single request.
apps/essence/app/layouts/default.vue (1)
2-11: Well-structured responsive layoutThe layout effectively handles both desktop and mobile views with a clean separation of concerns. The use of Tailwind's responsive classes and logical component hierarchy creates a maintainable structure.
packages/ui/i18n/locales/ru-RU.json (3)
3-3: LGTM! Appropriate translation added for the home navigation link.The Russian translation "На главную" for the "to-home" key is accurate and aligns with the application's navigation functionality.
15-18: LGTM! Toast notification translations look good.The Russian translations for toast notifications are properly structured and provide clear messaging for the user.
19-22: LGTM! Error message translations are appropriate.The error message translations are concise and user-friendly, following good practices for error handling in the UI.
apps/essence/app/app.config.ts (5)
3-8: LGTM! Clean color schema implementation.The color scheme is well-defined and follows a consistent pattern, using Tailwind's color naming conventions for easy integration with the UI framework.
9-11: LGTM! Custom loader icon defined.Setting "food:loader" as the loading icon aligns well with the application's domain.
21-24: LGTM! Extra-large button size variant added.The XL size variant with appropriate padding will provide better touch targets for important actions.
30-63: LGTM! Consistent input component sizing across variants.The XL size variants for input, inputNumber, select, and selectMenu components maintain consistent padding (px-4 py-3), ensuring a uniform UI experience.
68-70: LGTM! Simplified tab styling with utility classes.The gradient variant for tabs now uses utility classes, making the styling more maintainable and consistent with the rest of the UI.
apps/essence/i18n/locales/ru-RU.json (5)
3-3: LGTM! Short name added for the Russian language.The short name "Рус" is appropriate for UI elements where space is limited.
4-56: LGTM! Comprehensive common UI translations.The common namespace contains all necessary translations for general UI elements, including language settings, color modes, and geographic/currency information.
57-63: LGTM! Well-structured onboarding translations.The initialization translations provide clear guidance through the onboarding process with appropriate Russian phrasing.
64-98: LGTM! Complete application UI translations.The app namespace provides comprehensive translations for all navigation elements, menus, and application-specific UI components.
99-136: LGTM! Extensive toast notification messages.The toast namespace contains a complete set of success messages for all CRUD operations in the application, ensuring users receive appropriate feedback in Russian.
apps/essence/app/components/Navigation.vue (1)
63-70:Details
❓ Verification inconclusive
Technical support link could use verification.
The technical support link points to a GitHub repository, but there's no verification that this is the correct support channel.
Verify that this is the intended support channel:
🏁 Script executed:
#!/bin/bash # Check if there's a SUPPORT.md file in the repository that might contain official support information fd -t f -e md "SUPPORT" -x cat {} # Check for contact information in the README fd -t f "README.md" -x grep -i -A 5 "support|contact|help" {}Length of output: 99
Support link needs confirmation
- Location:
apps/essence/app/components/Navigation.vue(lines 63–70)- I didn’t find a SUPPORT.md or any “support/contact” section in the repo.
- Please confirm whether
https://github.com/nextorders/foodis the official technical support channel, or update thisto:URL to the correct support endpoint.apps/essence/stores/essence.ts (1)
3-10: Good Pinia store structure with computed stateThe store is well-structured with clear separation of state properties and a computed property to determine initialization status.
apps/essence/app/components/UserMenu.vue (1)
1-19: Well-structured dropdown menu UIThe dropdown menu is well-structured with clear organization and good UI customization.
apps/essence/app/components/form/InitOption.vue (1)
1-85: Well-structured form with good validationThe form structure is clear and well-organized, with appropriate field validation and a good use of UI components.
packages/ui/app/composables/useActionToast.ts (1)
54-54: Good use of createSharedComposable patternUsing
createSharedComposableis a great approach for this utility, ensuring consistent toast behavior across the application.
| import type { CurrencyCode, Locale, LocaleValue, WeightUnit } from '@nextorders/schema' | ||
|
|
||
| export const CURRENCY_SIGNS: Record<CurrencyCode, string> = { | ||
| RUB: '₽', | ||
| USD: '$', | ||
| EUR: '€', | ||
| GEL: '₾', | ||
| BYN: 'Br', | ||
| UAH: '₴', | ||
| KZT: '₸', | ||
| PLN: 'zł', | ||
| TRY: '₺', | ||
| } | ||
|
|
||
| export function getLocaleValue(data: { values?: LocaleValue[], locale: Locale, defaultLocale?: Locale }): string { | ||
| if (!data.values || !Array.isArray(data.values)) { | ||
| return '' | ||
| } | ||
|
|
||
| const hasCurrentLocale = data.values.some((n) => n.locale === data.locale) | ||
| return data.values.find((v) => hasCurrentLocale ? v.locale === data.locale : v.locale === data.defaultLocale)?.value ?? '' | ||
| } | ||
|
|
||
| export function getWeightLocalizedUnit<WeightUnitLiteral = string & object>(unit?: WeightUnit | WeightUnitLiteral): string { | ||
| const { t } = useI18n() | ||
|
|
||
| switch (unit as WeightUnit) { | ||
| case 'G': | ||
| return t('common.abbreviation.g') | ||
| case 'KG': | ||
| return t('common.abbreviation.kg') | ||
| case 'ML': | ||
| return t('common.abbreviation.ml') | ||
| case 'L': | ||
| return t('common.abbreviation.l') | ||
| case 'LB': | ||
| return t('common.abbreviation.lb') | ||
| case 'OZ': | ||
| return t('common.abbreviation.oz') | ||
| default: | ||
| return '' | ||
| } | ||
| } | ||
|
|
||
| export function getLocalizedLanguageCodesForSelect(): { value: string, label: string }[] { | ||
| return [ | ||
| { value: 'en', label: 'EN - English' }, | ||
| { value: 'ru', label: 'RU - Русский' }, | ||
| ] | ||
| } | ||
|
|
||
| export function getLocalizedCountryCodesForSelect(): { value: string, label: string }[] { | ||
| const { t } = useI18n() | ||
|
|
||
| return [ | ||
| { value: 'BY', label: `BY - ${t('common.country.by')}` }, | ||
| { value: 'DE', label: `DE - ${t('common.country.de')}` }, | ||
| { value: 'ES', label: `ES - ${t('common.country.es')}` }, | ||
| { value: 'FR', label: `FR - ${t('common.country.fr')}` }, | ||
| { value: 'GB', label: `GB - ${t('common.country.gb')}` }, | ||
| { value: 'GE', label: `GE - ${t('common.country.ge')}` }, | ||
| { value: 'GR', label: `GR - ${t('common.country.gr')}` }, | ||
| { value: 'IT', label: `IT - ${t('common.country.it')}` }, | ||
| { value: 'KZ', label: `KZ - ${t('common.country.kz')}` }, | ||
| { value: 'PL', label: `PL - ${t('common.country.pl')}` }, | ||
| { value: 'RU', label: `RU - ${t('common.country.ru')}` }, | ||
| { value: 'TR', label: `TR - ${t('common.country.tr')}` }, | ||
| { value: 'UA', label: `UA - ${t('common.country.ua')}` }, | ||
| { value: 'US', label: `US - ${t('common.country.us')}` }, | ||
| ] | ||
| } | ||
|
|
||
| export function getLocalizedCurrencyCodesForSelect(): { value: string, label: string }[] { | ||
| const { t } = useI18n() | ||
|
|
||
| return [ | ||
| { value: 'EUR', label: `EUR - ${t('common.currency.eur')}` }, | ||
| { value: 'BYN', label: `BYN - ${t('common.currency.byn')}` }, | ||
| { value: 'GEL', label: `GEL - ${t('common.currency.gel')}` }, | ||
| { value: 'KZT', label: `KZT - ${t('common.currency.kzt')}` }, | ||
| { value: 'PLN', label: `PLN - ${t('common.currency.pln')}` }, | ||
| { value: 'RUB', label: `RUB - ${t('common.currency.rub')}` }, | ||
| { value: 'UAH', label: `UAH - ${t('common.currency.uah')}` }, | ||
| { value: 'USD', label: `USD - ${t('common.currency.usd')}` }, | ||
| ] | ||
| } | ||
|
|
||
| export function getLocalizedWeightUnitsForSelect(): { value: WeightUnit, label: string }[] { | ||
| const { t } = useI18n() | ||
|
|
||
| return [ | ||
| { value: 'KG', label: t('common.weight-unit.kg') }, | ||
| { value: 'G', label: t('common.weight-unit.g') }, | ||
| { value: 'L', label: t('common.weight-unit.l') }, | ||
| { value: 'ML', label: t('common.weight-unit.ml') }, | ||
| { value: 'OZ', label: t('common.weight-unit.oz') }, | ||
| { value: 'LB', label: t('common.weight-unit.lb') }, | ||
| ] | ||
| } | ||
|
|
||
| export function getLocalizedTimezonesForSelect(): { value: string, label: string }[] { | ||
| return [ | ||
| { value: '-12:00', label: '-12:00' }, | ||
| { value: '-11:00', label: '-11:00' }, | ||
| { value: '-10:00', label: '-10:00' }, | ||
| { value: '-09:00', label: '-09:00' }, | ||
| { value: '-08:00', label: '-08:00' }, | ||
| { value: '-07:00', label: '-07:00' }, | ||
| { value: '-06:00', label: '-06:00' }, | ||
| { value: '-05:00', label: '-05:00' }, | ||
| { value: '-04:00', label: '-04:00' }, | ||
| { value: '-03:00', label: '-03:00' }, | ||
| { value: '-02:00', label: '-02:00' }, | ||
| { value: '-01:00', label: '-01:00' }, | ||
| { value: '+00:00', label: '+00:00' }, | ||
| { value: '+01:00', label: '+01:00' }, | ||
| { value: '+02:00', label: '+02:00' }, | ||
| { value: '+03:00', label: '+03:00' }, | ||
| { value: '+04:00', label: '+04:00' }, | ||
| { value: '+05:00', label: '+05:00' }, | ||
| { value: '+06:00', label: '+06:00' }, | ||
| { value: '+07:00', label: '+07:00' }, | ||
| { value: '+08:00', label: '+08:00' }, | ||
| { value: '+09:00', label: '+09:00' }, | ||
| { value: '+10:00', label: '+10:00' }, | ||
| { value: '+11:00', label: '+11:00' }, | ||
| { value: '+12:00', label: '+12:00' }, | ||
| { value: '+13:00', label: '+13:00' }, | ||
| { value: '+14:00', label: '+14:00' }, | ||
| ] | ||
| } | ||
|
|
||
| export function getLinkIconsForSelect(): { value: string, label: string, icon?: string, type?: 'separator' }[] { | ||
| return [ | ||
| { value: 'i-lucide-smartphone', icon: 'i-lucide-smartphone', label: 'Phone' }, | ||
| { value: 'i-lucide-gift', icon: 'i-lucide-gift', label: 'Gift' }, | ||
| { value: 'i-lucide-info', icon: 'i-lucide-info', label: 'Info' }, | ||
| { type: 'separator', label: '', value: '' }, | ||
| { value: 'simple-icons:bluesky', icon: 'simple-icons:bluesky', label: 'Bluesky' }, | ||
| { value: 'simple-icons:facebook', icon: 'simple-icons:facebook', label: 'Facebook' }, | ||
| { value: 'simple-icons:github', icon: 'simple-icons:github', label: 'GitHub' }, | ||
| { value: 'simple-icons:instagram', icon: 'simple-icons:instagram', label: 'Instagram' }, | ||
| { value: 'simple-icons:odnoklassniki', icon: 'simple-icons:odnoklassniki', label: 'Odnoklassniki' }, | ||
| { value: 'simple-icons:telegram', icon: 'simple-icons:telegram', label: 'Telegram' }, | ||
| { value: 'simple-icons:viber', icon: 'simple-icons:viber', label: 'Viber' }, | ||
| { value: 'simple-icons:vk', icon: 'simple-icons:vk', label: 'VK' }, | ||
| { value: 'simple-icons:whatsapp', icon: 'simple-icons:whatsapp', label: 'WhatsApp' }, | ||
| { value: 'simple-icons:x', icon: 'simple-icons:x', label: 'X' }, | ||
| { value: 'simple-icons:youtube', icon: 'simple-icons:youtube', label: 'YouTube' }, | ||
| ] | ||
| } | ||
|
|
||
| export function getTargetVariantsForSelect(): { value: string, label: string }[] { | ||
| const { t } = useI18n() | ||
|
|
||
| return [ | ||
| { value: '_self', label: t('common.no') }, | ||
| { value: '_blank', label: t('common.yes') }, | ||
| ] | ||
| } | ||
|
|
||
| export function getCategoryIconsForSelect(): { value: string, label: string, icon?: string }[] { | ||
| return [ | ||
| { value: '', icon: '', label: 'No icon' }, | ||
| { value: 'fluent-emoji-flat:avocado', icon: 'fluent-emoji-flat:avocado', label: 'Avocado' }, | ||
| { value: 'fluent-emoji-flat:bacon', icon: 'fluent-emoji-flat:bacon', label: 'Bacon' }, | ||
| { value: 'fluent-emoji-flat:bagel', icon: 'fluent-emoji-flat:bagel', label: 'Bagel' }, | ||
| { value: 'fluent-emoji-flat:banana', icon: 'fluent-emoji-flat:banana', label: 'Banana' }, | ||
| { value: 'fluent-emoji-flat:baguette-bread', icon: 'fluent-emoji-flat:baguette-bread', label: 'Baguette' }, | ||
| { value: 'fluent-emoji-flat:beer-mug', icon: 'fluent-emoji-flat:beer-mug', label: 'Beer' }, | ||
| { value: 'fluent-emoji-flat:bento-box', icon: 'fluent-emoji-flat:bento-box', label: 'Bento' }, | ||
| { value: 'fluent-emoji-flat:beverage-box', icon: 'fluent-emoji-flat:beverage-box', label: 'Beverage' }, | ||
| { value: 'fluent-emoji-flat:birthday-cake', icon: 'fluent-emoji-flat:birthday-cake', label: 'Birthday Cake' }, | ||
| { value: 'fluent-emoji-flat:bread', icon: 'fluent-emoji-flat:bread', label: 'Bread' }, | ||
| { value: 'fluent-emoji-flat:broccoli', icon: 'fluent-emoji-flat:broccoli', label: 'Broccoli' }, | ||
| { value: 'fluent-emoji-flat:brown-mushroom', icon: 'fluent-emoji-flat:brown-mushroom', label: 'Brown Mushroom' }, | ||
| { value: 'fluent-emoji-flat:bubble-tea', icon: 'fluent-emoji-flat:bubble-tea', label: 'Bubble Tea' }, | ||
| { value: 'fluent-emoji-flat:burrito', icon: 'fluent-emoji-flat:burrito', label: 'Burrito' }, | ||
| { value: 'fluent-emoji-flat:candy', icon: 'fluent-emoji-flat:candy', label: 'Candy' }, | ||
| { value: 'fluent-emoji-flat:carrot', icon: 'fluent-emoji-flat:carrot', label: 'Carrot' }, | ||
| { value: 'fluent-emoji-flat:cheese-wedge', icon: 'fluent-emoji-flat:cheese-wedge', label: 'Cheese' }, | ||
| { value: 'fluent-emoji-flat:chocolate-bar', icon: 'fluent-emoji-flat:chocolate-bar', label: 'Chocolate' }, | ||
| { value: 'fluent-emoji-flat:shortcake', icon: 'fluent-emoji-flat:shortcake', label: 'Cake' }, | ||
| { value: 'fluent-emoji-flat:cocktail-glass', icon: 'fluent-emoji-flat:cocktail-glass', label: 'Cocktail' }, | ||
| { value: 'fluent-emoji-flat:cooked-rice', icon: 'fluent-emoji-flat:cooked-rice', label: 'Cooked Rice' }, | ||
| { value: 'fluent-emoji-flat:cookie', icon: 'fluent-emoji-flat:cookie', label: 'Cookie' }, | ||
| { value: 'fluent-emoji-flat:cooking', icon: 'fluent-emoji-flat:cooking', label: 'Cooking' }, | ||
| { value: 'fluent-emoji-flat:crab', icon: 'fluent-emoji-flat:crab', label: 'Crab' }, | ||
| { value: 'fluent-emoji-flat:croissant', icon: 'fluent-emoji-flat:croissant', label: 'Croissant' }, | ||
| { value: 'fluent-emoji-flat:cupcake', icon: 'fluent-emoji-flat:cupcake', label: 'Cupcake' }, | ||
| { value: 'fluent-emoji-flat:curry-rice', icon: 'fluent-emoji-flat:curry-rice', label: 'Curry' }, | ||
| { value: 'fluent-emoji-flat:doughnut', icon: 'fluent-emoji-flat:doughnut', label: 'Doughnut' }, | ||
| { value: 'fluent-emoji-flat:fork-and-knife', icon: 'fluent-emoji-flat:fork-and-knife', label: 'Fork' }, | ||
| { value: 'fluent-emoji-flat:french-fries', icon: 'fluent-emoji-flat:french-fries', label: 'French Fries' }, | ||
| { value: 'fluent-emoji-flat:fried-shrimp', icon: 'fluent-emoji-flat:fried-shrimp', label: 'Fried Shrimp' }, | ||
| { value: 'fluent-emoji-flat:hamburger', icon: 'fluent-emoji-flat:hamburger', label: 'Hamburger' }, | ||
| { value: 'fluent-emoji-flat:hot-beverage', icon: 'fluent-emoji-flat:hot-beverage', label: 'Hot Beverage' }, | ||
| { value: 'fluent-emoji-flat:hot-dog', icon: 'fluent-emoji-flat:hot-dog', label: 'Hot Dog' }, | ||
| { value: 'fluent-emoji-flat:hot-pepper', icon: 'fluent-emoji-flat:hot-pepper', label: 'Hot Pepper' }, | ||
| { value: 'fluent-emoji-flat:ice-cream', icon: 'fluent-emoji-flat:ice-cream', label: 'Ice Cream' }, | ||
| { value: 'fluent-emoji-flat:pancakes', icon: 'fluent-emoji-flat:pancakes', label: 'Pancake' }, | ||
| { value: 'fluent-emoji-flat:pizza', icon: 'fluent-emoji-flat:pizza', label: 'Pizza' }, | ||
| { value: 'fluent-emoji-flat:popcorn', icon: 'fluent-emoji-flat:popcorn', label: 'Popcorn' }, | ||
| { value: 'fluent-emoji-flat:pretzel', icon: 'fluent-emoji-flat:pretzel', label: 'Pretzel' }, | ||
| { value: 'fluent-emoji-flat:green-salad', icon: 'fluent-emoji-flat:green-salad', label: 'Salad' }, | ||
| { value: 'fluent-emoji-flat:sandwich', icon: 'fluent-emoji-flat:sandwich', label: 'Sandwich' }, | ||
| { value: 'fluent-emoji-flat:pot-of-food', icon: 'fluent-emoji-flat:pot-of-food', label: 'Soup' }, | ||
| { value: 'fluent-emoji-flat:spaghetti', icon: 'fluent-emoji-flat:spaghetti', label: 'Spaghetti' }, | ||
| { value: 'fluent-emoji-flat:cut-of-meat', icon: 'fluent-emoji-flat:cut-of-meat', label: 'Steak' }, | ||
| { value: 'fluent-emoji-flat:steaming-bowl', icon: 'fluent-emoji-flat:steaming-bowl', label: 'Stew' }, | ||
| { value: 'fluent-emoji-flat:stuffed-flatbread', icon: 'fluent-emoji-flat:stuffed-flatbread', label: 'Stuffed Flatbread' }, | ||
| { value: 'fluent-emoji-flat:sushi', icon: 'fluent-emoji-flat:sushi', label: 'Sushi' }, | ||
| { value: 'fluent-emoji-flat:taco', icon: 'fluent-emoji-flat:taco', label: 'Taco' }, | ||
| { value: 'fluent-emoji-flat:tamale', icon: 'fluent-emoji-flat:tamale', label: 'Tamale' }, | ||
| { value: 'fluent-emoji-flat:teacup-without-handle', icon: 'fluent-emoji-flat:teacup-without-handle', label: 'Tea' }, | ||
| ] | ||
| } |
There was a problem hiding this comment.
Missing import for useI18n.
The useI18n function is used throughout the file but not imported.
Add the following import at the top of the file:
+import { useI18n } from 'vue-i18n'
import type { CurrencyCode, Locale, LocaleValue, WeightUnit } from '@nextorders/schema'📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import type { CurrencyCode, Locale, LocaleValue, WeightUnit } from '@nextorders/schema' | |
| export const CURRENCY_SIGNS: Record<CurrencyCode, string> = { | |
| RUB: '₽', | |
| USD: '$', | |
| EUR: '€', | |
| GEL: '₾', | |
| BYN: 'Br', | |
| UAH: '₴', | |
| KZT: '₸', | |
| PLN: 'zł', | |
| TRY: '₺', | |
| } | |
| export function getLocaleValue(data: { values?: LocaleValue[], locale: Locale, defaultLocale?: Locale }): string { | |
| if (!data.values || !Array.isArray(data.values)) { | |
| return '' | |
| } | |
| const hasCurrentLocale = data.values.some((n) => n.locale === data.locale) | |
| return data.values.find((v) => hasCurrentLocale ? v.locale === data.locale : v.locale === data.defaultLocale)?.value ?? '' | |
| } | |
| export function getWeightLocalizedUnit<WeightUnitLiteral = string & object>(unit?: WeightUnit | WeightUnitLiteral): string { | |
| const { t } = useI18n() | |
| switch (unit as WeightUnit) { | |
| case 'G': | |
| return t('common.abbreviation.g') | |
| case 'KG': | |
| return t('common.abbreviation.kg') | |
| case 'ML': | |
| return t('common.abbreviation.ml') | |
| case 'L': | |
| return t('common.abbreviation.l') | |
| case 'LB': | |
| return t('common.abbreviation.lb') | |
| case 'OZ': | |
| return t('common.abbreviation.oz') | |
| default: | |
| return '' | |
| } | |
| } | |
| export function getLocalizedLanguageCodesForSelect(): { value: string, label: string }[] { | |
| return [ | |
| { value: 'en', label: 'EN - English' }, | |
| { value: 'ru', label: 'RU - Русский' }, | |
| ] | |
| } | |
| export function getLocalizedCountryCodesForSelect(): { value: string, label: string }[] { | |
| const { t } = useI18n() | |
| return [ | |
| { value: 'BY', label: `BY - ${t('common.country.by')}` }, | |
| { value: 'DE', label: `DE - ${t('common.country.de')}` }, | |
| { value: 'ES', label: `ES - ${t('common.country.es')}` }, | |
| { value: 'FR', label: `FR - ${t('common.country.fr')}` }, | |
| { value: 'GB', label: `GB - ${t('common.country.gb')}` }, | |
| { value: 'GE', label: `GE - ${t('common.country.ge')}` }, | |
| { value: 'GR', label: `GR - ${t('common.country.gr')}` }, | |
| { value: 'IT', label: `IT - ${t('common.country.it')}` }, | |
| { value: 'KZ', label: `KZ - ${t('common.country.kz')}` }, | |
| { value: 'PL', label: `PL - ${t('common.country.pl')}` }, | |
| { value: 'RU', label: `RU - ${t('common.country.ru')}` }, | |
| { value: 'TR', label: `TR - ${t('common.country.tr')}` }, | |
| { value: 'UA', label: `UA - ${t('common.country.ua')}` }, | |
| { value: 'US', label: `US - ${t('common.country.us')}` }, | |
| ] | |
| } | |
| export function getLocalizedCurrencyCodesForSelect(): { value: string, label: string }[] { | |
| const { t } = useI18n() | |
| return [ | |
| { value: 'EUR', label: `EUR - ${t('common.currency.eur')}` }, | |
| { value: 'BYN', label: `BYN - ${t('common.currency.byn')}` }, | |
| { value: 'GEL', label: `GEL - ${t('common.currency.gel')}` }, | |
| { value: 'KZT', label: `KZT - ${t('common.currency.kzt')}` }, | |
| { value: 'PLN', label: `PLN - ${t('common.currency.pln')}` }, | |
| { value: 'RUB', label: `RUB - ${t('common.currency.rub')}` }, | |
| { value: 'UAH', label: `UAH - ${t('common.currency.uah')}` }, | |
| { value: 'USD', label: `USD - ${t('common.currency.usd')}` }, | |
| ] | |
| } | |
| export function getLocalizedWeightUnitsForSelect(): { value: WeightUnit, label: string }[] { | |
| const { t } = useI18n() | |
| return [ | |
| { value: 'KG', label: t('common.weight-unit.kg') }, | |
| { value: 'G', label: t('common.weight-unit.g') }, | |
| { value: 'L', label: t('common.weight-unit.l') }, | |
| { value: 'ML', label: t('common.weight-unit.ml') }, | |
| { value: 'OZ', label: t('common.weight-unit.oz') }, | |
| { value: 'LB', label: t('common.weight-unit.lb') }, | |
| ] | |
| } | |
| export function getLocalizedTimezonesForSelect(): { value: string, label: string }[] { | |
| return [ | |
| { value: '-12:00', label: '-12:00' }, | |
| { value: '-11:00', label: '-11:00' }, | |
| { value: '-10:00', label: '-10:00' }, | |
| { value: '-09:00', label: '-09:00' }, | |
| { value: '-08:00', label: '-08:00' }, | |
| { value: '-07:00', label: '-07:00' }, | |
| { value: '-06:00', label: '-06:00' }, | |
| { value: '-05:00', label: '-05:00' }, | |
| { value: '-04:00', label: '-04:00' }, | |
| { value: '-03:00', label: '-03:00' }, | |
| { value: '-02:00', label: '-02:00' }, | |
| { value: '-01:00', label: '-01:00' }, | |
| { value: '+00:00', label: '+00:00' }, | |
| { value: '+01:00', label: '+01:00' }, | |
| { value: '+02:00', label: '+02:00' }, | |
| { value: '+03:00', label: '+03:00' }, | |
| { value: '+04:00', label: '+04:00' }, | |
| { value: '+05:00', label: '+05:00' }, | |
| { value: '+06:00', label: '+06:00' }, | |
| { value: '+07:00', label: '+07:00' }, | |
| { value: '+08:00', label: '+08:00' }, | |
| { value: '+09:00', label: '+09:00' }, | |
| { value: '+10:00', label: '+10:00' }, | |
| { value: '+11:00', label: '+11:00' }, | |
| { value: '+12:00', label: '+12:00' }, | |
| { value: '+13:00', label: '+13:00' }, | |
| { value: '+14:00', label: '+14:00' }, | |
| ] | |
| } | |
| export function getLinkIconsForSelect(): { value: string, label: string, icon?: string, type?: 'separator' }[] { | |
| return [ | |
| { value: 'i-lucide-smartphone', icon: 'i-lucide-smartphone', label: 'Phone' }, | |
| { value: 'i-lucide-gift', icon: 'i-lucide-gift', label: 'Gift' }, | |
| { value: 'i-lucide-info', icon: 'i-lucide-info', label: 'Info' }, | |
| { type: 'separator', label: '', value: '' }, | |
| { value: 'simple-icons:bluesky', icon: 'simple-icons:bluesky', label: 'Bluesky' }, | |
| { value: 'simple-icons:facebook', icon: 'simple-icons:facebook', label: 'Facebook' }, | |
| { value: 'simple-icons:github', icon: 'simple-icons:github', label: 'GitHub' }, | |
| { value: 'simple-icons:instagram', icon: 'simple-icons:instagram', label: 'Instagram' }, | |
| { value: 'simple-icons:odnoklassniki', icon: 'simple-icons:odnoklassniki', label: 'Odnoklassniki' }, | |
| { value: 'simple-icons:telegram', icon: 'simple-icons:telegram', label: 'Telegram' }, | |
| { value: 'simple-icons:viber', icon: 'simple-icons:viber', label: 'Viber' }, | |
| { value: 'simple-icons:vk', icon: 'simple-icons:vk', label: 'VK' }, | |
| { value: 'simple-icons:whatsapp', icon: 'simple-icons:whatsapp', label: 'WhatsApp' }, | |
| { value: 'simple-icons:x', icon: 'simple-icons:x', label: 'X' }, | |
| { value: 'simple-icons:youtube', icon: 'simple-icons:youtube', label: 'YouTube' }, | |
| ] | |
| } | |
| export function getTargetVariantsForSelect(): { value: string, label: string }[] { | |
| const { t } = useI18n() | |
| return [ | |
| { value: '_self', label: t('common.no') }, | |
| { value: '_blank', label: t('common.yes') }, | |
| ] | |
| } | |
| export function getCategoryIconsForSelect(): { value: string, label: string, icon?: string }[] { | |
| return [ | |
| { value: '', icon: '', label: 'No icon' }, | |
| { value: 'fluent-emoji-flat:avocado', icon: 'fluent-emoji-flat:avocado', label: 'Avocado' }, | |
| { value: 'fluent-emoji-flat:bacon', icon: 'fluent-emoji-flat:bacon', label: 'Bacon' }, | |
| { value: 'fluent-emoji-flat:bagel', icon: 'fluent-emoji-flat:bagel', label: 'Bagel' }, | |
| { value: 'fluent-emoji-flat:banana', icon: 'fluent-emoji-flat:banana', label: 'Banana' }, | |
| { value: 'fluent-emoji-flat:baguette-bread', icon: 'fluent-emoji-flat:baguette-bread', label: 'Baguette' }, | |
| { value: 'fluent-emoji-flat:beer-mug', icon: 'fluent-emoji-flat:beer-mug', label: 'Beer' }, | |
| { value: 'fluent-emoji-flat:bento-box', icon: 'fluent-emoji-flat:bento-box', label: 'Bento' }, | |
| { value: 'fluent-emoji-flat:beverage-box', icon: 'fluent-emoji-flat:beverage-box', label: 'Beverage' }, | |
| { value: 'fluent-emoji-flat:birthday-cake', icon: 'fluent-emoji-flat:birthday-cake', label: 'Birthday Cake' }, | |
| { value: 'fluent-emoji-flat:bread', icon: 'fluent-emoji-flat:bread', label: 'Bread' }, | |
| { value: 'fluent-emoji-flat:broccoli', icon: 'fluent-emoji-flat:broccoli', label: 'Broccoli' }, | |
| { value: 'fluent-emoji-flat:brown-mushroom', icon: 'fluent-emoji-flat:brown-mushroom', label: 'Brown Mushroom' }, | |
| { value: 'fluent-emoji-flat:bubble-tea', icon: 'fluent-emoji-flat:bubble-tea', label: 'Bubble Tea' }, | |
| { value: 'fluent-emoji-flat:burrito', icon: 'fluent-emoji-flat:burrito', label: 'Burrito' }, | |
| { value: 'fluent-emoji-flat:candy', icon: 'fluent-emoji-flat:candy', label: 'Candy' }, | |
| { value: 'fluent-emoji-flat:carrot', icon: 'fluent-emoji-flat:carrot', label: 'Carrot' }, | |
| { value: 'fluent-emoji-flat:cheese-wedge', icon: 'fluent-emoji-flat:cheese-wedge', label: 'Cheese' }, | |
| { value: 'fluent-emoji-flat:chocolate-bar', icon: 'fluent-emoji-flat:chocolate-bar', label: 'Chocolate' }, | |
| { value: 'fluent-emoji-flat:shortcake', icon: 'fluent-emoji-flat:shortcake', label: 'Cake' }, | |
| { value: 'fluent-emoji-flat:cocktail-glass', icon: 'fluent-emoji-flat:cocktail-glass', label: 'Cocktail' }, | |
| { value: 'fluent-emoji-flat:cooked-rice', icon: 'fluent-emoji-flat:cooked-rice', label: 'Cooked Rice' }, | |
| { value: 'fluent-emoji-flat:cookie', icon: 'fluent-emoji-flat:cookie', label: 'Cookie' }, | |
| { value: 'fluent-emoji-flat:cooking', icon: 'fluent-emoji-flat:cooking', label: 'Cooking' }, | |
| { value: 'fluent-emoji-flat:crab', icon: 'fluent-emoji-flat:crab', label: 'Crab' }, | |
| { value: 'fluent-emoji-flat:croissant', icon: 'fluent-emoji-flat:croissant', label: 'Croissant' }, | |
| { value: 'fluent-emoji-flat:cupcake', icon: 'fluent-emoji-flat:cupcake', label: 'Cupcake' }, | |
| { value: 'fluent-emoji-flat:curry-rice', icon: 'fluent-emoji-flat:curry-rice', label: 'Curry' }, | |
| { value: 'fluent-emoji-flat:doughnut', icon: 'fluent-emoji-flat:doughnut', label: 'Doughnut' }, | |
| { value: 'fluent-emoji-flat:fork-and-knife', icon: 'fluent-emoji-flat:fork-and-knife', label: 'Fork' }, | |
| { value: 'fluent-emoji-flat:french-fries', icon: 'fluent-emoji-flat:french-fries', label: 'French Fries' }, | |
| { value: 'fluent-emoji-flat:fried-shrimp', icon: 'fluent-emoji-flat:fried-shrimp', label: 'Fried Shrimp' }, | |
| { value: 'fluent-emoji-flat:hamburger', icon: 'fluent-emoji-flat:hamburger', label: 'Hamburger' }, | |
| { value: 'fluent-emoji-flat:hot-beverage', icon: 'fluent-emoji-flat:hot-beverage', label: 'Hot Beverage' }, | |
| { value: 'fluent-emoji-flat:hot-dog', icon: 'fluent-emoji-flat:hot-dog', label: 'Hot Dog' }, | |
| { value: 'fluent-emoji-flat:hot-pepper', icon: 'fluent-emoji-flat:hot-pepper', label: 'Hot Pepper' }, | |
| { value: 'fluent-emoji-flat:ice-cream', icon: 'fluent-emoji-flat:ice-cream', label: 'Ice Cream' }, | |
| { value: 'fluent-emoji-flat:pancakes', icon: 'fluent-emoji-flat:pancakes', label: 'Pancake' }, | |
| { value: 'fluent-emoji-flat:pizza', icon: 'fluent-emoji-flat:pizza', label: 'Pizza' }, | |
| { value: 'fluent-emoji-flat:popcorn', icon: 'fluent-emoji-flat:popcorn', label: 'Popcorn' }, | |
| { value: 'fluent-emoji-flat:pretzel', icon: 'fluent-emoji-flat:pretzel', label: 'Pretzel' }, | |
| { value: 'fluent-emoji-flat:green-salad', icon: 'fluent-emoji-flat:green-salad', label: 'Salad' }, | |
| { value: 'fluent-emoji-flat:sandwich', icon: 'fluent-emoji-flat:sandwich', label: 'Sandwich' }, | |
| { value: 'fluent-emoji-flat:pot-of-food', icon: 'fluent-emoji-flat:pot-of-food', label: 'Soup' }, | |
| { value: 'fluent-emoji-flat:spaghetti', icon: 'fluent-emoji-flat:spaghetti', label: 'Spaghetti' }, | |
| { value: 'fluent-emoji-flat:cut-of-meat', icon: 'fluent-emoji-flat:cut-of-meat', label: 'Steak' }, | |
| { value: 'fluent-emoji-flat:steaming-bowl', icon: 'fluent-emoji-flat:steaming-bowl', label: 'Stew' }, | |
| { value: 'fluent-emoji-flat:stuffed-flatbread', icon: 'fluent-emoji-flat:stuffed-flatbread', label: 'Stuffed Flatbread' }, | |
| { value: 'fluent-emoji-flat:sushi', icon: 'fluent-emoji-flat:sushi', label: 'Sushi' }, | |
| { value: 'fluent-emoji-flat:taco', icon: 'fluent-emoji-flat:taco', label: 'Taco' }, | |
| { value: 'fluent-emoji-flat:tamale', icon: 'fluent-emoji-flat:tamale', label: 'Tamale' }, | |
| { value: 'fluent-emoji-flat:teacup-without-handle', icon: 'fluent-emoji-flat:teacup-without-handle', label: 'Tea' }, | |
| ] | |
| } | |
| import { useI18n } from 'vue-i18n' | |
| import type { CurrencyCode, Locale, LocaleValue, WeightUnit } from '@nextorders/schema' | |
| export const CURRENCY_SIGNS: Record<CurrencyCode, string> = { | |
| RUB: '₽', | |
| USD: '$', | |
| EUR: '€', | |
| GEL: '₾', | |
| BYN: 'Br', | |
| UAH: '₴', | |
| KZT: '₸', | |
| PLN: 'zł', | |
| TRY: '₺', | |
| } | |
| ... |
| import { repository } from '~~/server/services/db/repository' | ||
|
|
There was a problem hiding this comment.
Missing import for errorResolver
The code uses errorResolver on line 24 but doesn't import it. Add the missing import statement.
import { repository } from '~~/server/services/db/repository'
+import { errorResolver } from '~~/server/utils/errorResolver' // adjust path as needed📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { repository } from '~~/server/services/db/repository' | |
| import { repository } from '~~/server/services/db/repository' | |
| import { errorResolver } from '~~/server/utils/errorResolver' // adjust path as needed |
|
|
||
| const master = await repository.user.findMaster() | ||
|
|
||
| const channels = await repository.channel.list() |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Consider pagination for channels list
As the channels list grows, fetching all channels at once could impact performance. Consider implementing pagination or limiting the number of channels returned.
- const channels = await repository.channel.list()
+ // Consider pagination or limiting channels
+ const channels = await repository.channel.list({ limit: 50 })Let's check if the list method supports pagination or limiting:
🏁 Script executed:
#!/bin/bash
# Check if channel.list supports pagination or limiting options
rg -A 5 "channel.list" --glob "*.ts"Length of output: 437
Let’s locate where repository.channel comes from and inspect its list signature:
🏁 Script executed:
#!/bin/bash
# Show imports in the API handler to find where `repository` is coming from
echo "=== apps/essence/server/api/option/index.get.ts (lines 1–50) ==="
sed -n '1,50p' apps/essence/server/api/option/index.get.ts
# Find any channel-related repository implementation files
echo "=== channel*.ts files ==="
find . -type f -name "channel*.ts" | sed 's/^/ /'
# Grep for a `list` method inside those files (with context)
echo "=== Inspecting `list` in channel repository implementations ==="
rg -A5 "list\(" --glob "*/channel*.ts" || trueLength of output: 1604
Let’s inspect the channel repository implementation to see if list accepts any pagination options:
🏁 Script executed:
#!/bin/bash
# Display the channel repository to check `list` signature
echo "=== apps/essence/server/services/db/repository/channel.ts ==="
sed -n '1,200p' apps/essence/server/services/db/repository/channel.tsLength of output: 1127
Add pagination support to Channel repository and API handler
The current implementation of repository.channel.list() fetches all channels without limits:
-
In
apps/essence/server/api/option/index.get.ts:- const channels = await repository.channel.list() + // TODO: pass pagination params (e.g. limit/offset) once supported + const channels = await repository.channel.list({ limit: 50, offset: 0 })
-
In
apps/essence/server/services/db/repository/channel.ts, updatelist()to accept and forward pagination options:- static async list() { - return useDatabase().query.channels.findMany() - } + static async list(params: { limit?: number; offset?: number } = {}) { + const { limit, offset } = params + return useDatabase().query.channels.findMany({ + limit, + offset, + }) + }
Steps to complete:
- Confirm that
findManysupportslimitandoffset(Drizzle-ORM). - Add type definitions for the pagination parameters.
- Wire up client-driven pagination (e.g. query params) as needed.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const channels = await repository.channel.list() | |
| // TODO: pass pagination params (e.g. limit/offset) once supported | |
| const channels = await repository.channel.list({ limit: 50, offset: 0 }) |
| } catch (error) { | ||
| console.error(error) | ||
| actionToast.error() |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Error handling could provide more specific feedback.
The current error handling logs to console and shows a generic error toast without providing specific information to the user.
Improve error handling to provide more specific feedback:
} catch (error) {
console.error(error)
- actionToast.error()
+ if (error.response?.data?.message) {
+ actionToast.error(error.response.data.message)
+ } else {
+ actionToast.error(t('toast.general-error'))
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } catch (error) { | |
| console.error(error) | |
| actionToast.error() | |
| } catch (error) { | |
| console.error(error) | |
| if (error.response?.data?.message) { | |
| actionToast.error(error.response.data.message) | |
| } else { | |
| actionToast.error(t('toast.general-error')) | |
| } | |
| } |
| "toast": { | ||
| "payment-method-created": "Payment Method created!", | ||
| "payment-method-updated": "Payment Method updated!", | ||
| "payment-method-deleted": "Payment Method deleted!", | ||
| "receiving-method-updated": "Method updated!", | ||
| "receiver-created": "Receiver created!", | ||
| "receiver-updated": "Receiver updated!", | ||
| "receiver-deleted": "Receiver deleted!", | ||
| "menu-created": "Menu created!", | ||
| "menu-updated": "Menu updated!", | ||
| "channel-created": "Channel created!", | ||
| "channel-updated": "Channel updated!", | ||
| "channel-deleted": "Channel deleted!", | ||
| "category-created": "Category created!", | ||
| "category-updated": "Category updated!", | ||
| "category-deleted": "Category deleted!", | ||
| "product-created": "Product created!", | ||
| "product-updated": "Product updated!", | ||
| "product-deleted": "Product deleted!", | ||
| "variant-created": "Product variant created!", | ||
| "variant-updated": "Product variant updated!", | ||
| "variant-deleted": "Product variant deleted!", | ||
| "link-created": "Link created!", | ||
| "link-updated": "Link updated!", | ||
| "link-deleted": "Link deleted!", | ||
| "page-created": "Page created!", | ||
| "page-updated": "Page updated!", | ||
| "page-deleted": "Page deleted!", | ||
| "warehouse-created": "Warehouse created!", | ||
| "warehouse-updated": "Warehouse updated!", | ||
| "website-configured": "Website configured!", | ||
| "website-updated": "Website updated!", | ||
| "account-configured": "Account configured!", | ||
| "working-day-updated": "Working day updated!", | ||
| "opening-hours-updated": "Opening hours updated!", | ||
| "photo-loaded": "Photo loaded!", | ||
| "options-updated": "Options updated!" | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Missing general error message in the toast section.
There's no general error message that could be used for unspecified errors or fallbacks.
Add a general error message to the toast section:
"toast": {
+ "general-error": "An error occurred. Please try again.",
"payment-method-created": "Payment Method created!",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "toast": { | |
| "payment-method-created": "Payment Method created!", | |
| "payment-method-updated": "Payment Method updated!", | |
| "payment-method-deleted": "Payment Method deleted!", | |
| "receiving-method-updated": "Method updated!", | |
| "receiver-created": "Receiver created!", | |
| "receiver-updated": "Receiver updated!", | |
| "receiver-deleted": "Receiver deleted!", | |
| "menu-created": "Menu created!", | |
| "menu-updated": "Menu updated!", | |
| "channel-created": "Channel created!", | |
| "channel-updated": "Channel updated!", | |
| "channel-deleted": "Channel deleted!", | |
| "category-created": "Category created!", | |
| "category-updated": "Category updated!", | |
| "category-deleted": "Category deleted!", | |
| "product-created": "Product created!", | |
| "product-updated": "Product updated!", | |
| "product-deleted": "Product deleted!", | |
| "variant-created": "Product variant created!", | |
| "variant-updated": "Product variant updated!", | |
| "variant-deleted": "Product variant deleted!", | |
| "link-created": "Link created!", | |
| "link-updated": "Link updated!", | |
| "link-deleted": "Link deleted!", | |
| "page-created": "Page created!", | |
| "page-updated": "Page updated!", | |
| "page-deleted": "Page deleted!", | |
| "warehouse-created": "Warehouse created!", | |
| "warehouse-updated": "Warehouse updated!", | |
| "website-configured": "Website configured!", | |
| "website-updated": "Website updated!", | |
| "account-configured": "Account configured!", | |
| "working-day-updated": "Working day updated!", | |
| "opening-hours-updated": "Opening hours updated!", | |
| "photo-loaded": "Photo loaded!", | |
| "options-updated": "Options updated!" | |
| } | |
| "toast": { | |
| "general-error": "An error occurred. Please try again.", | |
| "payment-method-created": "Payment Method created!", | |
| "payment-method-updated": "Payment Method updated!", | |
| "payment-method-deleted": "Payment Method deleted!", | |
| "receiving-method-updated": "Method updated!", | |
| "receiver-created": "Receiver created!", | |
| "receiver-updated": "Receiver updated!", | |
| "receiver-deleted": "Receiver deleted!", | |
| "menu-created": "Menu created!", | |
| "menu-updated": "Menu updated!", | |
| "channel-created": "Channel created!", | |
| "channel-updated": "Channel updated!", | |
| "channel-deleted": "Channel deleted!", | |
| "category-created": "Category created!", | |
| "category-updated": "Category updated!", | |
| "category-deleted": "Category deleted!", | |
| "product-created": "Product created!", | |
| "product-updated": "Product updated!", | |
| "product-deleted": "Product deleted!", | |
| "variant-created": "Product variant created!", | |
| "variant-updated": "Product variant updated!", | |
| "variant-deleted": "Product variant deleted!", | |
| "link-created": "Link created!", | |
| "link-updated": "Link updated!", | |
| "link-deleted": "Link deleted!", | |
| "page-created": "Page created!", | |
| "page-updated": "Page updated!", | |
| "page-deleted": "Page deleted!", | |
| "warehouse-created": "Warehouse created!", | |
| "warehouse-updated": "Warehouse updated!", | |
| "website-configured": "Website configured!", | |
| "website-updated": "Website updated!", | |
| "account-configured": "Account configured!", | |
| "working-day-updated": "Working day updated!", | |
| "opening-hours-updated": "Opening hours updated!", | |
| "photo-loaded": "Photo loaded!", | |
| "options-updated": "Options updated!" | |
| } |
| } catch (error) { | ||
| console.error(error) | ||
| actionToast.error() | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Enhance error handling in submit function
The error handling in the submit function only logs to console and shows a generic error toast. Consider providing more specific error information to the user.
} catch (error) {
console.error(error)
- actionToast.error()
+ let errorMessage = t('error.default')
+
+ if (error instanceof Error) {
+ console.error('Form submission error:', error.message)
+
+ // Provide more specific error messages based on the error
+ if (error.message.includes('network')) {
+ errorMessage = t('error.network')
+ } else if (error.message.includes('validation')) {
+ errorMessage = t('error.validation')
+ }
+ }
+
+ actionToast.error(errorMessage)
}Committable suggestion skipped: line range outside the PR's diff.
| function _useActionToast() { | ||
| const toast = useToast() | ||
| const { t } = useI18n() | ||
|
|
||
| const id = ref(useId()) | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Potential issue with toast ID reuse
The toast ID is generated once and reused for all toast notifications. This could cause issues if multiple toasts need to be displayed simultaneously or in quick succession.
function _useActionToast() {
const toast = useToast()
const { t } = useI18n()
- const id = ref(useId())
+ // Generate a new ID for each toast operation to support multiple simultaneous toasts
+ function generateId() {
+ return useId()
+ }
+
+ let currentId = ref('')Then update all the functions to use this new approach:
function start() {
+ currentId.value = generateId()
toast.add({
- id: id.value,
+ id: currentId.value,
title: t('toast.in-process'),
// ...And similarly update the other methods. Also update the return value:
return {
- id: id.value,
+ id: currentId.value,
start,
// ...| <template> | ||
| <UForm | ||
| :schema="ChannelCreateSchema" | ||
| :state="state" | ||
| class="flex flex-col gap-3" | ||
| @submit="onSubmit" | ||
| > | ||
| <UFormField :label="$t('common.title')" name="name"> | ||
| <UInput | ||
| v-model="state.name" | ||
| size="xl" | ||
| class="w-full items-center justify-center" | ||
| /> | ||
| </UFormField> | ||
|
|
||
| <UFormField :label="$t('common.description')" name="description"> | ||
| <UInput | ||
| v-model="state.description" | ||
| :placeholder="$t('common.optional')" | ||
| size="xl" | ||
| class="w-full items-center justify-center" | ||
| /> | ||
| </UFormField> | ||
|
|
||
| <UFormField :label="$t('common.language')" name="defaultLocale"> | ||
| <USelect | ||
| v-model="state.defaultLocale" | ||
| :items="getLocalizedLanguageCodesForSelect()" | ||
| :placeholder="$t('common.select')" | ||
| size="xl" | ||
| class="w-full" | ||
| /> | ||
| </UFormField> | ||
|
|
||
| <UFormField :label="$t('app.country')" name="countryCode"> | ||
| <USelect | ||
| v-model="state.countryCode" | ||
| :items="getLocalizedCountryCodesForSelect()" | ||
| :placeholder="$t('common.select')" | ||
| size="xl" | ||
| class="w-full" | ||
| /> | ||
| </UFormField> | ||
|
|
||
| <UFormField :label="$t('app.currency')" name="currencyCode"> | ||
| <USelect | ||
| v-model="state.currencyCode" | ||
| :items="getLocalizedCurrencyCodesForSelect()" | ||
| :placeholder="$t('common.select')" | ||
| size="xl" | ||
| class="w-full" | ||
| /> | ||
| </UFormField> | ||
|
|
||
| <UFormField :label="$t('app.timezone')" name="timeZone"> | ||
| <USelect | ||
| v-model="state.timeZone" | ||
| :items="getLocalizedTimezonesForSelect()" | ||
| :placeholder="$t('common.select')" | ||
| size="xl" | ||
| class="w-full" | ||
| /> | ||
| </UFormField> | ||
|
|
||
| <UButton | ||
| type="submit" | ||
| variant="solid" | ||
| color="primary" | ||
| size="xl" | ||
| block | ||
| class="mt-3" | ||
| > | ||
| {{ $t('app.create.title') }} | ||
| </UButton> | ||
| </UForm> | ||
| </template> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add form validation feedback.
The form uses a schema for validation but there's no visual feedback to help users understand validation errors before submitting.
Consider adding validation feedback by using the built-in validation features of UForm. For example:
<UForm
:schema="ChannelCreateSchema"
:state="state"
class="flex flex-col gap-3"
+ validate-on="submit"
@submit="onSubmit"
>This will show validation errors when users attempt to submit an invalid form, helping them correct issues before sending data to the server.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <template> | |
| <UForm | |
| :schema="ChannelCreateSchema" | |
| :state="state" | |
| class="flex flex-col gap-3" | |
| @submit="onSubmit" | |
| > | |
| <UFormField :label="$t('common.title')" name="name"> | |
| <UInput | |
| v-model="state.name" | |
| size="xl" | |
| class="w-full items-center justify-center" | |
| /> | |
| </UFormField> | |
| <UFormField :label="$t('common.description')" name="description"> | |
| <UInput | |
| v-model="state.description" | |
| :placeholder="$t('common.optional')" | |
| size="xl" | |
| class="w-full items-center justify-center" | |
| /> | |
| </UFormField> | |
| <UFormField :label="$t('common.language')" name="defaultLocale"> | |
| <USelect | |
| v-model="state.defaultLocale" | |
| :items="getLocalizedLanguageCodesForSelect()" | |
| :placeholder="$t('common.select')" | |
| size="xl" | |
| class="w-full" | |
| /> | |
| </UFormField> | |
| <UFormField :label="$t('app.country')" name="countryCode"> | |
| <USelect | |
| v-model="state.countryCode" | |
| :items="getLocalizedCountryCodesForSelect()" | |
| :placeholder="$t('common.select')" | |
| size="xl" | |
| class="w-full" | |
| /> | |
| </UFormField> | |
| <UFormField :label="$t('app.currency')" name="currencyCode"> | |
| <USelect | |
| v-model="state.currencyCode" | |
| :items="getLocalizedCurrencyCodesForSelect()" | |
| :placeholder="$t('common.select')" | |
| size="xl" | |
| class="w-full" | |
| /> | |
| </UFormField> | |
| <UFormField :label="$t('app.timezone')" name="timeZone"> | |
| <USelect | |
| v-model="state.timeZone" | |
| :items="getLocalizedTimezonesForSelect()" | |
| :placeholder="$t('common.select')" | |
| size="xl" | |
| class="w-full" | |
| /> | |
| </UFormField> | |
| <UButton | |
| type="submit" | |
| variant="solid" | |
| color="primary" | |
| size="xl" | |
| block | |
| class="mt-3" | |
| > | |
| {{ $t('app.create.title') }} | |
| </UButton> | |
| </UForm> | |
| </template> | |
| <template> | |
| <UForm | |
| :schema="ChannelCreateSchema" | |
| :state="state" | |
| class="flex flex-col gap-3" | |
| validate-on="submit" | |
| @submit="onSubmit" | |
| > |
| :items="getLocalizedLanguageCodesForSelect()" | ||
| :placeholder="$t('common.select')" | ||
| size="xl" | ||
| class="w-full" | ||
| /> |
There was a problem hiding this comment.
Import missing helper functions.
The component is using getLocalizedLanguageCodesForSelect() but only imports getLocalizedCountryCodesForSelect. You need to import the other helper functions used in the component.
-import { getLocalizedCountryCodesForSelect } from '../../utils/helpers'
+import {
+ getLocalizedCountryCodesForSelect,
+ getLocalizedLanguageCodesForSelect,
+ getLocalizedCurrencyCodesForSelect,
+ getLocalizedTimezonesForSelect
+} from '../../utils/helpers'| <UButton | ||
| type="submit" | ||
| variant="solid" | ||
| color="primary" | ||
| size="xl" | ||
| block | ||
| class="mt-3" | ||
| > | ||
| {{ $t('app.create.title') }} | ||
| </UButton> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add loading state to submit button.
The form submission process is asynchronous, but there's no visual indication to users that the submission is in progress. Add a loading state to prevent multiple submissions and improve user experience.
<UButton
type="submit"
variant="solid"
color="primary"
size="xl"
block
+ :loading="isSubmitting"
class="mt-3"
>
{{ $t('app.create.title') }}
</UButton>And add a reactive state to track submission:
const state = ref<Partial<ChannelCreate>>({
name: undefined,
description: undefined,
defaultLocale: undefined,
currencyCode: undefined,
countryCode: undefined,
timeZone: undefined,
})
+const isSubmitting = ref(false)Update the submission handler:
async function onSubmit(event: FormSubmitEvent<ChannelCreate>) {
+ if (isSubmitting.value) return
+ isSubmitting.value = true
actionToast.start()
emit('submitted')
try {
// ... existing code
} catch (error) {
// ... existing code
} finally {
+ isSubmitting.value = false
}
}
|
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
apps/essence/app/utils/ui.ts (1)
4-19: Consider improving type safety for createValidator.The implementation looks good overall, but I would recommend adding more specific types instead of using
anyfor the schema and state parameters. This would provide better compile-time safety and developer experience.- export function createValidator(schema: any) { + export function createValidator<T>(schema: ReturnType<typeof type>) { const { t } = useI18n() - return function (state: any) { + return function (state: T) { const errors: FormError[] = []
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
apps/essence/app/components/form/InitOption.vue(1 hunks)apps/essence/app/pages/settings/index.vue(1 hunks)apps/essence/app/utils/ui.ts(1 hunks)apps/essence/i18n/locales/en-US.json(1 hunks)apps/essence/i18n/locales/ru-RU.json(1 hunks)packages/schema/src/services/option.ts(1 hunks)packages/schema/src/types/channel.ts(1 hunks)packages/schema/src/types/date.ts(1 hunks)packages/schema/src/types/food.ts(2 hunks)
✅ Files skipped from review due to trivial changes (3)
- apps/essence/app/pages/settings/index.vue
- packages/schema/src/types/food.ts
- packages/schema/src/types/channel.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/schema/src/services/option.ts
- apps/essence/i18n/locales/ru-RU.json
- apps/essence/app/components/form/InitOption.vue
- apps/essence/i18n/locales/en-US.json
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: build
🔇 Additional comments (3)
packages/schema/src/types/date.ts (1)
12-12: The schema enhancement looks good.Adding
.describe('error.not-selected')to the TimeZoneSchema improves error handling consistency with other schemas in the project. This will enable proper error message localization for timezone validation.apps/essence/app/utils/ui.ts (2)
5-5: Verify Composition API usage.Using
useI18n()outside of a Vue component's setup function might cause issues since composables typically rely on a component context. Ensure that this utility works correctly in your application's context.Consider wrapping this in a function that's called within the component context or use a non-composable alternative if needed.
13-13: Robust error message fallback.Good approach using the schema's description for error messages with a fallback to 'error.common'. This ensures that validation errors are always communicated to the user, even if a schema doesn't have a description.


Summary by CodeRabbit
New Features
Enhancements
Bug Fixes
Chores
Documentation