diff --git a/apps/README.md b/apps/README.md new file mode 100644 index 00000000..d3b3166f --- /dev/null +++ b/apps/README.md @@ -0,0 +1,44 @@ +# Core + +> [!NOTE] +> Нere are the main parts of our app + +## ✍️ Usage + +### Frontend + +```sh +cd client +``` + +#### Dev + +```sh +# Install dependencies +pnpm i + +# Development +pnpm run dev +``` + +#### UI Kit + +```sh +pnpm run storybook +``` + +### Backend + +#### Dev + +```sh +cd server +``` + +```sh +# Install dependencies +pnpm i + +# Development +pnpm run start +``` diff --git a/apps/client/.gitignore b/apps/client/.gitignore new file mode 100644 index 00000000..70342655 --- /dev/null +++ b/apps/client/.gitignore @@ -0,0 +1,32 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo + +*storybook.log \ No newline at end of file diff --git a/apps/client/.storybook/-static/logo.png b/apps/client/.storybook/-static/logo.png new file mode 100644 index 00000000..c3823631 Binary files /dev/null and b/apps/client/.storybook/-static/logo.png differ diff --git a/apps/client/.storybook/main.ts b/apps/client/.storybook/main.ts new file mode 100644 index 00000000..b833aaf7 --- /dev/null +++ b/apps/client/.storybook/main.ts @@ -0,0 +1,23 @@ +import type { StorybookConfig } from '@storybook/vue3-vite' + +const config: StorybookConfig = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: [ + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/addon-interactions', + '@storybook/addon-themes', + ], + framework: { + name: '@storybook/vue3-vite', + options: { + docgen: 'vue-component-meta', + }, + }, + viteFinal: async config => ({ + ...config, + ...(await import('unocss/vite')).default, + }), + staticDirs: ['./-static'], +} +export default config diff --git a/apps/client/.storybook/manager-head.html b/apps/client/.storybook/manager-head.html new file mode 100644 index 00000000..fb396b0c --- /dev/null +++ b/apps/client/.storybook/manager-head.html @@ -0,0 +1,5 @@ + diff --git a/apps/client/.storybook/manager.ts b/apps/client/.storybook/manager.ts new file mode 100644 index 00000000..e3d2c70a --- /dev/null +++ b/apps/client/.storybook/manager.ts @@ -0,0 +1,12 @@ +import { addons } from '@storybook/manager-api' +import { themes } from '@storybook/theming' +// @ts-ignore +import logo from './-static/logo.png' + +addons.setConfig({ + theme: { + ...themes.light, + brandImage: logo, + brandUrl: 'https://jenda.vercel.app/', + }, +}) diff --git a/apps/client/.storybook/preview.ts b/apps/client/.storybook/preview.ts new file mode 100644 index 00000000..85d9eecd --- /dev/null +++ b/apps/client/.storybook/preview.ts @@ -0,0 +1,52 @@ +import { withThemeByClassName } from '@storybook/addon-themes' +import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport' +import { themes } from '@storybook/theming' +import { useColorMode } from '@vueuse/core' +import type { Preview } from '@storybook/vue3' + +import 'virtual:uno.css' +import '@unocss/reset/tailwind-compat.css' +import '@/app/styles/primary/index.css' + +const preview: Preview = { + decorators: [ + withThemeByClassName({ + themes: { + light: 'light', + dark: 'dark', + }, + defaultTheme: 'light', + }), + ], + parameters: { + options: { + storySort: (a, b) => + a.id === b.id + ? 0 + : a.id.localeCompare(b.id, undefined, { numeric: true }), + }, + layout: 'centered', + viewport: { + viewports: INITIAL_VIEWPORTS, + defaultViewport: 'desktop', + }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + docs: { + theme: themes.light, + }, + backgrounds: { + disable: true, + }, + }, +} + +useColorMode({ + initialValue: 'light', +}) + +export default preview diff --git a/apps/client/LICENSE b/apps/client/LICENSE new file mode 100644 index 00000000..d4a0c079 --- /dev/null +++ b/apps/client/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Alex Peshkov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/apps/client/README.md b/apps/client/README.md new file mode 100644 index 00000000..cd61ba0a --- /dev/null +++ b/apps/client/README.md @@ -0,0 +1,4 @@ +# @jenda/client + +> [!WARNING] +> 🚧 Client currently on dev mode rn diff --git a/apps/client/components.json b/apps/client/components.json new file mode 100644 index 00000000..2453e3fe --- /dev/null +++ b/apps/client/components.json @@ -0,0 +1,16 @@ +{ + "style": "new-york", + "typescript": true, + "tsConfigPath": "./tsconfig.json", + "tailwind": { + "config": "tailwind.config.js", + "css": "@/app/styles/primary/index.scss", + "baseColor": "neutral", + "cssVariables": false + }, + "framework": "vite", + "aliases": { + "components": "@/shared", + "utils": "@/shared/lib/shadcn/utils" + } +} diff --git a/apps/client/env.d.ts b/apps/client/env.d.ts new file mode 100644 index 00000000..dabd0deb --- /dev/null +++ b/apps/client/env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/apps/client/eslint.config.js b/apps/client/eslint.config.js new file mode 100644 index 00000000..8a44eac7 --- /dev/null +++ b/apps/client/eslint.config.js @@ -0,0 +1,116 @@ +import jendaEslintConfig from '@jenda/eslint-config' +import vitest from '@vitest/eslint-plugin' +import storybook from 'eslint-plugin-storybook' + +export default jendaEslintConfig( + { + vue: true, + typescript: true, + formatters: { + css: true, + }, + }, + { + files: ['**/*.vue'], + rules: { + 'vue/multi-word-component-names': 'warn', + 'vue/block-order': [ + 'error', + { + order: ['script', 'template', 'style'], + }, + ], + 'vue/component-name-in-template-casing': [ + 'error', + 'PascalCase', + { + registeredComponentsOnly: false, + }, + ], + 'vue/html-self-closing': [ + 'error', + { + html: { + void: 'always', + }, + }, + ], + 'vue/max-attributes-per-line': [ + 'error', + { + singleline: { max: 10 }, + multiline: { max: 1 }, + }, + ], + }, + }, + { + files: ['src/pages/**/*.vue'], + rules: { + 'vue/multi-word-component-names': 'off', + }, + }, + { + rules: { + 'import/order': [ + 'error', + { + groups: [ + 'builtin', + ['external', 'internal'], + 'parent', + 'sibling', + 'index', + 'object', + 'type', + ], + pathGroups: [ + { + pattern: 'vue', + group: 'external', + position: 'before', + }, + ], + pathGroupsExcludedImportTypes: ['type'], + }, + ], + 'perfectionist/sort-imports': 'off', + }, + }, + { + files: ['__tests__/*'], + plugins: { + vitest, + }, + rules: { + ...vitest.configs.recommended.rules, + 'vitest/expect-expect': 'off', + 'vitest/valid-expect': 'error', + }, + settings: { + vitest: { + typecheck: true, + }, + }, + languageOptions: { + globals: { + ...vitest.environments.env.globals, + definePage: 'readonly', + }, + }, + }, + { + files: ['**/*.stories.*'], + plugins: { + storybook, + }, + rules: { + ...storybook.configs['flat/recommended'].rules, + }, + }, + { + settings: { + 'import/core-modules': ['vue-router/auto-routes'], + }, + }, +) diff --git a/apps/client/index.html b/apps/client/index.html new file mode 100644 index 00000000..1cd4cb94 --- /dev/null +++ b/apps/client/index.html @@ -0,0 +1,25 @@ + + + + + + + Jenda - cloud program for project and task management + + + + + + + + + + + + + +
+ + + diff --git a/apps/client/package.json b/apps/client/package.json new file mode 100644 index 00000000..e1806af9 --- /dev/null +++ b/apps/client/package.json @@ -0,0 +1,102 @@ +{ + "name": "@jenda/core-client", + "type": "module", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "test:unit": "vitest", + "build-only": "vite build", + "debug:build": "NODE_OPTIONS='--inspect-brk' pnpm run build", + "type-check": "vue-tsc --build --force", + "lint": "eslint . --fix", + "stylelint": "npx stylelint **/*.scss --fix", + "format": "prettier --write src/", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "dependencies": { + "@formkit/auto-animate": "^0.8.2", + "@tanstack/vue-table": "8.20.5", + "@unhead/vue": "^1.11.15", + "@unocss/core": "^65.4.0", + "@unocss/reset": "^65.4.0", + "@vee-validate/zod": "^4.15.0", + "@vitest/eslint-plugin": "^1.1.7", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "axios": "^1.7.2", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "floating-vue": "^5.2.2", + "pinia": "^2.1.7", + "radix-vue": "^1.9.12", + "splitpanes": "^3.1.5", + "tailwind-merge": "^2.5.5", + "universal-cookie": "^6.1.3", + "unocss": "^65.4.0", + "unplugin-vue-router-extend": "0.1.15", + "vee-validate": "^4.15.0", + "vue": "^3.5.8", + "vue-data-ui": "^2.3.44", + "vue-i18n": "^11.0.1", + "vue-router": "^4.3.3", + "vue-sonner": "^1.2.1", + "vue-writer": "^2.0.2", + "vue3-marquee": "^4.2.2", + "vuedraggable": "^4.1.0", + "zod": "^3.24.0" + }, + "devDependencies": { + "@iconify-json/hugeicons": "1.2.1", + "@iconify-json/lucide": "1.2.17", + "@iconify/utils": "2.2.1", + "@iconify/vue": "4.1.2", + "@mnenie/prettier": "^1.0.4", + "@rushstack/eslint-patch": "^1.8.0", + "@storybook/addon-essentials": "^8.1.10", + "@storybook/addon-interactions": "^8.1.10", + "@storybook/addon-links": "^8.1.10", + "@storybook/addon-themes": "^8.4.1", + "@storybook/addon-viewport": "8.4.6", + "@storybook/blocks": "^8.1.10", + "@storybook/manager-api": "8.4.6", + "@storybook/test": "^8.1.10", + "@storybook/theming": "8.4.6", + "@storybook/vue3": "^8.1.10", + "@storybook/vue3-vite": "^8.1.10", + "@tsconfig/node20": "^20.1.4", + "@types/jsdom": "^21.1.7", + "@types/node": "^22.5.2", + "@vitejs/plugin-vue": "^5.0.5", + "@vue/eslint-config-prettier": "^10.0.0", + "@vue/eslint-config-typescript": "^14.1.1", + "@vue/test-utils": "^2.4.6", + "@vue/tsconfig": "^0.5.1", + "eslint": "^9.13.0", + "eslint-define-config": "^2.1.0", + "eslint-plugin-antfu": "^2.7.0", + "eslint-plugin-format": "0.1.3", + "eslint-plugin-storybook": "^0.10.2", + "eslint-plugin-vue": "^9.29.0", + "husky": "^9.0.11", + "jsdom": "^24.1.0", + "lint-staged": "^15.2.7", + "npm-run-all2": "^6.2.0", + "prettier": "^3.2.5", + "sonda": "0.7.0", + "storybook": "^8.1.10", + "stylelint": "^16.6.1", + "stylelint-config-standard-scss": "^13.1.0", + "typescript": "~5.4.0", + "unocss-preset-animations": "1.1.0", + "unocss-preset-shadcn": "0.3.1", + "unplugin-vue-router": "0.10.9", + "vite": "^5.3.1", + "vite-plugin-vue-devtools": "^7.7.0", + "vitest": "^1.6.0", + "vue-tsc": "^2.0.21" + } +} diff --git a/apps/client/public/dev/dev-card-dark.png b/apps/client/public/dev/dev-card-dark.png new file mode 100644 index 00000000..f5292dc6 Binary files /dev/null and b/apps/client/public/dev/dev-card-dark.png differ diff --git a/apps/client/public/dev/dev-card-section-dark.png b/apps/client/public/dev/dev-card-section-dark.png new file mode 100644 index 00000000..2bf77344 Binary files /dev/null and b/apps/client/public/dev/dev-card-section-dark.png differ diff --git a/apps/client/public/dev/dev-card-section.png b/apps/client/public/dev/dev-card-section.png new file mode 100644 index 00000000..9469542d Binary files /dev/null and b/apps/client/public/dev/dev-card-section.png differ diff --git a/apps/client/public/dev/dev-card.png b/apps/client/public/dev/dev-card.png new file mode 100644 index 00000000..4871754d Binary files /dev/null and b/apps/client/public/dev/dev-card.png differ diff --git a/apps/client/public/dev/dev-dark.png b/apps/client/public/dev/dev-dark.png new file mode 100644 index 00000000..59b71128 Binary files /dev/null and b/apps/client/public/dev/dev-dark.png differ diff --git a/apps/client/public/dev/dev.png b/apps/client/public/dev/dev.png new file mode 100644 index 00000000..cf5d443b Binary files /dev/null and b/apps/client/public/dev/dev.png differ diff --git a/apps/client/public/favicon.ico b/apps/client/public/favicon.ico new file mode 100644 index 00000000..b79451b5 Binary files /dev/null and b/apps/client/public/favicon.ico differ diff --git a/apps/client/public/icons/arrow-back.svg b/apps/client/public/icons/arrow-back.svg new file mode 100644 index 00000000..93ed7629 --- /dev/null +++ b/apps/client/public/icons/arrow-back.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/client/public/icons/arrows.svg b/apps/client/public/icons/arrows.svg new file mode 100644 index 00000000..62b1a62a --- /dev/null +++ b/apps/client/public/icons/arrows.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/apps/client/public/icons/blockquote-dark.png b/apps/client/public/icons/blockquote-dark.png new file mode 100644 index 00000000..579ffcc2 Binary files /dev/null and b/apps/client/public/icons/blockquote-dark.png differ diff --git a/apps/client/public/icons/blockquote-light.png b/apps/client/public/icons/blockquote-light.png new file mode 100644 index 00000000..fdf567a8 Binary files /dev/null and b/apps/client/public/icons/blockquote-light.png differ diff --git a/apps/client/public/icons/collaborative.svg b/apps/client/public/icons/collaborative.svg new file mode 100644 index 00000000..cacc60ea --- /dev/null +++ b/apps/client/public/icons/collaborative.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apps/client/public/icons/github-d.png b/apps/client/public/icons/github-d.png new file mode 100644 index 00000000..4bb2db90 Binary files /dev/null and b/apps/client/public/icons/github-d.png differ diff --git a/apps/client/public/icons/github.png b/apps/client/public/icons/github.png new file mode 100644 index 00000000..9490ffc6 Binary files /dev/null and b/apps/client/public/icons/github.png differ diff --git a/apps/client/public/icons/google.png b/apps/client/public/icons/google.png new file mode 100644 index 00000000..6b7ea547 Binary files /dev/null and b/apps/client/public/icons/google.png differ diff --git a/apps/client/public/icons/kanban-dark.png b/apps/client/public/icons/kanban-dark.png new file mode 100644 index 00000000..9613e15c Binary files /dev/null and b/apps/client/public/icons/kanban-dark.png differ diff --git a/apps/client/public/icons/kanban.png b/apps/client/public/icons/kanban.png new file mode 100644 index 00000000..a0b97755 Binary files /dev/null and b/apps/client/public/icons/kanban.png differ diff --git a/apps/client/public/icons/telegram.webp b/apps/client/public/icons/telegram.webp new file mode 100644 index 00000000..f6b2b7c0 Binary files /dev/null and b/apps/client/public/icons/telegram.webp differ diff --git a/apps/client/src/App.vue b/apps/client/src/App.vue new file mode 100644 index 00000000..c0d95e34 --- /dev/null +++ b/apps/client/src/App.vue @@ -0,0 +1,25 @@ + + + diff --git a/apps/client/src/core/__tests__/FooterWelcome.spec.ts b/apps/client/src/core/__tests__/FooterWelcome.spec.ts new file mode 100644 index 00000000..661908ef --- /dev/null +++ b/apps/client/src/core/__tests__/FooterWelcome.spec.ts @@ -0,0 +1,26 @@ +import { shallowMount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import FooterWelcome from '../components/footer/AppFooter.vue' +import i18n from '@/shared/libs/i18n' + +describe('tests for FooterWelcome.vue', () => { + const wrapper = shallowMount(FooterWelcome, { + global: { + plugins: [i18n], + + mocks: { + t: (key: string) => { + const translations: Record = { + 'welcome.footer': 'footer', + } + return translations[key] + }, + }, + }, + }) + + // snapshot is covered needs for i18n I think ;) + it('should be render correctly', () => { + expect(wrapper.html()).toMatchSnapshot() + }) +}) diff --git a/apps/client/src/core/__tests__/HeaderWelcome.spec.ts b/apps/client/src/core/__tests__/HeaderWelcome.spec.ts new file mode 100644 index 00000000..73a43297 --- /dev/null +++ b/apps/client/src/core/__tests__/HeaderWelcome.spec.ts @@ -0,0 +1,71 @@ +import { type ComponentPublicInstance, nextTick } from 'vue' +import { shallowMount } from '@vue/test-utils' +import { describe, expect, it, vi } from 'vitest' +import HeaderWelcome from '../components/headers/HeaderWelcome.vue' +import type { headerLinks } from '../constants/header' +import type { VueWrapper } from '@vue/test-utils' +import i18n from '@/shared/libs/i18n' +import { UiButton } from '@/shared/ui' + +type HeaderWelcomeInstance = ComponentPublicInstance< + {}, + {}, + { + isDark: boolean + width: number + headerLinks: typeof headerLinks + } +> + +const mockRouter = { + push: vi.fn(), + beforeEach: vi.fn(), +} + +describe('tests for HeaderWelcome.vue', () => { + const wrapper = shallowMount(HeaderWelcome, { + global: { + plugins: [i18n], + + mocks: { + t: (key: string) => { + const translations: Record = { + 'welcome.header.login': 'Log In', + 'welcome.header.reg': 'Registration', + } + return translations[key] + }, + tm: (key: string) => { + const translationsArr: Record = { + 'welcome.header.links': [], + } + return translationsArr[key] + }, + $router: mockRouter, + }, + }, + }) as VueWrapper + + it('should be render correctly', async () => { + wrapper.vm.width = 1600 + await nextTick() + expect(wrapper.html()).toMatchSnapshot() + }) + + it('should redirect correctly to login', async () => { + const loginButton = wrapper.find('.btns').findAllComponents(UiButton).at(0) + + await loginButton.trigger('click') + expect(mockRouter.push).toHaveBeenCalledWith({ name: 'sign-in' }) + }) + + it('should redirect correctly to registration', async () => { + const registrationButton = wrapper + .find('.btns') + .findAllComponents(UiButton) + .at(1) + + await registrationButton.trigger('click') + expect(mockRouter.push).toHaveBeenCalledWith({ name: 'sign-up' }) + }) +}) diff --git a/apps/client/src/core/__tests__/UserMenu.spec.ts b/apps/client/src/core/__tests__/UserMenu.spec.ts new file mode 100644 index 00000000..22e29df1 --- /dev/null +++ b/apps/client/src/core/__tests__/UserMenu.spec.ts @@ -0,0 +1,43 @@ +import { nextTick } from 'vue' +import { mount } from '@vue/test-utils' +import { describe, expect, it, vi } from 'vitest' +import UserMenu from '../components/headers/UserMenu.vue' +import i18n from '@/shared/libs/i18n' +import { UiDropdownMenuItem, UiDropdownMenuTrigger } from '@/shared/ui' + +const mockRouter = { + push: vi.fn(), + beforeEach: vi.fn(), +} + +describe('tests for UserMenu.vue', () => { + const wrapper = mount(UserMenu, { + global: { + plugins: [i18n], + mocks: { + t: (key: string) => { + const translations: Record = { + 'header.user.welcome': 'welcome', + 'header.user.logout': 'logout', + } + return translations[key] + }, + router: mockRouter, + }, + }, + }) + + it('should redirect correctly to welcome', async () => { + await wrapper.findComponent(UiDropdownMenuTrigger).trigger('click') + await nextTick() + const items = wrapper.findAllComponents(UiDropdownMenuItem) + expect(items.length).toBeGreaterThan(0) + const welcomeTrigger = items.at(0) + await welcomeTrigger?.trigger('click') + expect(mockRouter.push).toHaveBeenCalledWith({ name: 'welcome' }) + }) + + it('should redirect correctly to logout', async () => { + // needs to be fixed + }) +}) diff --git a/apps/client/src/core/__tests__/WorkSpace.spec.ts b/apps/client/src/core/__tests__/WorkSpace.spec.ts new file mode 100644 index 00000000..ea735536 --- /dev/null +++ b/apps/client/src/core/__tests__/WorkSpace.spec.ts @@ -0,0 +1,58 @@ +import { mount, RouterLinkStub } from '@vue/test-utils' +import { describe, expect, it, vi } from 'vitest' +import { useRoute } from 'vue-router/auto' +import WorkSpace from '../components/sidebar/WorkSpace.vue' +import i18n from '@/shared/libs/i18n' + +vi.mock('vue-router/auto') + +vi.mock('@/shared/composables/expanded', () => ({ + useExpandedContext: vi.fn(() => ({ + isExpanded: { + value: true, + }, + })), +})) + +describe('tests for WorkSpace.vue', () => { + // @ts-expect-error mock types + useRoute.mockReturnValue({ name: 'boards' }) + const wrapper = mount(WorkSpace, { + global: { + plugins: [i18n], + mocks: { + t: (key: string) => { + const translations: Record = { + 'sidebar.boards': 'Boards', + 'sidebar.section': 'Workspace', + } + return translations[key] + }, + }, + stubs: { + RouterLink: RouterLinkStub, + }, + directives: { + tooltip() {}, + }, + }, + props: { + links: [ + { + id: 0, + name: 'boards', + pathName: 'boards', + icon: 'i-hugeicons-trello', + }, + ], + }, + }) + + it('should be render correctly', () => { + expect(wrapper.html()).toMatchSnapshot() + }) + + it('should be redirect with RouterLink correctly', () => { + expect(wrapper.findComponent(RouterLinkStub).props('to')).toEqual('/boards') + }) +}) diff --git a/apps/client/src/core/__tests__/__snapshots__/FooterWelcome.spec.ts.snap b/apps/client/src/core/__tests__/__snapshots__/FooterWelcome.spec.ts.snap new file mode 100644 index 00000000..e27e7c1a --- /dev/null +++ b/apps/client/src/core/__tests__/__snapshots__/FooterWelcome.spec.ts.snap @@ -0,0 +1,9 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`tests for FooterWelcome.vue > should be render correctly 1`] = ` +"
+
+

Jenda - cloud-based program for task management. The source code is available on Github

+
+
" +`; diff --git a/apps/client/src/core/__tests__/__snapshots__/HeaderWelcome.spec.ts.snap b/apps/client/src/core/__tests__/__snapshots__/HeaderWelcome.spec.ts.snap new file mode 100644 index 00000000..3692f472 --- /dev/null +++ b/apps/client/src/core/__tests__/__snapshots__/HeaderWelcome.spec.ts.snap @@ -0,0 +1,31 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`tests for HeaderWelcome.vue > should be render correctly 1`] = ` +"
+
+
+
+
+

Jenda

+
+ + + + + + +
+
+
+ +
+ +
+
+
+ + +
+
+
" +`; diff --git a/apps/client/src/core/__tests__/__snapshots__/WorkSpace.spec.ts.snap b/apps/client/src/core/__tests__/__snapshots__/WorkSpace.spec.ts.snap new file mode 100644 index 00000000..35d141b0 --- /dev/null +++ b/apps/client/src/core/__tests__/__snapshots__/WorkSpace.spec.ts.snap @@ -0,0 +1,9 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`tests for WorkSpace.vue > should be render correctly 1`] = ` +"" +`; diff --git a/apps/client/src/core/components/footer/AppFooter.vue b/apps/client/src/core/components/footer/AppFooter.vue new file mode 100644 index 00000000..8ad00b89 --- /dev/null +++ b/apps/client/src/core/components/footer/AppFooter.vue @@ -0,0 +1,19 @@ + diff --git a/apps/client/src/core/components/headers/BurgerMenu.vue b/apps/client/src/core/components/headers/BurgerMenu.vue new file mode 100644 index 00000000..46779ebd --- /dev/null +++ b/apps/client/src/core/components/headers/BurgerMenu.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/apps/client/src/core/components/headers/HeaderMain.vue b/apps/client/src/core/components/headers/HeaderMain.vue new file mode 100644 index 00000000..2785873f --- /dev/null +++ b/apps/client/src/core/components/headers/HeaderMain.vue @@ -0,0 +1,124 @@ + + + diff --git a/apps/client/src/core/components/headers/HeaderWelcome.vue b/apps/client/src/core/components/headers/HeaderWelcome.vue new file mode 100644 index 00000000..9f17ea63 --- /dev/null +++ b/apps/client/src/core/components/headers/HeaderWelcome.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/apps/client/src/core/components/headers/UserMenu.vue b/apps/client/src/core/components/headers/UserMenu.vue new file mode 100644 index 00000000..be3a5d4c --- /dev/null +++ b/apps/client/src/core/components/headers/UserMenu.vue @@ -0,0 +1,56 @@ + + + diff --git a/apps/client/src/core/components/headers/system/LanguageSelect.vue b/apps/client/src/core/components/headers/system/LanguageSelect.vue new file mode 100644 index 00000000..2eb4da0d --- /dev/null +++ b/apps/client/src/core/components/headers/system/LanguageSelect.vue @@ -0,0 +1,53 @@ + + + diff --git a/apps/client/src/core/components/headers/system/ThemeSwitcher.vue b/apps/client/src/core/components/headers/system/ThemeSwitcher.vue new file mode 100644 index 00000000..bc37020e --- /dev/null +++ b/apps/client/src/core/components/headers/system/ThemeSwitcher.vue @@ -0,0 +1,19 @@ + + + diff --git a/apps/client/src/core/components/sidebar/AppSidebar.vue b/apps/client/src/core/components/sidebar/AppSidebar.vue new file mode 100644 index 00000000..e96080a0 --- /dev/null +++ b/apps/client/src/core/components/sidebar/AppSidebar.vue @@ -0,0 +1,51 @@ + + + diff --git a/apps/client/src/core/components/sidebar/InfoMenu.vue b/apps/client/src/core/components/sidebar/InfoMenu.vue new file mode 100644 index 00000000..0a5e322c --- /dev/null +++ b/apps/client/src/core/components/sidebar/InfoMenu.vue @@ -0,0 +1,80 @@ + + + diff --git a/apps/client/src/core/components/sidebar/IntegrationItems.vue b/apps/client/src/core/components/sidebar/IntegrationItems.vue new file mode 100644 index 00000000..2a2b67a9 --- /dev/null +++ b/apps/client/src/core/components/sidebar/IntegrationItems.vue @@ -0,0 +1,55 @@ + + + diff --git a/apps/client/src/core/components/sidebar/ProjectsList.vue b/apps/client/src/core/components/sidebar/ProjectsList.vue new file mode 100644 index 00000000..9ab89442 --- /dev/null +++ b/apps/client/src/core/components/sidebar/ProjectsList.vue @@ -0,0 +1,98 @@ + + + diff --git a/apps/client/src/core/components/sidebar/WorkSpace.vue b/apps/client/src/core/components/sidebar/WorkSpace.vue new file mode 100644 index 00000000..4f73f839 --- /dev/null +++ b/apps/client/src/core/components/sidebar/WorkSpace.vue @@ -0,0 +1,69 @@ + + + diff --git a/apps/client/src/core/components/sidebar/WorkSpaceChooser.vue b/apps/client/src/core/components/sidebar/WorkSpaceChooser.vue new file mode 100644 index 00000000..878c5b0a --- /dev/null +++ b/apps/client/src/core/components/sidebar/WorkSpaceChooser.vue @@ -0,0 +1,62 @@ + + + diff --git a/apps/client/src/core/components/sidebar/plan/PlanCard.vue b/apps/client/src/core/components/sidebar/plan/PlanCard.vue new file mode 100644 index 00000000..c7be99fc --- /dev/null +++ b/apps/client/src/core/components/sidebar/plan/PlanCard.vue @@ -0,0 +1,52 @@ + + + diff --git a/apps/client/src/core/components/sidebar/plan/SubscribePlan.vue b/apps/client/src/core/components/sidebar/plan/SubscribePlan.vue new file mode 100644 index 00000000..46468d66 --- /dev/null +++ b/apps/client/src/core/components/sidebar/plan/SubscribePlan.vue @@ -0,0 +1,13 @@ + + + diff --git a/apps/client/src/core/constants/header.ts b/apps/client/src/core/constants/header.ts new file mode 100644 index 00000000..0499f62f --- /dev/null +++ b/apps/client/src/core/constants/header.ts @@ -0,0 +1,42 @@ +import type { HeaderNavLink, MenuLink, SectionItem } from '../types' + +export const headerLinks: HeaderNavLink[] = [ + { id: 0, name: 'About', pagePrefix: 'about' }, + { id: 1, name: 'Kanban', pagePrefix: 'kanban' }, + { id: 2, name: 'Collaborative', pagePrefix: 'collaborative' }, + { id: 3, name: 'Activity', pagePrefix: 'activity' }, + { id: 4, name: 'Members', pagePrefix: 'members' }, + { id: 5, name: 'Chats', pagePrefix: 'chats' }, +] + +export const menuLinks: MenuLink[] = [ + { url: 'https://github.com/mnenie/jenda' }, + { url: 'https://t.me/youngjuicycashrussia' }, +] + +export const items: SectionItem[] = [ + { + icon: '✨', + urlPrefix: 'about', + }, + { + icon: '🧑‍💻', + urlPrefix: 'kanban', + }, + { + icon: '👥', + urlPrefix: 'collaborative', + }, + { + icon: '🌐', + urlPrefix: 'members', + }, + { + icon: '👔', + urlPrefix: 'activity', + }, + { + icon: '💬', + urlPrefix: 'activity', + }, +] diff --git a/apps/client/src/core/constants/layouts.ts b/apps/client/src/core/constants/layouts.ts new file mode 100644 index 00000000..a34c9c8a --- /dev/null +++ b/apps/client/src/core/constants/layouts.ts @@ -0,0 +1,8 @@ +import type { LayoutsEnum } from '@/shared/constants/layouts' + +export const LayoutToFileMap: Record = { + default: 'DefaultLayout.vue', + auth: 'AuthLayout.vue', + welcome: 'WelcomeLayout.vue', + empty: 'EmptyLayout.vue', +} diff --git a/apps/client/src/core/injections/index.ts b/apps/client/src/core/injections/index.ts new file mode 100644 index 00000000..cda42e23 --- /dev/null +++ b/apps/client/src/core/injections/index.ts @@ -0,0 +1 @@ +// with seo diff --git a/apps/client/src/core/layouts/AppLayout.vue b/apps/client/src/core/layouts/AppLayout.vue new file mode 100644 index 00000000..4cbbcbfa --- /dev/null +++ b/apps/client/src/core/layouts/AppLayout.vue @@ -0,0 +1,8 @@ + diff --git a/apps/client/src/core/layouts/AuthLayout.vue b/apps/client/src/core/layouts/AuthLayout.vue new file mode 100644 index 00000000..ec8e7e3b --- /dev/null +++ b/apps/client/src/core/layouts/AuthLayout.vue @@ -0,0 +1,12 @@ + + + diff --git a/apps/client/src/core/layouts/DefaultLayout.vue b/apps/client/src/core/layouts/DefaultLayout.vue new file mode 100644 index 00000000..4b6d6c91 --- /dev/null +++ b/apps/client/src/core/layouts/DefaultLayout.vue @@ -0,0 +1,56 @@ + + + diff --git a/apps/client/src/core/layouts/EmptyLayout.vue b/apps/client/src/core/layouts/EmptyLayout.vue new file mode 100644 index 00000000..14351cce --- /dev/null +++ b/apps/client/src/core/layouts/EmptyLayout.vue @@ -0,0 +1,5 @@ + diff --git a/apps/client/src/core/layouts/WelcomeLayout.vue b/apps/client/src/core/layouts/WelcomeLayout.vue new file mode 100644 index 00000000..3724b8b9 --- /dev/null +++ b/apps/client/src/core/layouts/WelcomeLayout.vue @@ -0,0 +1,21 @@ + + + diff --git a/apps/client/src/core/pages/[...path].vue b/apps/client/src/core/pages/[...path].vue new file mode 100644 index 00000000..cca43902 --- /dev/null +++ b/apps/client/src/core/pages/[...path].vue @@ -0,0 +1,51 @@ + + + diff --git a/apps/client/src/core/pages/index.vue b/apps/client/src/core/pages/index.vue new file mode 100644 index 00000000..353d0a1c --- /dev/null +++ b/apps/client/src/core/pages/index.vue @@ -0,0 +1,41 @@ + + + diff --git a/apps/client/src/core/types/header.ts b/apps/client/src/core/types/header.ts new file mode 100644 index 00000000..42c2b74f --- /dev/null +++ b/apps/client/src/core/types/header.ts @@ -0,0 +1,23 @@ +export interface SectionItem { + title?: string + description?: string + icon: string + urlPrefix: + | 'kanban' + | 'about' + | 'collaborative' + | 'activity' + | 'members' + | 'chats' +} + +export interface MenuLink { + title?: string + url: string +} + +export interface HeaderNavLink { + id: number + name: string + pagePrefix: string +} diff --git a/apps/client/src/core/types/index.ts b/apps/client/src/core/types/index.ts new file mode 100644 index 00000000..d153d3b4 --- /dev/null +++ b/apps/client/src/core/types/index.ts @@ -0,0 +1,2 @@ +export * from './header' +export * from './sidebar' diff --git a/apps/client/src/core/types/sidebar.ts b/apps/client/src/core/types/sidebar.ts new file mode 100644 index 00000000..5ba0a4c1 --- /dev/null +++ b/apps/client/src/core/types/sidebar.ts @@ -0,0 +1,16 @@ +import type { IconifyIcon } from '@iconify/vue' +import type { RouterLinkProps } from 'vue-router/auto' + +export interface Link { + id: number + name: string + pathName: RouterLinkProps['to'] + // string with unocss update + icon: IconifyIcon | string +} + +export interface IntegrationItem { + name: string + link: string + icon: string +} diff --git a/apps/client/src/main.ts b/apps/client/src/main.ts new file mode 100644 index 00000000..e8503ab0 --- /dev/null +++ b/apps/client/src/main.ts @@ -0,0 +1,35 @@ +import { createApp } from 'vue' +import { createHead } from '@unhead/vue' +import { vTooltip } from 'floating-vue' +import Vue3Marquee from 'vue3-marquee' +import { VueUiRadar } from 'vue-data-ui' + +// @ts-expect-error: unresolved type definitions for vue-writer +import VueWriter from 'vue-writer' +import App from './App.vue' +import autoAnimatePlugin from './plugins/formkit' + +import { pinia } from './store' +import { router } from './router' +import i18n from '@/shared/libs/i18n' + +import './styles/index.css' +import '@unocss/reset/tailwind-compat.css' +import 'virtual:uno.css' +import 'floating-vue/dist/style.css' +import 'vue-data-ui/style.css' + +const app = createApp(App) +// head plugin +const head = createHead() + +app.use(pinia) +app.use(router) +app.use(i18n) +app.use(head) +app.use(autoAnimatePlugin) +app.use(Vue3Marquee) +app.use(VueWriter) +app.directive('tooltip', vTooltip) +app.component('VueUiRadar', VueUiRadar) +app.mount('#app') diff --git a/apps/client/src/modules/auth/components/AuthContainer.vue b/apps/client/src/modules/auth/components/AuthContainer.vue new file mode 100644 index 00000000..3ce9f7e7 --- /dev/null +++ b/apps/client/src/modules/auth/components/AuthContainer.vue @@ -0,0 +1,68 @@ + + + diff --git a/apps/client/src/modules/auth/components/ConfirmSection.vue b/apps/client/src/modules/auth/components/ConfirmSection.vue new file mode 100644 index 00000000..5dcd2b22 --- /dev/null +++ b/apps/client/src/modules/auth/components/ConfirmSection.vue @@ -0,0 +1,32 @@ + + + diff --git a/apps/client/src/modules/auth/components/UserAvatar.vue b/apps/client/src/modules/auth/components/UserAvatar.vue new file mode 100644 index 00000000..a2d12403 --- /dev/null +++ b/apps/client/src/modules/auth/components/UserAvatar.vue @@ -0,0 +1,8 @@ + diff --git a/apps/client/src/modules/auth/components/WorkspaceChoosing.vue b/apps/client/src/modules/auth/components/WorkspaceChoosing.vue new file mode 100644 index 00000000..d91ed445 --- /dev/null +++ b/apps/client/src/modules/auth/components/WorkspaceChoosing.vue @@ -0,0 +1,27 @@ + + + diff --git a/apps/client/src/modules/auth/components/WorkspaceCreating.vue b/apps/client/src/modules/auth/components/WorkspaceCreating.vue new file mode 100644 index 00000000..84f46f14 --- /dev/null +++ b/apps/client/src/modules/auth/components/WorkspaceCreating.vue @@ -0,0 +1,31 @@ + + + diff --git a/apps/client/src/modules/auth/components/common/BgPanel.vue b/apps/client/src/modules/auth/components/common/BgPanel.vue new file mode 100644 index 00000000..81810587 --- /dev/null +++ b/apps/client/src/modules/auth/components/common/BgPanel.vue @@ -0,0 +1,47 @@ + + + diff --git a/apps/client/src/modules/auth/components/common/LogoFile.vue b/apps/client/src/modules/auth/components/common/LogoFile.vue new file mode 100644 index 00000000..7e1afed4 --- /dev/null +++ b/apps/client/src/modules/auth/components/common/LogoFile.vue @@ -0,0 +1,43 @@ + + + diff --git a/apps/client/src/modules/auth/components/common/PrivacyPolicy.vue b/apps/client/src/modules/auth/components/common/PrivacyPolicy.vue new file mode 100644 index 00000000..75e458fe --- /dev/null +++ b/apps/client/src/modules/auth/components/common/PrivacyPolicy.vue @@ -0,0 +1,38 @@ + + + diff --git a/apps/client/src/modules/auth/components/features/GoogleOauth.vue b/apps/client/src/modules/auth/components/features/GoogleOauth.vue new file mode 100644 index 00000000..a29e33a1 --- /dev/null +++ b/apps/client/src/modules/auth/components/features/GoogleOauth.vue @@ -0,0 +1,15 @@ + + + diff --git a/apps/client/src/modules/auth/components/features/HandleLogo.vue b/apps/client/src/modules/auth/components/features/HandleLogo.vue new file mode 100644 index 00000000..05bec823 --- /dev/null +++ b/apps/client/src/modules/auth/components/features/HandleLogo.vue @@ -0,0 +1,17 @@ + + + diff --git a/apps/client/src/modules/auth/components/features/WorkspaceLogoChooser.vue b/apps/client/src/modules/auth/components/features/WorkspaceLogoChooser.vue new file mode 100644 index 00000000..e7fb7695 --- /dev/null +++ b/apps/client/src/modules/auth/components/features/WorkspaceLogoChooser.vue @@ -0,0 +1,55 @@ + + + diff --git a/apps/client/src/modules/auth/components/forms/ConfirmForm.vue b/apps/client/src/modules/auth/components/forms/ConfirmForm.vue new file mode 100644 index 00000000..1990c625 --- /dev/null +++ b/apps/client/src/modules/auth/components/forms/ConfirmForm.vue @@ -0,0 +1,75 @@ + + + diff --git a/apps/client/src/modules/auth/components/forms/SignInForm.vue b/apps/client/src/modules/auth/components/forms/SignInForm.vue new file mode 100644 index 00000000..e51598cc --- /dev/null +++ b/apps/client/src/modules/auth/components/forms/SignInForm.vue @@ -0,0 +1,88 @@ + + + diff --git a/apps/client/src/modules/auth/components/forms/SignUpForm.vue b/apps/client/src/modules/auth/components/forms/SignUpForm.vue new file mode 100644 index 00000000..c6bba9a5 --- /dev/null +++ b/apps/client/src/modules/auth/components/forms/SignUpForm.vue @@ -0,0 +1,88 @@ + + + diff --git a/apps/client/src/modules/auth/components/forms/WorkspaceChanger.vue b/apps/client/src/modules/auth/components/forms/WorkspaceChanger.vue new file mode 100644 index 00000000..b1d84244 --- /dev/null +++ b/apps/client/src/modules/auth/components/forms/WorkspaceChanger.vue @@ -0,0 +1,60 @@ + + + diff --git a/apps/client/src/modules/auth/components/forms/WorkspaceForm.vue b/apps/client/src/modules/auth/components/forms/WorkspaceForm.vue new file mode 100644 index 00000000..3cd18488 --- /dev/null +++ b/apps/client/src/modules/auth/components/forms/WorkspaceForm.vue @@ -0,0 +1,66 @@ + + + diff --git a/apps/client/src/modules/auth/composables/auth-workspace.ts b/apps/client/src/modules/auth/composables/auth-workspace.ts new file mode 100644 index 00000000..4f9c85e9 --- /dev/null +++ b/apps/client/src/modules/auth/composables/auth-workspace.ts @@ -0,0 +1,30 @@ +import { inject, type InjectionKey, provide } from 'vue' + +interface AuthWorkspace { + reset: () => void + openFileChooser: (event: any) => void +} + +type T = AuthWorkspace + +const key: InjectionKey = Symbol('auth-workspace-chooser') + +export function provideWorkspaceChooserContext(value: T) { + provide(key, value) + return value +} + +export function useWorkspaceChooserContext< + U extends T | undefined = T, +>( + fallback?: U, +): U extends null ? T | null : T { + const expanded = inject(key, fallback) + if (expanded) + return expanded as T + + if (expanded === null) + return expanded as any + + throw new Error('not provided') +} diff --git a/apps/client/src/modules/auth/composables/panel.ts b/apps/client/src/modules/auth/composables/panel.ts new file mode 100644 index 00000000..3a267d8d --- /dev/null +++ b/apps/client/src/modules/auth/composables/panel.ts @@ -0,0 +1,21 @@ +import { ref, watch } from 'vue' +import { useRoute } from 'vue-router' +import type { Review } from '../types' + +export function useTextChanging(reviews: Review[]) { + // so, maybe Ill do with live data + const currentIndex = ref(0) + const route = useRoute() + + const changeReviewText = () => { + currentIndex.value = Math.floor(Math.random() * reviews.length) + } + watch(() => route.path, () => { + changeReviewText() + }, { immediate: true }) + + return { + currentIndex, + changeReviewText, + } +} diff --git a/apps/client/src/modules/auth/fixtures/reviews.ts b/apps/client/src/modules/auth/fixtures/reviews.ts new file mode 100644 index 00000000..b048cd0f --- /dev/null +++ b/apps/client/src/modules/auth/fixtures/reviews.ts @@ -0,0 +1,20 @@ +import type { Review } from '../types' + +// mocks -> after data from backend +export const reviews: Review[] = [ + { + id: '0', + author: '@alexpeshkov', + text: `Y'all Jenda is amazing! 🙌 Barely an hour since I've been here and I don't want to leave my workspace. 🤯🤯🤯`, + }, + { + id: '1', + author: '@airvt1x', + text: `I just learned about Jenda and im in love 💫 Jenda is a Trello/Jira alternative!💡so, simple for understanding !!!`, + }, + { + id: '2', + author: '@alexpeshkov', + text: `So, Jenda is just 🔥 Now I understand why many people like to use it.`, + }, +] diff --git a/apps/client/src/modules/auth/pages/auth/sign-in/index.vue b/apps/client/src/modules/auth/pages/auth/sign-in/index.vue new file mode 100644 index 00000000..496acd71 --- /dev/null +++ b/apps/client/src/modules/auth/pages/auth/sign-in/index.vue @@ -0,0 +1,26 @@ + + + diff --git a/apps/client/src/modules/auth/pages/auth/sign-in/workspace.vue b/apps/client/src/modules/auth/pages/auth/sign-in/workspace.vue new file mode 100644 index 00000000..88984ad9 --- /dev/null +++ b/apps/client/src/modules/auth/pages/auth/sign-in/workspace.vue @@ -0,0 +1,43 @@ + + + diff --git a/apps/client/src/modules/auth/pages/auth/sign-up/confirm.vue b/apps/client/src/modules/auth/pages/auth/sign-up/confirm.vue new file mode 100644 index 00000000..2d9bb2ab --- /dev/null +++ b/apps/client/src/modules/auth/pages/auth/sign-up/confirm.vue @@ -0,0 +1,27 @@ + + + diff --git a/apps/client/src/modules/auth/pages/auth/sign-up/index.vue b/apps/client/src/modules/auth/pages/auth/sign-up/index.vue new file mode 100644 index 00000000..3049d759 --- /dev/null +++ b/apps/client/src/modules/auth/pages/auth/sign-up/index.vue @@ -0,0 +1,26 @@ + + + diff --git a/apps/client/src/modules/auth/pages/auth/sign-up/workspace.vue b/apps/client/src/modules/auth/pages/auth/sign-up/workspace.vue new file mode 100644 index 00000000..dccbb414 --- /dev/null +++ b/apps/client/src/modules/auth/pages/auth/sign-up/workspace.vue @@ -0,0 +1,32 @@ + + + diff --git a/apps/client/src/modules/auth/stores/auth.ts b/apps/client/src/modules/auth/stores/auth.ts new file mode 100644 index 00000000..bd4c5c9b --- /dev/null +++ b/apps/client/src/modules/auth/stores/auth.ts @@ -0,0 +1,11 @@ +import { ref } from 'vue' +import { defineStore } from 'pinia' +import type { User } from '../types' + +export const useUserStore = defineStore('user', () => { + const user = ref({} as User) + + return { + user, + } +}) diff --git a/apps/client/src/modules/auth/types/index.ts b/apps/client/src/modules/auth/types/index.ts new file mode 100644 index 00000000..ae71d87c --- /dev/null +++ b/apps/client/src/modules/auth/types/index.ts @@ -0,0 +1,2 @@ +export * from './review' +export * from './user' diff --git a/apps/client/src/modules/auth/types/review.ts b/apps/client/src/modules/auth/types/review.ts new file mode 100644 index 00000000..6d40c411 --- /dev/null +++ b/apps/client/src/modules/auth/types/review.ts @@ -0,0 +1,5 @@ +export interface Review { + id: string + author: string + text: string +} diff --git a/apps/client/src/modules/auth/types/user.ts b/apps/client/src/modules/auth/types/user.ts new file mode 100644 index 00000000..7514dafe --- /dev/null +++ b/apps/client/src/modules/auth/types/user.ts @@ -0,0 +1,16 @@ +interface DateParams { + createdAt?: Date + updatedAt?: Date + deletedAt?: Date +} + +export interface User extends DateParams { + _id?: string + email: string + photoUrl?: string + role?: string +} + +export interface UserAuth extends User { + token: string +} diff --git a/apps/client/src/modules/boards/components/BoardsActionsPanel.vue b/apps/client/src/modules/boards/components/BoardsActionsPanel.vue new file mode 100644 index 00000000..83776500 --- /dev/null +++ b/apps/client/src/modules/boards/components/BoardsActionsPanel.vue @@ -0,0 +1,46 @@ + + + diff --git a/apps/client/src/modules/boards/components/BoardsDataTable.vue b/apps/client/src/modules/boards/components/BoardsDataTable.vue new file mode 100644 index 00000000..306cd8f7 --- /dev/null +++ b/apps/client/src/modules/boards/components/BoardsDataTable.vue @@ -0,0 +1,165 @@ + + + diff --git a/apps/client/src/modules/boards/components/EmptyBoards.vue b/apps/client/src/modules/boards/components/EmptyBoards.vue new file mode 100644 index 00000000..f98ba0e1 --- /dev/null +++ b/apps/client/src/modules/boards/components/EmptyBoards.vue @@ -0,0 +1,22 @@ + + + diff --git a/apps/client/src/modules/boards/components/NewBoardDialog.vue b/apps/client/src/modules/boards/components/NewBoardDialog.vue new file mode 100644 index 00000000..622b781e --- /dev/null +++ b/apps/client/src/modules/boards/components/NewBoardDialog.vue @@ -0,0 +1,27 @@ + + + diff --git a/apps/client/src/modules/boards/components/PaginationItems.vue b/apps/client/src/modules/boards/components/PaginationItems.vue new file mode 100644 index 00000000..facaeee8 --- /dev/null +++ b/apps/client/src/modules/boards/components/PaginationItems.vue @@ -0,0 +1,38 @@ + + + diff --git a/apps/client/src/modules/boards/components/TableControls.vue b/apps/client/src/modules/boards/components/TableControls.vue new file mode 100644 index 00000000..ece095ae --- /dev/null +++ b/apps/client/src/modules/boards/components/TableControls.vue @@ -0,0 +1,45 @@ + + + diff --git a/apps/client/src/modules/boards/components/features/ChooseRowsCount.vue b/apps/client/src/modules/boards/components/features/ChooseRowsCount.vue new file mode 100644 index 00000000..51374022 --- /dev/null +++ b/apps/client/src/modules/boards/components/features/ChooseRowsCount.vue @@ -0,0 +1,32 @@ + + + diff --git a/apps/client/src/modules/boards/components/features/CreateNewBoard.vue b/apps/client/src/modules/boards/components/features/CreateNewBoard.vue new file mode 100644 index 00000000..b82a0b71 --- /dev/null +++ b/apps/client/src/modules/boards/components/features/CreateNewBoard.vue @@ -0,0 +1,43 @@ + + + diff --git a/apps/client/src/modules/boards/components/features/handlers/AddBoard.vue b/apps/client/src/modules/boards/components/features/handlers/AddBoard.vue new file mode 100644 index 00000000..2b3756d9 --- /dev/null +++ b/apps/client/src/modules/boards/components/features/handlers/AddBoard.vue @@ -0,0 +1,24 @@ + + + diff --git a/apps/client/src/modules/boards/components/features/handlers/RemoveBoard.vue b/apps/client/src/modules/boards/components/features/handlers/RemoveBoard.vue new file mode 100644 index 00000000..3a151297 --- /dev/null +++ b/apps/client/src/modules/boards/components/features/handlers/RemoveBoard.vue @@ -0,0 +1,20 @@ + + + diff --git a/apps/client/src/modules/boards/components/filters/BoardsAdvancedFilter.vue b/apps/client/src/modules/boards/components/filters/BoardsAdvancedFilter.vue new file mode 100644 index 00000000..1e5cdacf --- /dev/null +++ b/apps/client/src/modules/boards/components/filters/BoardsAdvancedFilter.vue @@ -0,0 +1,108 @@ + + + diff --git a/apps/client/src/modules/boards/components/filters/BoardsSort.vue b/apps/client/src/modules/boards/components/filters/BoardsSort.vue new file mode 100644 index 00000000..f4d2075d --- /dev/null +++ b/apps/client/src/modules/boards/components/filters/BoardsSort.vue @@ -0,0 +1,33 @@ + + + diff --git a/apps/client/src/modules/boards/components/filters/SearchBoards.vue b/apps/client/src/modules/boards/components/filters/SearchBoards.vue new file mode 100644 index 00000000..6cf3b11b --- /dev/null +++ b/apps/client/src/modules/boards/components/filters/SearchBoards.vue @@ -0,0 +1,7 @@ + + + diff --git a/apps/client/src/modules/boards/components/forms/CreateBoardForm.vue b/apps/client/src/modules/boards/components/forms/CreateBoardForm.vue new file mode 100644 index 00000000..bb4f6f4e --- /dev/null +++ b/apps/client/src/modules/boards/components/forms/CreateBoardForm.vue @@ -0,0 +1,100 @@ + + + diff --git a/apps/client/src/modules/boards/components/forms/TagsChooser.vue b/apps/client/src/modules/boards/components/forms/TagsChooser.vue new file mode 100644 index 00000000..cdb92555 --- /dev/null +++ b/apps/client/src/modules/boards/components/forms/TagsChooser.vue @@ -0,0 +1,50 @@ + + + diff --git a/apps/client/src/modules/boards/composables/filtered.ts b/apps/client/src/modules/boards/composables/filtered.ts new file mode 100644 index 00000000..a897b375 --- /dev/null +++ b/apps/client/src/modules/boards/composables/filtered.ts @@ -0,0 +1,78 @@ +import { computed, inject, provide, toValue } from 'vue' +import type { InjectionKey, Ref } from 'vue' +import type { BoardRow } from '../types' + +export function useFilteredBoards( + boards: Ref, + sortRefValue: Ref, + advancedFilterRefValues: Ref, +) { + const filterBoards = (boards: U[], filterValues: string[]): U[] => { + return boards.filter((board) => { + if (filterValues.length === 0) + return true + const status = filterValues.includes(board.status) + const labels = Array.isArray(board.labels) + ? board.labels.some(label => filterValues.includes(label.name)) + : filterValues.includes(board.labels) + return status || labels + }) + } + const sortBoards = (boards: U[], sortValue: string): U[] => { + return [...boards].sort((a, b): number => { + if (sortValue === 'name') { + return a.name.localeCompare(b.name) + } + if (sortValue === 'tasks') { + return (b.tasks || 0) - (a.tasks || 0) + } + if (sortValue === 'date') { + return (b.date ? +new Date(b.date) : 0) - (a.date ? +new Date(a.date) : 0) + } + return 0 + }) + } + const filteredBoards = computed(() => { + const sortValue = toValue(sortRefValue) + const filterValues = toValue(advancedFilterRefValues) + + let result = filterBoards(boards.value, filterValues) + if (sortValue !== 'default') { + result = sortBoards(result, sortValue) + } + return result + }) + + return { + filteredBoards, + } +} + +interface FilteredContext { + sortModel: Ref + advancedModel: Ref +} + +type T = FilteredContext + +const key: InjectionKey = Symbol('filtered-boards') + +export function provideFilteredContext(value: T) { + provide(key, value) + return value +} + +export function useFilteredContext< + U extends T | undefined = T, +>( + fallback?: U, +): U extends null ? T | null : T { + const expanded = inject(key, fallback) + if (expanded) + return expanded as T + + if (expanded === null) + return expanded as any + + throw new Error('not provided') +} diff --git a/apps/client/src/modules/boards/pages/boards.vue b/apps/client/src/modules/boards/pages/boards.vue new file mode 100644 index 00000000..db7ec7e3 --- /dev/null +++ b/apps/client/src/modules/boards/pages/boards.vue @@ -0,0 +1,75 @@ + + + diff --git a/apps/client/src/modules/boards/pages/boards/[id].vue b/apps/client/src/modules/boards/pages/boards/[id].vue new file mode 100644 index 00000000..049cab60 --- /dev/null +++ b/apps/client/src/modules/boards/pages/boards/[id].vue @@ -0,0 +1,47 @@ + + + diff --git a/apps/client/src/modules/boards/pages/boards/new.vue b/apps/client/src/modules/boards/pages/boards/new.vue new file mode 100644 index 00000000..80f01e90 --- /dev/null +++ b/apps/client/src/modules/boards/pages/boards/new.vue @@ -0,0 +1,19 @@ + + + diff --git a/apps/client/src/modules/boards/stores/board.ts b/apps/client/src/modules/boards/stores/board.ts new file mode 100644 index 00000000..78fd2211 --- /dev/null +++ b/apps/client/src/modules/boards/stores/board.ts @@ -0,0 +1,32 @@ +import { ref } from 'vue' +import { acceptHMRUpdate, defineStore } from 'pinia' +import type { Board, BoardRow } from '../types' + +export const useBoardsStore = defineStore('boards', () => { + const boards = ref([]) + const board = ref() + + function removeBoards(idxs: string[]) { + idxs.forEach((id) => { + const index = boards.value.findIndex(board => board._id === id) + if (index !== -1) { + boards.value.splice(index, 1) + } + }) + } + + function addBoard(board: Board) { + boards.value.push(board) + } + + return { + boards, + board, + addBoard, + removeBoards, + } +}) + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useBoardsStore, import.meta.hot)) +} diff --git a/apps/client/src/modules/boards/types/index.ts b/apps/client/src/modules/boards/types/index.ts new file mode 100644 index 00000000..2a57bc23 --- /dev/null +++ b/apps/client/src/modules/boards/types/index.ts @@ -0,0 +1,60 @@ +import type { User } from '@/modules/auth/types' + +interface DateParams { + createdAt?: Date + updatedAt?: Date + deletedAt?: Date +} + +export interface Label { + name: string + color: string +} + +export type Status = 'active' | 'archived' + +type Priority = 'none' | 'low' | 'medium' | 'high' + +export interface BoardRow extends Omit { + tasks?: number +} + +export interface Board extends DateParams { + _id: string + name: string + columns?: Column[] + status: Status + labels: Label[] + users: User[] + color?: string + estimate?: number + // fix + date?: string +} + +interface Tag { + _id: string + name: string +} + +export interface Card extends DateParams { + _id: string + title: string + priority: Priority + tags?: Tag[] + chat?: boolean + chatCount?: number + users: User[] +} + +export interface Column extends DateParams { + _id: string + title: string + cards?: Card[] +} + +export interface StatusBadge { + _id: string + indicator: string + status: Status +} diff --git a/apps/client/src/modules/charts/components/ChartItemWrapper.vue b/apps/client/src/modules/charts/components/ChartItemWrapper.vue new file mode 100644 index 00000000..334489b5 --- /dev/null +++ b/apps/client/src/modules/charts/components/ChartItemWrapper.vue @@ -0,0 +1,40 @@ + + + diff --git a/apps/client/src/modules/charts/components/SharedSection.vue b/apps/client/src/modules/charts/components/SharedSection.vue new file mode 100644 index 00000000..f5fa9d6a --- /dev/null +++ b/apps/client/src/modules/charts/components/SharedSection.vue @@ -0,0 +1,17 @@ + + + diff --git a/apps/client/src/modules/charts/components/charts/BoardsChart.vue b/apps/client/src/modules/charts/components/charts/BoardsChart.vue new file mode 100644 index 00000000..16e9e776 --- /dev/null +++ b/apps/client/src/modules/charts/components/charts/BoardsChart.vue @@ -0,0 +1,100 @@ + + + diff --git a/apps/client/src/modules/charts/components/charts/OnlineChart.vue b/apps/client/src/modules/charts/components/charts/OnlineChart.vue new file mode 100644 index 00000000..ea207ca8 --- /dev/null +++ b/apps/client/src/modules/charts/components/charts/OnlineChart.vue @@ -0,0 +1,79 @@ + + + diff --git a/apps/client/src/modules/charts/components/charts/TasksChart.vue b/apps/client/src/modules/charts/components/charts/TasksChart.vue new file mode 100644 index 00000000..5b777102 --- /dev/null +++ b/apps/client/src/modules/charts/components/charts/TasksChart.vue @@ -0,0 +1,114 @@ + + + diff --git a/apps/client/src/modules/charts/components/charts/UsersChart.vue b/apps/client/src/modules/charts/components/charts/UsersChart.vue new file mode 100644 index 00000000..0b9e7478 --- /dev/null +++ b/apps/client/src/modules/charts/components/charts/UsersChart.vue @@ -0,0 +1,44 @@ + + + diff --git a/apps/client/src/modules/charts/composables/charts.ts b/apps/client/src/modules/charts/composables/charts.ts new file mode 100644 index 00000000..b6c6a536 --- /dev/null +++ b/apps/client/src/modules/charts/composables/charts.ts @@ -0,0 +1,38 @@ +import { computed } from 'vue' +import { useBreakpoints } from '@/shared/composables/breakpoints' +import { useExpandedContext } from '@/shared/composables/expanded' + +export function useCharts() { + const { breakpoints } = useBreakpoints() + + const { isExpanded } = useExpandedContext() + + const isBetweenIntermediateDesktopAnd4K = breakpoints.between( + 'intermediateDesktop', + 'desktop4K', + ).value + + const chartOnlineInWorkspaceValue = computed(() => { + if (isBetweenIntermediateDesktopAnd4K) { + return isExpanded.value ? 7.7 : 6.5 + } + return isExpanded.value ? 10 : 8 + }) + + const chartBoardsValue = computed(() => { + if (isBetweenIntermediateDesktopAnd4K) { + return isExpanded.value ? 10 : 9 + } + return isExpanded.value ? 13 : 11 + }) + + const chartTasksValue = computed(() => + isBetweenIntermediateDesktopAnd4K ? 14 : 18, + ) + + return { + chartOnlineInWorkspaceValue, + chartBoardsValue, + chartTasksValue, + } +} diff --git a/apps/client/src/modules/charts/pages/analytics.vue b/apps/client/src/modules/charts/pages/analytics.vue new file mode 100644 index 00000000..f5a5addb --- /dev/null +++ b/apps/client/src/modules/charts/pages/analytics.vue @@ -0,0 +1,67 @@ + + + diff --git a/apps/client/src/modules/charts/types/index.ts b/apps/client/src/modules/charts/types/index.ts new file mode 100644 index 00000000..6644478f --- /dev/null +++ b/apps/client/src/modules/charts/types/index.ts @@ -0,0 +1,4 @@ +export interface Chart { + key: 'users' | 'boards' | 'tasks' | 'online' + section: string +} diff --git a/apps/client/src/modules/common/components/ShareLink.vue b/apps/client/src/modules/common/components/ShareLink.vue new file mode 100644 index 00000000..bcef5a8d --- /dev/null +++ b/apps/client/src/modules/common/components/ShareLink.vue @@ -0,0 +1,32 @@ + + + diff --git a/apps/client/src/modules/common/components/controls/ViewControl.vue b/apps/client/src/modules/common/components/controls/ViewControl.vue new file mode 100644 index 00000000..2c805def --- /dev/null +++ b/apps/client/src/modules/common/components/controls/ViewControl.vue @@ -0,0 +1,15 @@ + + + diff --git a/apps/client/src/modules/common/components/dialogs/HotkeysDialog.vue b/apps/client/src/modules/common/components/dialogs/HotkeysDialog.vue new file mode 100644 index 00000000..45ced5ce --- /dev/null +++ b/apps/client/src/modules/common/components/dialogs/HotkeysDialog.vue @@ -0,0 +1,61 @@ + + + diff --git a/apps/client/src/modules/common/components/dialogs/ShareDialog.vue b/apps/client/src/modules/common/components/dialogs/ShareDialog.vue new file mode 100644 index 00000000..a7e8f956 --- /dev/null +++ b/apps/client/src/modules/common/components/dialogs/ShareDialog.vue @@ -0,0 +1,40 @@ + + + diff --git a/apps/client/src/modules/common/readme.md b/apps/client/src/modules/common/readme.md new file mode 100644 index 00000000..db88aeed --- /dev/null +++ b/apps/client/src/modules/common/readme.md @@ -0,0 +1 @@ +shared module diff --git a/apps/client/src/modules/members/pages/members.vue b/apps/client/src/modules/members/pages/members.vue new file mode 100644 index 00000000..a85813cb --- /dev/null +++ b/apps/client/src/modules/members/pages/members.vue @@ -0,0 +1,24 @@ + + + diff --git a/apps/client/src/modules/notes/.gitkeep b/apps/client/src/modules/notes/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/apps/client/src/modules/notes/pages/notes.vue b/apps/client/src/modules/notes/pages/notes.vue new file mode 100644 index 00000000..9e8e7fae --- /dev/null +++ b/apps/client/src/modules/notes/pages/notes.vue @@ -0,0 +1,20 @@ + + + diff --git a/apps/client/src/modules/search/components/PanelItems.vue b/apps/client/src/modules/search/components/PanelItems.vue new file mode 100644 index 00000000..39f15f5b --- /dev/null +++ b/apps/client/src/modules/search/components/PanelItems.vue @@ -0,0 +1,24 @@ + + + diff --git a/apps/client/src/modules/search/components/SearchBox.vue b/apps/client/src/modules/search/components/SearchBox.vue new file mode 100644 index 00000000..519d6b2b --- /dev/null +++ b/apps/client/src/modules/search/components/SearchBox.vue @@ -0,0 +1,63 @@ + + + diff --git a/apps/client/src/modules/search/components/SearchFilter.vue b/apps/client/src/modules/search/components/SearchFilter.vue new file mode 100644 index 00000000..6926e45c --- /dev/null +++ b/apps/client/src/modules/search/components/SearchFilter.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/apps/client/src/modules/search/components/SearchItem.vue b/apps/client/src/modules/search/components/SearchItem.vue new file mode 100644 index 00000000..2aec06be --- /dev/null +++ b/apps/client/src/modules/search/components/SearchItem.vue @@ -0,0 +1,33 @@ + + + diff --git a/apps/client/src/modules/search/components/SearchList.vue b/apps/client/src/modules/search/components/SearchList.vue new file mode 100644 index 00000000..62a34cea --- /dev/null +++ b/apps/client/src/modules/search/components/SearchList.vue @@ -0,0 +1,93 @@ + + + diff --git a/apps/client/src/modules/search/types/index.ts b/apps/client/src/modules/search/types/index.ts new file mode 100644 index 00000000..17d0a134 --- /dev/null +++ b/apps/client/src/modules/search/types/index.ts @@ -0,0 +1,11 @@ +export interface SearchRoute { + name: string + path: string + icon?: string + color?: string +} + +export interface PanelItem { + icon: string + tPrefix: string +} diff --git a/apps/client/src/modules/settings/pages/settings.vue b/apps/client/src/modules/settings/pages/settings.vue new file mode 100644 index 00000000..7b57f72a --- /dev/null +++ b/apps/client/src/modules/settings/pages/settings.vue @@ -0,0 +1,20 @@ + + + diff --git a/apps/client/src/modules/templates/__tests__/TemplateItem.spec.ts b/apps/client/src/modules/templates/__tests__/TemplateItem.spec.ts new file mode 100644 index 00000000..89a2d61c --- /dev/null +++ b/apps/client/src/modules/templates/__tests__/TemplateItem.spec.ts @@ -0,0 +1,28 @@ +import { shallowMount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import { templatesInfo } from '../constants/templates' +import TemplateItem from '../components/TemplateItem.vue' +import i18n from '@/shared/libs/i18n' + +describe('tests for TemplateItem.vue', () => { + const wrapper = shallowMount(TemplateItem, { + global: { + plugins: [i18n], + mocks: { + t: (key: string) => { + const translations: Record = { + 'templates.user': 'test user', + } + return translations[key] + }, + }, + }, + props: { + template: templatesInfo[0], + }, + }) + + it('should render correctly', () => { + expect(wrapper.html()).toMatchSnapshot() + }) +}) diff --git a/apps/client/src/modules/templates/__tests__/__snapshots__/TemplateItem.spec.ts.snap b/apps/client/src/modules/templates/__tests__/__snapshots__/TemplateItem.spec.ts.snap new file mode 100644 index 00000000..6533970c --- /dev/null +++ b/apps/client/src/modules/templates/__tests__/__snapshots__/TemplateItem.spec.ts.snap @@ -0,0 +1,13 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`tests for TemplateItem.vue > should render correctly 1`] = ` +"
+
+ +
+

Base Kanban

Create a basic project with "Base Kanban" template +
+
June 12, 2024
+
+
" +`; diff --git a/apps/client/src/modules/templates/components/AllTemplates.vue b/apps/client/src/modules/templates/components/AllTemplates.vue new file mode 100644 index 00000000..e7c03a2e --- /dev/null +++ b/apps/client/src/modules/templates/components/AllTemplates.vue @@ -0,0 +1,39 @@ + + + diff --git a/apps/client/src/modules/templates/components/ImportTemplate.vue b/apps/client/src/modules/templates/components/ImportTemplate.vue new file mode 100644 index 00000000..099e0734 --- /dev/null +++ b/apps/client/src/modules/templates/components/ImportTemplate.vue @@ -0,0 +1,12 @@ + diff --git a/apps/client/src/modules/templates/components/TemplateItem.vue b/apps/client/src/modules/templates/components/TemplateItem.vue new file mode 100644 index 00000000..10092c8a --- /dev/null +++ b/apps/client/src/modules/templates/components/TemplateItem.vue @@ -0,0 +1,43 @@ + + + diff --git a/apps/client/src/modules/templates/constants/templates.ts b/apps/client/src/modules/templates/constants/templates.ts new file mode 100644 index 00000000..b0f86d4d --- /dev/null +++ b/apps/client/src/modules/templates/constants/templates.ts @@ -0,0 +1,29 @@ +import type { Template } from '../types' + +// mocks -> after data from backend +export const templatesInfo: Template[] = [ + { + id: '0', + title: 'Base Kanban', + tag: 'Recommended', + description: `Create a basic project with "Base Kanban" template`, + date: 'June 12, 2024', + user: 'https://avatars.githubusercontent.com/u/121057011?v=4', + }, + { + id: '1', + title: 'Roadmap', + tag: 'New', + description: `Create a project based on our "Roadmap" template`, + date: 'June 18, 2024', + user: 'https://avatars.githubusercontent.com/u/121057011?v=4', + }, + { + id: '2', + title: 'Web Development', + tag: 'New', + description: `Create a basic project with "Web Development" template`, + date: 'July 16, 2024', + user: 'https://avatars.githubusercontent.com/u/121057011?v=4', + }, +] diff --git a/apps/client/src/modules/templates/pages/templates.vue b/apps/client/src/modules/templates/pages/templates.vue new file mode 100644 index 00000000..77b0b32e --- /dev/null +++ b/apps/client/src/modules/templates/pages/templates.vue @@ -0,0 +1,42 @@ + + + diff --git a/apps/client/src/modules/templates/pages/templates/community.vue b/apps/client/src/modules/templates/pages/templates/community.vue new file mode 100644 index 00000000..2a092c67 --- /dev/null +++ b/apps/client/src/modules/templates/pages/templates/community.vue @@ -0,0 +1,14 @@ + + + diff --git a/apps/client/src/modules/templates/types/index.ts b/apps/client/src/modules/templates/types/index.ts new file mode 100644 index 00000000..b1b7628b --- /dev/null +++ b/apps/client/src/modules/templates/types/index.ts @@ -0,0 +1,8 @@ +export interface Template { + id: string + title: string + tag: string + description: string + date: string + user: string +} diff --git a/apps/client/src/modules/welcome/__tests__/AboutSection.spec.ts b/apps/client/src/modules/welcome/__tests__/AboutSection.spec.ts new file mode 100644 index 00000000..eb5ea15d --- /dev/null +++ b/apps/client/src/modules/welcome/__tests__/AboutSection.spec.ts @@ -0,0 +1,31 @@ +import { shallowMount } from '@vue/test-utils' +import { describe, expect, it, vi } from 'vitest' +import AboutSection from '../components/sections/AboutSection.vue' +import { UiButton } from '@/shared/ui' +import i18n from '@/shared/libs/i18n' + +const mockRouter = { + push: vi.fn(), + beforeEach: vi.fn(), +} + +describe('tests for AboutSection.vue', () => { + const wrapper = shallowMount(AboutSection, { + global: { + plugins: [i18n], + mocks: { + $router: mockRouter, + }, + }, + }) + + it('should be render correctly', () => { + expect(wrapper.html()).toMatchSnapshot() + }) + + it('should redirect correctly', async () => { + const btn = wrapper.find('.btns').findAllComponents(UiButton).at(0) + await btn.trigger('click') + expect(mockRouter.push).toHaveBeenCalledWith({ name: 'sign-up' }) + }) +}) diff --git a/apps/client/src/modules/welcome/__tests__/MarketingCards.spec.ts b/apps/client/src/modules/welcome/__tests__/MarketingCards.spec.ts new file mode 100644 index 00000000..2ce393e3 --- /dev/null +++ b/apps/client/src/modules/welcome/__tests__/MarketingCards.spec.ts @@ -0,0 +1,65 @@ +import { type ComponentPublicInstance, nextTick } from 'vue' +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import MarketingCards from '../components/MarketingCards.vue' +import type { VueWrapper } from '@vue/test-utils' +import i18n from '@/shared/libs/i18n' +import { UiCard } from '@/shared/ui' + +// TODO: `ComponentPublicInstance` needs to be fixed with certain types +type MarketingCardsInstance = ComponentPublicInstance< + {}, + {}, + { + cards: any[] + width: number + getCurrentColor: (id: number) => string + currentWidth: (width: string | undefined) => string + currentHeight: (height: string | undefined) => string + } +> + +describe('tests for MarketingCards.vue', () => { + const wrapper = mount(MarketingCards, { + global: { + plugins: [i18n], + }, + }) as VueWrapper + + it('should be render correctly', () => { + expect(wrapper.html()).toMatchSnapshot() + }) + + it('should render subcomponents', () => { + expect(wrapper.findComponent(UiCard).exists()).toBe(true) + }) + + it('should count cards correctly', async () => { + const cards = wrapper.vm.cards + wrapper.vm.width = 1900 + await nextTick() + expect(cards).toHaveLength(5) + }) + + it('returns correct width and height', () => { + expect(wrapper.vm.currentWidth('500')).toBe('500px') + expect(wrapper.vm.currentWidth(undefined)).toBe('100%') + expect(wrapper.vm.currentHeight('100')).toBe('100px') + expect(wrapper.vm.currentHeight(undefined)).toBe('100%') + }) + + it('renders card titles and descriptions correctly', () => { + const _titles = wrapper.findAll('h3') + const _descriptions = wrapper.findAll('p') + + expect(_titles.length).toBe(5) + expect(_descriptions.length).toBe(5) + + _titles.forEach((title, index) => { + expect(title.text()).toBe(wrapper.vm.cards[index].title) + }) + _descriptions.forEach((desc, index) => { + expect(desc.text()).toBe(wrapper.vm.cards[index].description) + }) + }) +}) diff --git a/apps/client/src/modules/welcome/__tests__/__snapshots__/AboutSection.spec.ts.snap b/apps/client/src/modules/welcome/__tests__/__snapshots__/AboutSection.spec.ts.snap new file mode 100644 index 00000000..1ed21b14 --- /dev/null +++ b/apps/client/src/modules/welcome/__tests__/__snapshots__/AboutSection.spec.ts.snap @@ -0,0 +1,13 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`tests for AboutSection.vue > should be render correctly 1`] = ` +"
+
Core: Task Boards, Real-Time Tracking
+

Your path to perfection

+

Jenda is a cloud-based program for efficient collaborative and individual project and task management.

+
+ + +
+
" +`; diff --git a/apps/client/src/modules/welcome/__tests__/__snapshots__/MarketingCards.spec.ts.snap b/apps/client/src/modules/welcome/__tests__/__snapshots__/MarketingCards.spec.ts.snap new file mode 100644 index 00000000..3ac86d5f --- /dev/null +++ b/apps/client/src/modules/welcome/__tests__/__snapshots__/MarketingCards.spec.ts.snap @@ -0,0 +1,44 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`tests for MarketingCards.vue > should be render correctly 1`] = ` +"
+
+

All convenience in one place

+
+
+
+
+
+

Kanban 🧑‍💻

+

Organize and visualize your projects with our intuitive Kanban board. Enhance productivity with ease.

+
+
+
+
+
+

Collaborative 👥

+

Improve your teamwork!

+
+
+
+
+
+
+
+

Activity 👔

+

Analyze your activity and move forward.

+
+
+
+
+
+

Chats 💬

+

Communicate, share, discuss. Our convenience for you 😉

+
+
+
+
+ +
+
" +`; diff --git a/apps/client/src/modules/welcome/components/MarketingCards.vue b/apps/client/src/modules/welcome/components/MarketingCards.vue new file mode 100644 index 00000000..7c07ce2c --- /dev/null +++ b/apps/client/src/modules/welcome/components/MarketingCards.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/apps/client/src/modules/welcome/components/MarqueeList.vue b/apps/client/src/modules/welcome/components/MarqueeList.vue new file mode 100644 index 00000000..1db99f74 --- /dev/null +++ b/apps/client/src/modules/welcome/components/MarqueeList.vue @@ -0,0 +1,15 @@ + diff --git a/apps/client/src/modules/welcome/components/ProjectDemo.vue b/apps/client/src/modules/welcome/components/ProjectDemo.vue new file mode 100644 index 00000000..5247af27 --- /dev/null +++ b/apps/client/src/modules/welcome/components/ProjectDemo.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/apps/client/src/modules/welcome/components/reviews/ReviewWrapper.vue b/apps/client/src/modules/welcome/components/reviews/ReviewWrapper.vue new file mode 100644 index 00000000..d5b4d9fd --- /dev/null +++ b/apps/client/src/modules/welcome/components/reviews/ReviewWrapper.vue @@ -0,0 +1,29 @@ + + + diff --git a/apps/client/src/modules/welcome/components/sections/AboutSection.vue b/apps/client/src/modules/welcome/components/sections/AboutSection.vue new file mode 100644 index 00000000..88b87673 --- /dev/null +++ b/apps/client/src/modules/welcome/components/sections/AboutSection.vue @@ -0,0 +1,79 @@ + + + diff --git a/apps/client/src/modules/welcome/components/sections/ActivitySection.vue b/apps/client/src/modules/welcome/components/sections/ActivitySection.vue new file mode 100644 index 00000000..d2c16831 --- /dev/null +++ b/apps/client/src/modules/welcome/components/sections/ActivitySection.vue @@ -0,0 +1,56 @@ + + + diff --git a/apps/client/src/modules/welcome/components/sections/ChatsSection.vue b/apps/client/src/modules/welcome/components/sections/ChatsSection.vue new file mode 100644 index 00000000..d01411c7 --- /dev/null +++ b/apps/client/src/modules/welcome/components/sections/ChatsSection.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/client/src/modules/welcome/components/sections/CollaborativeSection.vue b/apps/client/src/modules/welcome/components/sections/CollaborativeSection.vue new file mode 100644 index 00000000..39bee09f --- /dev/null +++ b/apps/client/src/modules/welcome/components/sections/CollaborativeSection.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/client/src/modules/welcome/components/sections/KanbanSection.vue b/apps/client/src/modules/welcome/components/sections/KanbanSection.vue new file mode 100644 index 00000000..5833abe3 --- /dev/null +++ b/apps/client/src/modules/welcome/components/sections/KanbanSection.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/client/src/modules/welcome/components/sections/MembersSection.vue b/apps/client/src/modules/welcome/components/sections/MembersSection.vue new file mode 100644 index 00000000..4f7f06e6 --- /dev/null +++ b/apps/client/src/modules/welcome/components/sections/MembersSection.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/client/src/modules/welcome/components/sections/SectionWrapper.vue b/apps/client/src/modules/welcome/components/sections/SectionWrapper.vue new file mode 100644 index 00000000..c7fb5845 --- /dev/null +++ b/apps/client/src/modules/welcome/components/sections/SectionWrapper.vue @@ -0,0 +1,78 @@ + + + diff --git a/apps/client/src/modules/welcome/composables/cards.ts b/apps/client/src/modules/welcome/composables/cards.ts new file mode 100644 index 00000000..695afcfa --- /dev/null +++ b/apps/client/src/modules/welcome/composables/cards.ts @@ -0,0 +1,23 @@ +import { computed, triggerRef, watch } from 'vue' +import { useDark } from '@vueuse/core' +import { cardsInfo as cards } from '../constants/cards' + +export function useCards() { + const isDark = useDark() + const urlImg = computed(() => + isDark.value ? '/dev/dev-card-dark.png' : '/dev/dev-card.png', + ) + + watch( + isDark, + () => { + cards.value.forEach(c => (c.url = urlImg.value)) + triggerRef(cards) + }, + { immediate: true, flush: 'sync' }, + ) + + return { + cards, + } +} diff --git a/apps/client/src/modules/welcome/composables/sections.ts b/apps/client/src/modules/welcome/composables/sections.ts new file mode 100644 index 00000000..a5cd438a --- /dev/null +++ b/apps/client/src/modules/welcome/composables/sections.ts @@ -0,0 +1,42 @@ +import { computed, type MaybeRefOrGetter } from 'vue' +import { useDark } from '@vueuse/core' +import { useI18n } from 'vue-i18n' +import type { SectionWrapperType } from '../types' + +export function useSection( + _section: MaybeRefOrGetter, + imgPrefix: string, + userImg: string, +) { + const { t } = useI18n() + const isDark = useDark() + + const img = computed(() => + isDark.value + ? `/dev/${imgPrefix}-section-dark.png` + : `/dev/${imgPrefix}-section.png`, + ) + + const parentKeys = ['name', 'heading', 'about', 'writer'] as const + const childReviewKeys = ['name', 'status', 'comment'] as const + + const section = computed(() => { + return { + ...(Object.fromEntries( + parentKeys.map(key => [key, t(`welcome.${_section}.${key}`)]), + ) as Pick), + img: img.value, + review: { + ...Object.fromEntries( + childReviewKeys.map(key => [ + key, + t(`welcome.${_section}.review.${key}`), + ]), + ), + img: userImg, + }, + } + }) + + return { section } +} diff --git a/apps/client/src/modules/welcome/constants/cards.ts b/apps/client/src/modules/welcome/constants/cards.ts new file mode 100644 index 00000000..ebf8dfcf --- /dev/null +++ b/apps/client/src/modules/welcome/constants/cards.ts @@ -0,0 +1,57 @@ +import { shallowRef } from 'vue' +import type { MarketingCard } from '../types' + +export const cardsInfo = shallowRef([ + { + id: 0, + pagePrefix: 'kanban', + height: '270', + width: '640', + // img + bottom: '-260px', + right: '-360px', + imgWidth: '720px', + maxWidth: '400px', + }, + { + id: 1, + pagePrefix: 'collaborative', + height: '300', + width: '640', + // img + bottom: '-160px', + right: '20px', + imgWidth: '600px', + maxWidth: '360px', + }, + { + id: 2, + pagePrefix: 'members', + width: '500', + height: '460', + // img + bottom: '-230px', + right: '-290px', + imgWidth: '600px', + maxWidth: '240px', + }, + { + id: 3, + pagePrefix: 'activity', + width: '500', + // img + bottom: '-200px', + right: '-500px', + imgWidth: '700px', + maxWidth: '300px', + }, + { + id: 4, + pagePrefix: 'chats', + // img + bottom: '0px', + right: '-420px', + imgWidth: '700px', + maxWidth: '300px', + }, +]) diff --git a/apps/client/src/modules/welcome/types/index.ts b/apps/client/src/modules/welcome/types/index.ts new file mode 100644 index 00000000..c4c5340d --- /dev/null +++ b/apps/client/src/modules/welcome/types/index.ts @@ -0,0 +1,34 @@ +export interface MarketingCard extends ImgInCard { + id: number + title?: string + description?: string + width?: string + height?: string + pagePrefix: string + maxWidth?: string +} + +interface ImgInCard { + url?: string + bottom?: string + right?: string + left?: string + top?: string + imgWidth?: string +} + +export interface SectionWrapperType { + name: string + heading: string + about: string + writer: string + img: string + review?: Partial +} + +export interface ReviewWrapper { + img: string + name: string + status: string + comment: string +} diff --git a/apps/client/src/modules/workspace/components/ChooseImg.vue b/apps/client/src/modules/workspace/components/ChooseImg.vue new file mode 100644 index 00000000..4b0cef35 --- /dev/null +++ b/apps/client/src/modules/workspace/components/ChooseImg.vue @@ -0,0 +1,12 @@ + + + diff --git a/apps/client/src/modules/workspace/components/ChoosingButton.vue b/apps/client/src/modules/workspace/components/ChoosingButton.vue new file mode 100644 index 00000000..3d785290 --- /dev/null +++ b/apps/client/src/modules/workspace/components/ChoosingButton.vue @@ -0,0 +1,30 @@ + + + diff --git a/apps/client/src/modules/workspace/components/ShareLink.vue b/apps/client/src/modules/workspace/components/ShareLink.vue new file mode 100644 index 00000000..bcef5a8d --- /dev/null +++ b/apps/client/src/modules/workspace/components/ShareLink.vue @@ -0,0 +1,32 @@ + + + diff --git a/apps/client/src/modules/workspace/components/WorkspaceMenu.vue b/apps/client/src/modules/workspace/components/WorkspaceMenu.vue new file mode 100644 index 00000000..c10fdd9a --- /dev/null +++ b/apps/client/src/modules/workspace/components/WorkspaceMenu.vue @@ -0,0 +1,95 @@ + + + diff --git a/apps/client/src/modules/workspace/stores/workspace.ts b/apps/client/src/modules/workspace/stores/workspace.ts new file mode 100644 index 00000000..304d2546 --- /dev/null +++ b/apps/client/src/modules/workspace/stores/workspace.ts @@ -0,0 +1,19 @@ +import { ref } from 'vue' +import { defineStore } from 'pinia' +import type { BaseTestWorkspace } from '../types' + +export const useWorkspaceStore = defineStore('workspace', () => { + const workspace = ref({ + _id: '0', + name: 'Example.io', + img: 'https://avatars.githubusercontent.com/u/185750893?s=100&v=4', + link: 'https://jenda-app-mnenie.com/example.io', + status: 'active', + members: [], + plan: 'FREE', + }) + + return { + workspace, + } +}) diff --git a/apps/client/src/modules/workspace/types/index.ts b/apps/client/src/modules/workspace/types/index.ts new file mode 100644 index 00000000..ef2f9645 --- /dev/null +++ b/apps/client/src/modules/workspace/types/index.ts @@ -0,0 +1,13 @@ +import type { User } from "@/modules/auth/types" + +export interface BaseTestWorkspace { + _id: string + name: string + img: string | Blob + link: string + status: 'active' | 'archive' + members: User[] + plan: 'FREE' | 'PREMIUM' +} + +export type ChoosingWorkspaceItem = Pick diff --git a/apps/client/src/plugins/formkit.ts b/apps/client/src/plugins/formkit.ts new file mode 100644 index 00000000..498723b6 --- /dev/null +++ b/apps/client/src/plugins/formkit.ts @@ -0,0 +1,46 @@ +import autoAnimate from '@formkit/auto-animate' +import type { AutoAnimateOptions } from '@formkit/auto-animate' +import type { App } from 'vue' + +export default { + install: (app: App, options?: Partial) => { + app.directive('auto-animate', { + mounted(el, binding) { + const config: Partial = { + duration: binding.value?.duration || 200, + easing: binding.value?.easing || 'ease-in', + disrespectUserMotionPreference: false, + ...binding.value, + } + autoAnimate(el, (el, action, oldCoords, newCoords) => { + let keyframes: Keyframe[] + if (action === 'add') { + keyframes = [ + { transform: 'translateY(100%)', opacity: 0 }, + { transform: 'translateY(0)', opacity: 1 }, + ] + } + if (action === 'remain') { + const deltaY = oldCoords!.top - newCoords!.top + + const start = { transform: `translate(0, ${deltaY}px)` } + const mid = { + transform: `translate(0, ${deltaY * 0.4}px)`, + offset: 0.7, + } + const end = { transform: `translate(0, 0)` } + + keyframes = [start, mid, end] + } + if (action === 'remove') { + keyframes = [ + { transform: 'translateY(0)', opacity: 1 }, + { transform: 'translateY(-100%)', opacity: 0 }, + ] + } + return new KeyframeEffect(el, keyframes!, config) + }) + }, + }) + }, +} diff --git a/apps/client/src/router/guards/index.ts b/apps/client/src/router/guards/index.ts new file mode 100644 index 00000000..16275878 --- /dev/null +++ b/apps/client/src/router/guards/index.ts @@ -0,0 +1 @@ +export * from './layoutResolver' diff --git a/apps/client/src/router/guards/layoutResolver.ts b/apps/client/src/router/guards/layoutResolver.ts new file mode 100644 index 00000000..63855ab1 --- /dev/null +++ b/apps/client/src/router/guards/layoutResolver.ts @@ -0,0 +1,11 @@ +import type { RouteLocationNormalized } from 'vue-router/auto' +import { LayoutToFileMap } from '@/core/constants/layouts' +import { LayoutsEnum } from '@/shared/constants/layouts' + +export async function layoutResolverGuard(route: RouteLocationNormalized) { + const { layout } = route.meta + const normalizedLayoutName = layout as LayoutsEnum || LayoutsEnum.default + const fileName = LayoutToFileMap[normalizedLayoutName].split('.vue')[0] + const component = await import(`@/core/layouts/${fileName}.vue`) + route.meta.layoutComponent = component.default +} diff --git a/apps/client/src/router/index.ts b/apps/client/src/router/index.ts new file mode 100644 index 00000000..c74f77a3 --- /dev/null +++ b/apps/client/src/router/index.ts @@ -0,0 +1,22 @@ +import { createRouter, createWebHistory } from 'vue-router/auto' +import { handleHotUpdate, routes } from 'vue-router/auto-routes' +import { layoutResolverGuard } from './guards' + +export const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + // resolved: https://github.com/mnenie/jenda/issues/56 + routes, +}) + +router.beforeEach((to, from) => { + // Needs to add guard auth logic in router + // if (to.meta.requiresAuth === true) { + // return router.push({ name: 'sign-in' }) + // } +}) + +router.beforeEach(layoutResolverGuard) + +if (import.meta.hot) { + handleHotUpdate(router) +} diff --git a/apps/client/src/router/types.ts b/apps/client/src/router/types.ts new file mode 100644 index 00000000..e9279f2d --- /dev/null +++ b/apps/client/src/router/types.ts @@ -0,0 +1,15 @@ +import type { RouteRecordRaw } from 'vue-router' +import type { LayoutsEnum } from '@/shared/constants/layouts' +import 'vue-router' + +declare module 'vue-router' { + interface RouteMeta { + layout?: LayoutsEnum + requiresAuth?: boolean + } +} + +export type RouterRecord = Omit & { + name: string + children?: RouterRecord[] +} diff --git a/apps/client/src/shared/assets/fonts/geist/GeistVariableVF.woff2 b/apps/client/src/shared/assets/fonts/geist/GeistVariableVF.woff2 new file mode 100644 index 00000000..328a020b Binary files /dev/null and b/apps/client/src/shared/assets/fonts/geist/GeistVariableVF.woff2 differ diff --git a/apps/client/src/shared/assets/icons/ProjectGrid.vue b/apps/client/src/shared/assets/icons/ProjectGrid.vue new file mode 100644 index 00000000..6d099df4 --- /dev/null +++ b/apps/client/src/shared/assets/icons/ProjectGrid.vue @@ -0,0 +1,281 @@ + + + diff --git a/apps/client/src/shared/assets/icons/custom-jenda/import.svg b/apps/client/src/shared/assets/icons/custom-jenda/import.svg new file mode 100644 index 00000000..c66a3e26 --- /dev/null +++ b/apps/client/src/shared/assets/icons/custom-jenda/import.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/client/src/shared/assets/icons/custom-jenda/project.svg b/apps/client/src/shared/assets/icons/custom-jenda/project.svg new file mode 100644 index 00000000..893fe35b --- /dev/null +++ b/apps/client/src/shared/assets/icons/custom-jenda/project.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/client/src/shared/assets/icons/index.ts b/apps/client/src/shared/assets/icons/index.ts new file mode 100644 index 00000000..e79b92a0 --- /dev/null +++ b/apps/client/src/shared/assets/icons/index.ts @@ -0,0 +1,3 @@ +import ProjectGrid from './ProjectGrid.vue' + +export { ProjectGrid } diff --git a/apps/client/src/shared/composables/breakpoints.ts b/apps/client/src/shared/composables/breakpoints.ts new file mode 100644 index 00000000..9e2dccca --- /dev/null +++ b/apps/client/src/shared/composables/breakpoints.ts @@ -0,0 +1,24 @@ +import { + type Breakpoints, + breakpointsSematic, + useBreakpoints as VueUseBreakpoints, +} from '@vueuse/core' + +interface CustomBreakpoints extends Breakpoints { + intermediateDesktop: string + intermediateLaptop: string +} + +export function useBreakpoints() { + const breakpoints: CustomBreakpoints = { + ...breakpointsSematic, + intermediateDesktop: '1820px', + intermediateLaptop: '1600px', + } + + const breakpointsInstance = VueUseBreakpoints(breakpoints) + + return { + breakpoints: breakpointsInstance, + } +} diff --git a/apps/client/src/shared/composables/expanded.ts b/apps/client/src/shared/composables/expanded.ts new file mode 100644 index 00000000..f0ee0b0c --- /dev/null +++ b/apps/client/src/shared/composables/expanded.ts @@ -0,0 +1,30 @@ +import { inject, type InjectionKey, provide, type Ref } from 'vue' + +interface Expanded { + isExpanded: Ref + onToggleArea: () => void +} + +type T = Expanded + +const key: InjectionKey = Symbol('expanded') + +export function provideExpandedContext(value: T) { + provide(key, value) + return value +} + +export function useExpandedContext< + U extends T | undefined = T, +>( + fallback?: U, +): U extends null ? T | null : T { + const expanded = inject(key, fallback) + if (expanded) + return expanded as T + + if (expanded === null) + return expanded as any + + throw new Error('not provided') +} diff --git a/apps/client/src/shared/composables/language.ts b/apps/client/src/shared/composables/language.ts new file mode 100644 index 00000000..dd442f2d --- /dev/null +++ b/apps/client/src/shared/composables/language.ts @@ -0,0 +1,33 @@ +import { type Ref, watch } from 'vue' +import { useLocalStorage } from '@vueuse/core' +import { useI18n } from 'vue-i18n' + +interface Value { + name: string + value: string +} + +export function useLanguage(values: Value[], language: Ref) { + const { locale } = useI18n() + const storage = useLocalStorage('i18n', null) + + const setLanguage = (value: string) => { + locale.value = value + storage.value = value + } + + watch( + () => locale.value, + (newLocale) => { + const selectedOption = values.find(option => option.value === newLocale) + if (selectedOption) { + language.value = selectedOption.value + } + }, + { immediate: true }, + ) + + return { + setLanguage, + } +} diff --git a/apps/client/src/shared/composables/layout-paths.ts b/apps/client/src/shared/composables/layout-paths.ts new file mode 100644 index 00000000..39f67feb --- /dev/null +++ b/apps/client/src/shared/composables/layout-paths.ts @@ -0,0 +1,66 @@ +import { computed, type MaybeRefOrGetter, toValue } from 'vue' +import { useI18n } from 'vue-i18n' +import { type RouterLinkProps, useRoute } from 'vue-router/auto' +import type { + CombinedLink, + ProjectLink, + WorkspaceLink, +} from '@/shared/config/types-shared' + +export function useLayoutPaths( + links: MaybeRefOrGetter, + projects: MaybeRefOrGetter, +) { + const route = useRoute() + const { t } = useI18n() + + const combinedArr = computed(() => { + return [ + ...toValue(links).map(link => ({ + ...link, + name: t(`sidebar.${link.name}`), + icon: link.icon, + })), + ...toValue(projects).map(project => ({ + _id: project._id, + name: project.name, + pathName: `/boards/${project._id}`, + color: project.color, + })), + ] + }) + function isProject( + item: CombinedLink, + ): item is ProjectLink & { pathName: RouterLinkProps['to'] } { + return '_id' in item + } + + function isCurrentPath(item: CombinedLink): boolean { + if (isProject(item)) { + return route.path === `/boards/${item._id}` + } + if (route.name === 'boards-new') { + return item.pathName === 'boards' + } + if (route.name === 'community') { + return item.pathName === 'templates' + } + return route.path === `/${item.pathName}` + } + + const active = computed(() => { + const activeItem = toValue(combinedArr).find(item => isCurrentPath(item)) + return { + ...activeItem, + name: activeItem!.name, + extendedAttrs: { + color: 'color' in activeItem! ? activeItem.color : undefined, + icon: 'icon' in activeItem! ? activeItem.icon : undefined, + }, + } + }) + + return { + active, + } +} diff --git a/apps/client/src/shared/composables/scroll.ts b/apps/client/src/shared/composables/scroll.ts new file mode 100644 index 00000000..9dceff03 --- /dev/null +++ b/apps/client/src/shared/composables/scroll.ts @@ -0,0 +1,19 @@ +import { nextTick } from 'vue' + +export function useScroll(arr: T) { + const keys = [...arr] as const + + const scrollToEl = async (key: (typeof keys)[number]) => { + const el = document.getElementById(key) + if (!el) { + throw new Error(`${key} is undefined`) + } + await nextTick(() => { + el.scrollIntoView({ block: 'center', behavior: 'smooth' }) + }) + } + + return { + scrollToEl, + } +} diff --git a/apps/client/src/shared/config/shared-types.ts b/apps/client/src/shared/config/shared-types.ts new file mode 100644 index 00000000..915fe5ff --- /dev/null +++ b/apps/client/src/shared/config/shared-types.ts @@ -0,0 +1,18 @@ +import type { IconifyIcon } from '@iconify/vue' + +interface Path { + pathName: string +} + +export interface WorkspaceLink extends Path { + id: number + name: string + icon: IconifyIcon | string +} +export interface ProjectLink extends Path { + _id: string + name: string + color?: string +} + +export type CombinedLink = WorkspaceLink | ProjectLink diff --git a/apps/client/src/shared/constants/layouts.ts b/apps/client/src/shared/constants/layouts.ts new file mode 100644 index 00000000..014c2266 --- /dev/null +++ b/apps/client/src/shared/constants/layouts.ts @@ -0,0 +1,6 @@ +export enum LayoutsEnum { + default = 'default', + auth = 'auth', + welcome = 'welcome', + empty = 'empty', +} diff --git a/apps/client/src/shared/constants/links.ts b/apps/client/src/shared/constants/links.ts new file mode 100644 index 00000000..b4d0cbf7 --- /dev/null +++ b/apps/client/src/shared/constants/links.ts @@ -0,0 +1,20 @@ +import type { WorkspaceLink } from '../config/shared-types' + +export const links: WorkspaceLink[] = [ + { id: 0, name: 'boards', pathName: 'boards', icon: 'hugeicons:trello' }, + { id: 1, name: 'notes', pathName: 'notes', icon: 'hugeicons:checkmark-square-03' }, + { + id: 2, + name: 'templates', + pathName: 'templates', + icon: 'hugeicons:dashboard-square-add', + }, + { + id: 3, + name: 'analytics', + pathName: 'analytics', + icon: 'hugeicons:analytics-01', + }, + { id: 4, name: 'members', pathName: 'members', icon: 'hugeicons:user-multiple-02' }, + { id: 5, name: 'settings', pathName: 'settings', icon: 'hugeicons:setting-07' }, +] diff --git a/apps/client/src/shared/helpers/helperColor.ts b/apps/client/src/shared/helpers/helperColor.ts new file mode 100644 index 00000000..9908a53e --- /dev/null +++ b/apps/client/src/shared/helpers/helperColor.ts @@ -0,0 +1,11 @@ +export function formatLabelColor(hex: string, amount: number): string { + let color = hex.replace('#', '') + if (color.length === 3) { + color = color.split('').map(c => c + c).join('') + } + const r = Math.max(0, Math.min(255, Number.parseInt(color.slice(0, 2), 16) - amount)) + const g = Math.max(0, Math.min(255, Number.parseInt(color.slice(2, 4), 16) - amount)) + const b = Math.max(0, Math.min(255, Number.parseInt(color.slice(4, 6), 16) - amount)) + + return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}` +} diff --git a/apps/client/src/shared/helpers/redirectBlank.ts b/apps/client/src/shared/helpers/redirectBlank.ts new file mode 100644 index 00000000..3b043a40 --- /dev/null +++ b/apps/client/src/shared/helpers/redirectBlank.ts @@ -0,0 +1,3 @@ +export function redirect(url: string) { + window.open(url, '_blank') +} diff --git a/apps/client/src/shared/libs/i18n/index.ts b/apps/client/src/shared/libs/i18n/index.ts new file mode 100644 index 00000000..50d6465b --- /dev/null +++ b/apps/client/src/shared/libs/i18n/index.ts @@ -0,0 +1,54 @@ +import { computed } from 'vue' +import { createI18n } from 'vue-i18n' +import { enZod, ruZod, zhZod } from '../vee-validate/rules' +import { enLocale, ruLocale, zhLocale } from './locales' +import { customPluralRule } from './plurals' + +export type MessageSchema = typeof enLocale + +const messages = { + 'en-US': { + ...enLocale, + errors: { + ...enZod, + }, + }, + 'ru-RU': { + ...ruLocale, + errors: { + ...ruZod, + }, + }, + 'zh-CN': { + ...zhLocale, + errors: { + ...zhZod, + }, + }, +} + +const getCurrentLocale = computed(() => { + const storageLanguage = localStorage.getItem('i18n') + if (storageLanguage) { + return storageLanguage + } + if (navigator.language.split('-')[0] === 'ru') { + return 'ru-RU' + } + return 'en-US' +}) + +const i18n = createI18n<[MessageSchema], 'en-US' | 'ru-RU' | 'zh-CN'>({ + legacy: false, + locale: getCurrentLocale.value, + globalInjection: true, + pluralRules: { + 'ru-RU': { + // @ts-expect-error i18n shema + customPluralRule, + }, + }, + messages, +}) + +export default i18n diff --git a/apps/client/src/shared/libs/i18n/locales/en-US.ts b/apps/client/src/shared/libs/i18n/locales/en-US.ts new file mode 100644 index 00000000..7dd5df15 --- /dev/null +++ b/apps/client/src/shared/libs/i18n/locales/en-US.ts @@ -0,0 +1,528 @@ +export default { + sidebar: { + badge: 'free', + input: 'Search', + section: 'workspace', + projects: 'projects', + boards: 'Boards', + notes: 'Notes', + templates: 'Templates', + analytics: 'Analytics', + members: 'Members', + settings: 'Settings', + integrations: 'Integraions', + help: { + title: 'Help', + items: 'Support developers | Keyboard shortcuts | Join us', + }, + plan: { + btn: 'Buy a subscription', + title: 'Premium Plan', + description: + 'boards left', + tooltip: '1/3 free workspaces', + }, + soon: 'soon', + }, + header: { + navigator: { + question: 'Hotkeys', + messages: 'Messages', + }, + user: { + welcome: 'Go to Welcome', + logout: 'Log out', + }, + share: 'Share link', + }, + boards: { + create: 'Add board | Add new board', + filters: { + sort: { + title: 'Sort by', + name: 'Name', + tasks: 'Tasks', + date: 'Creation date', + default: 'Default', + }, + advanced: { + title: 'Advanced filter', + empty: 'No such filters available :/', + statuses: { + title: 'Statuses', + arr: [ + { + value: 'active', + label: 'Active', + }, + { + value: 'archived', + label: 'Archived', + }, + ], + }, + labels: { + title: 'Labels', + }, + }, + search: 'Find a board...', + }, + forms: { + creating: { + title: 'Creating a board', + description: 'You can create a new board to manage tasks', + name: { + label: 'Name', + placeholder: 'Enter the name of the board', + }, + labels: { + label: 'Labels', + placeholder: 'Enter a label', + description: 'Start typing a label and press \'Enter\' to save it to your existing labels', + alert: '📝 How to change the label color? Specify the label name followed by [#desired color]. For example: jenda [#000000].', + }, + btns: 'Cancel | Create', + }, + }, + empty: { + title: 'No board', + description: 'Create your first board and start working', + }, + card: { + date_updated: 'Updated', + }, + chart: { + title_boards: 'Сreated boards', + description_boards: + 'Create, share, work, and then analyze your activity on Jenda', + title_tasks: 'Your tasks', + description_tasks: 'Solve and complete the tasks you started', + }, + columns: ['Kanban name', 'Status', 'Labels', 'Participants', 'Tasks', 'Estimate', 'Creation date'], + }, + picker: { + placeholder: 'Custom color', + tabs: 'Solid | Gradients', + }, + templates: { + title: 'Templates', + description: 'Choose one of the available templates to create your project', + import: 'Click or drag files to import. Supports files', + community: 'Comunity templates', + create: 'Create new template', + user: 'creator', + items: [ + { + title: 'Base Kanban', + tag: 'Recommended', + description: 'Create a basic project with "Base Kanban" template', + date: 'June 12, 2024', + }, + { + title: 'Roadmap', + tag: 'New', + description: 'Create a project based on our "Roadmap" special template', + date: 'June 18, 2024', + }, + { + title: 'Web Development', + tag: 'New', + description: 'Create a basic project with "Web Development" template', + date: 'July 16, 2024', + }, + ], + }, + members: { + title: 'Workspace participants', + description: + 'Members can view and join whiteboards for the workspace, as well as create new whiteboards in that space.', + content: { + all: { + title: 'Invite users', + description: + 'Anyone who has an invitation link can join this free workspace. The link can be disabled and recreated at any time. Pending invitations count towards a limit of 10 participants.', + btn: 'All participants', + }, + guests: { + title: 'Guests', + description: + 'Guests can only view and edit the boards they have been added to.', + btn: 'Guests', + }, + }, + role: { + member: 'member', + admin: 'admin', + }, + btns: { + leave: 'Leave', + view: 'View the boards', + }, + }, + analytics: { + title: 'Workspace Analytics', + description: 'View and analyze your activity on Jenda', + charts: { + users: { + name: 'Total users', + description: 'Number of users in the workspace', + }, + online: { + name: 'Workspace activity', + }, + tasks: { + name: 'Total tasks', + dataset: { + names: ['Uncompleted', 'Completed'], + }, + }, + boards: { + name: 'Total boards', + }, + }, + badges: ['Convenient', 'Informative', 'Anytime'], + share: { + btn: 'Share', + description: 'you can easily take a screenshot and send it to anyone', + }, + }, + settings: { + title: 'Settings', + description: 'Manage your account settings and set some preferences.', + lang: { + label: 'Language', + about: 'This is the language that will be used in the dashboard.', + }, + theme: { + label: 'Theme', + about: 'Select the theme for the dashboard.', + variants: { + light: 'Light', + dark: 'Dark', + auto: 'System', + }, + btn: 'Update preferences', + }, + }, + sheet: { + title: 'Create board', + description: + 'The new board will allow you to create tasks for solving them.', + form: { + name: { + label: 'Board name', + placeholder: 'e.g. "Jenda"', + }, + description: { + label: 'Board description', + placeholder: 'e.g. development board for our company', + }, + submit: 'Create board', + }, + }, + authentication: { + login: { + title: 'Welcome back', + description: 'Enter your info below to sign in your account', + btn: 'Continue', + proposal: 'Don\'t have an account?', + route: 'Sign Up Now', + }, + registration: { + title: 'Get started', + description: 'Enter your info below to create your account', + btn: 'Continue', + proposal: 'Have an account?', + route: 'Sign In Now', + }, + confirm: { + title: 'Confirm your email', + description: 'Enter the confirmation code sent to your email', + btn: 'Confirm', + alert: 'We care about your security. You can trust us to keep your information safe and secure!', + proposal: 'Didn\'t receive the code?', + route: 'Resend', + }, + workspace: { + creating: { + title: 'Create your workspace', + logo: { + label: 'Choose a logo for your workspace', + btn: 'Choose image | Delete', + description: '*.png, *.jpeg files up to 10MB at least 400px by 400px', + }, + alert: 'Remember, if you don’t choose a logo, jenda will automatically generate one for you', + form: { + name: { + label: 'Name', + placeholder: 'Enter your workspace name', + }, + url: { + label: 'Custom link', + }, + }, + }, + choosing: { + title: 'Choose a workspace', + description: 'The more your team engages with Jenda, the more dynamic your workspaces become. | Jenda transforms collaboration in shared workspaces—from organizing projects and managing tasks to centralizing discussions and building efficient workflows.', + label: 'Available workspaces', + }, + route: 'Continue', + }, + form: { + email: 'Email', + password: 'Password', + otp: 'You need to enter a sequence of single-digit numerical values.', + }, + line: 'Or continue with', + privacy: [ + 'By clicking continue, you agree to our', + 'By clicking sign in, you agree to our', + 'Terms of Service', + 'and', + 'Privacy Policy.', + ], + back: 'To Home', + auth_alert: + 'authorization via google will be replaced with gitlab after 28.02.2025', + }, + kanban: { + sorting: { + all: 'All Tasks', + activity: 'By activity', + workload: 'By workload', + }, + active: 'active', + archived: 'archived', + new: 'Add new column', + cards: { + add: 'Add card', + }, + sheet: { + column: { + title: 'Add new column', + description: + 'When creating a new column, you can create cards and use the kanban board.', + form: { + label: 'Column name', + placeholder: 'Enter the column name', + submit: 'Create column', + }, + }, + }, + configuration: { + title: 'Configure your board', + description: 'Customize the board for your convenience', + name: 'Here the name and the color indicator of your project', + users: 'Participants', + status: 'Select the status of your project', + clear: 'Clear the board', + update: 'Apply changes', + }, + }, + welcome: { + header: { + links: [ + 'About', + 'Kanban', + 'Collaborative', + 'Activity', + 'Members', + 'Chats', + ], + login: 'Log In', + reg: 'Get started', + }, + about: { + tagline: 'Your path to perfection', + description: + 'Jenda is a cloud-based program for efficient collaborative and individual project and task management.', + badge: 'Core: Task Boards, Real-Time Tracking', + badge_mobile: 'Core: Boards, Real-Time and etc.', + btn: 'Get started', + }, + marketing: { + heading: 'All convenience in one place', + cards: [ + { + title: 'Kanban 🧑‍💻', + description: + 'Organize and visualize your projects with our intuitive Kanban board. Enhance productivity with ease.', + }, + { + title: 'Collaborative 👥', + description: 'Improve your teamwork!', + }, + { + title: 'Members 🌐', + description: + 'Invite new participants to your projects. Enjoy the development together!', + }, + { + title: 'Activity 👔', + description: 'Analyze your activity and move forward.', + }, + { + title: 'Chats 💬', + description: + 'Communicate, share, discuss. Our convenience for you 😉', + }, + ], + }, + marquee: [ + 'We know', + 'You enjoy', + '👑 The best alternative in Russia', + 'Open source', + '👻 It\'s easier with us', + 'No ads', + 'Perfection is close 🤟', + ], + kanban: { + name: 'Use Kanban', + heading: 'What do you know about our Kanban? 👀', + about: + 'Kanban is a simple and effective way to manage projects, tasks, and time. By visualizing your tasks on a board, you can easily track progress and see what needs to be done next.', + writer: 'Try to conquer the world fully with our Kanban', + review: { + name: 'Alex Peshkov', + status: 'Frontend Developer, Open Source Enthusiast', + comment: + 'I enjoy kanban every day and I advise everyone to try it. This is amazing!', + }, + }, + collaborative: { + name: 'Easier Together', + heading: 'Every task is a step towards 🤝 collective success!', + about: + 'Our service is designed for teams striving for success through collaboration. With it, every participant can easily track tasks, share ideas, and move towards a common goal together with colleagues.', + writer: 'Join us and move towards success as a team!', + review: { + name: 'Bagaudtinov Airat', + status: 'Backend Developer', + comment: + 'Working together is great! Jenda provides such a wonderful opportunity.', + }, + }, + activity: { + heading: 'Progress in action', + about: + 'You can view statistics on completed tasks and open boards. This allows you to track team progress, see completed tasks, and manage current tasks and projects.', + }, + members: { + name: 'Team of participants', + heading: 'Work with a friendly 😉 team of developers', + about: + 'Our service allows you to create and manage teams, add participants, share tasks, and receive real-time feedback.', + writer: 'Join and be in the spotlight!', + review: { + name: 'Alex Peshkov', + status: 'Frontend Developer, Open Source Enthusiast', + comment: + 'A great tool for collaboration. Convenient interface and useful features! 👍 Working with friends', + }, + }, + chats: { + name: 'Ease of communication', + heading: 'How often do you communicate with your team? 🤔', + about: `Our chats allow you to easily exchange messages, share ideas, and stay in sync with your team. Stay connected and work more efficiently!`, + writer: 'Share your thoughts and ideas in our convenient chats!', + review: { + name: 'Ayrat Bagaoutdinov', + status: 'Backend Developer', + comment: + 'The chats here are really convenient! It\'s easy to exchange ideas and quickly resolve issues with the team.', + }, + }, + footer: + 'cloud-based program for task management. The source code is available on', + mobile: { + section_blocks: [ + { + title: 'About Us', + description: 'Get to know Jenda', + }, + { + title: 'Kanban', + description: 'Organize projects with Kanban', + }, + { + title: 'Collaboration', + description: 'Improve your teamwork!', + }, + { + title: 'Members', + description: 'Invite members to projects', + }, + { + title: 'Activity', + description: 'Analyze your activity', + }, + { + title: 'Chats', + description: 'Communicate, share, discuss', + }, + ], + links: [ + { + title: 'product', + }, + { + title: 'contact us on Telegram', + }, + ], + }, + }, + not_found: { + title: 'Not Found', + description: 'The page you were looking for can\'t be found', + btn: 'Go to Home', + }, + dialogs: { + share: { + title: 'Share Link', + description: 'Anyone who has this link will be able to join', + close: 'Cancel', + }, + hotkeys: { + title: 'Hotkeys', + badges: 'Open a search in the workspace | Go to the main page | Log out', + alert: 'You\'ll see { badge } used a lot below. This indicates command on Mac and control on Windows and Linux.', + }, + }, + validations: { + email: 'Email', + password: 'Password', + pin: 'Confirmation code', + name: 'Name', + url: 'URL handle', + }, + search: { + placeholder: 'Type smth for search...', + empty: 'No results found', + recently: 'Recently', + links: 'Links', + select: 'Select', + open: 'Open', + close: 'Close', + }, + workspace: { + popover: { + members: '{n} members', + section: 'Management', + pay: 'Plans and Billing', + invite: 'Invite to Project', + team: 'Team', + settings: 'Change Information', + active: 'Active', + archive: 'Archived', + }, + }, + table: { + row_from_all: 'from', + row_selected: 'rows selected', + rows_on_page: 'Rows per page', + page: 'Page', + empty: 'The table is empty', + }, +} diff --git a/apps/client/src/shared/libs/i18n/locales/index.ts b/apps/client/src/shared/libs/i18n/locales/index.ts new file mode 100644 index 00000000..7dc8bfe2 --- /dev/null +++ b/apps/client/src/shared/libs/i18n/locales/index.ts @@ -0,0 +1,3 @@ +export { default as enLocale } from './en-US' +export { default as ruLocale } from './ru-RU' +export { default as zhLocale } from './zh-CN' diff --git a/apps/client/src/shared/libs/i18n/locales/ru-RU.ts b/apps/client/src/shared/libs/i18n/locales/ru-RU.ts new file mode 100644 index 00000000..b943b3cb --- /dev/null +++ b/apps/client/src/shared/libs/i18n/locales/ru-RU.ts @@ -0,0 +1,529 @@ +export default { + sidebar: { + badge: 'free', + input: 'Поиск', + section: 'воркспейс', + projects: 'проекты', + boards: 'Канбаны', + templates: 'Шаблоны', + notes: 'Заметки', + analytics: 'Аналитика', + members: 'Участники', + settings: 'Настройки', + integrations: 'Интеграции', + help: { + title: 'Помощь', + items: 'Поддержать разработчиков | Сочетания клавиш | Присоединяйтесь к нам', + }, + plan: { + btn: 'Приобрести', + title: 'Премиум план', + description: 'проектов осталось', + tooltip: '1/3 бесплатных workspaces', + }, + soon: 'скоро', + }, + header: { + navigator: { + question: 'Хоткеи', + messages: 'Сообщения', + }, + user: { + welcome: 'Перейти на главную', + logout: 'Выйти', + }, + share: 'Поделиться', + }, + boards: { + create: 'Добавить канбан | Добавить новый канбан', + filters: { + sort: { + title: 'Отсортированно по', + name: 'Названию', + tasks: 'Задачам', + date: 'Дате создания', + default: 'Умолчанию', + }, + advanced: { + title: 'Расширенный фильтр', + empty: 'Таких фильтров нет :/', + statuses: { + title: 'Статусы', + arr: [ + { + value: 'active', + label: 'Активно', + }, + { + value: 'archived', + label: 'В архиве', + }, + ], + }, + labels: { + title: 'Лейблы', + }, + }, + search: 'Найти канбан...', + }, + forms: { + creating: { + title: 'Создание канбана', + description: 'Вы можете создать новый канбан чтобы управлять задачами', + name: { + label: 'Название', + placeholder: 'Введите название канбана', + }, + labels: { + label: 'Лейблы', + placeholder: 'Введите лейбл', + description: 'Начните писать лейбл и нажмите \'Enter\', чтобы он записался в ваши существующие лейблы', + alert: '📝 Как изменить цвет лейбла? Укажите название лейбла, а затем — [#нужный цвет]. Например: jenda [#000000].', + }, + btns: 'Отмена | Создать', + }, + }, + empty: { + title: 'Нет канбанов', + description: 'Создайте свой первый канбан и начните работать', + }, + card: { + date_updated: 'Обновлено', + }, + chart: { + title_boards: 'Созданные доски', + description_boards: + 'Создавайте, делитесь, а затем анализируйте свою активность на Jenda', + title_tasks: 'Ваши задачи', + description_tasks: 'Решайте и завершайте начатые задачи', + }, + columns: ['Название канбана', 'Статус', 'Лейблы', 'Участники', 'Задачи', 'Оценка', 'Дата создания'], + }, + picker: { + placeholder: 'Кастомный цвет', + tabs: 'Однородные | Градиенты', + }, + templates: { + title: 'Шаблоны', + description: + 'Выберите один из доступных шаблонов, чтобы создать ваш проект', + import: 'Нажмите или перетащите файлы для импорта. Поддерживает файлы', + community: 'Шаблоны сообщества', + create: 'Создать шаблон', + user: 'создатель', + items: [ + { + title: 'Базовый шаблон', + tag: 'Рекомендуемый', + description: + 'Создайте базовый проект с помощью шаблона "Базовый шаблон"', + date: '12 июня 2024', + }, + { + title: 'Дорожная карта', + tag: 'Новый', + description: + 'Создайте проект на основе нашего шаблона "Дорожная карта"', + date: '18 июня 2024', + }, + { + title: 'Веб-разработка', + tag: 'Новый', + description: + 'Создайте базовый проект с помощью шаблона "Веб-разработка"', + date: '16 июля 2024', + }, + ], + }, + members: { + title: 'Участники рабочего пространства', + description: + 'Участники могут просматривать и присоединяться к доскам рабочего пространства, а также создавать новые доски в этом пространстве.', + content: { + all: { + title: 'Пригласить пользователей', + description: + 'Каждый, у кого есть ссылка-приглашение, может присоединиться к этому бесплатному рабочему пространству. Ссылка может быть отключена и в любой момент воссоздана. Ожидающие приглашения учитываются в лимите в 10 участников.', + btn: 'Все участники', + }, + guests: { + title: 'Гости', + description: + 'Гости могут только просматривать и редактировать доски, к которым они были добавлены.', + btn: 'Гости', + }, + }, + role: { + member: 'участник', + admin: 'админ', + }, + btns: { + leave: 'Покинуть', + view: 'Просмотреть доски', + }, + }, + analytics: { + title: 'Аналитика рабочего пространства', + description: 'Просматривайте и анализируйте свою активность на Jenda', + charts: { + users: { + name: 'Всего пользователей', + description: 'Количество пользователей в рабочем пространстве', + }, + online: { + name: 'Активность в рабочем пространстве', + }, + tasks: { + name: 'Сводка по задачам', + dataset: { + names: ['Не выполнено', 'Выполнено'], + }, + }, + boards: { + name: 'Созданные доски', + }, + }, + badges: ['Удобно', 'Информативно', 'В любое время'], + share: { + btn: 'Поделиться', + description: 'вы можете легко сделать скриншот и отправить кому угодно', + }, + }, + settings: { + title: 'Настройки', + description: + 'Управляйте настройками своей учетной записи и установите предпочтения.', + lang: { + label: 'Язык', + about: 'Это язык, который будет использоваться в панели инструментов.', + }, + theme: { + label: 'Тема', + about: 'Выберите тему для панели инструментов.', + variants: { + light: 'Светлая', + dark: 'Темная', + auto: 'Системная', + }, + btn: 'Обновить настройки', + }, + }, + sheet: { + title: 'Создать доску', + description: 'Новая доска позволит вам создавать задачи для их решения.', + form: { + name: { + label: 'Название доски', + placeholder: 'например, "Jenda"', + }, + description: { + label: 'Описание доски', + placeholder: 'например, доска для разработки нашей компании', + }, + submit: 'Создать доску', + }, + }, + authentication: { + login: { + title: 'С возвращением', + description: 'Введите свои данные ниже, чтобы войти в свой аккаунт', + btn: 'Продолжить', + proposal: 'Ещё нет аккаунта?', + route: 'Зарегистрируйтесь', + }, + registration: { + title: 'Создайте аккаунт', + description: 'Введите свои данные ниже, чтобы создать аккаунт', + btn: 'Продолжить', + proposal: 'Уже есть аккаунт?', + route: 'Войдите', + }, + confirm: { + title: 'Подтвердите почту', + description: 'Введите код подтверждения, отправленный на вашу почту', + btn: 'Подтвердить', + alert: 'Мы заботимся о вашей безопасности. Вы можете смело нам доверять!', + proposal: 'Не получили код?', + route: 'Отправить повторно', + }, + workspace: { + creating: { + title: 'Создайте воркспейс', + logo: { + label: 'Выберите логотип', + btn: 'Выбрать картинку | Удалить', + description: '*.png, *.jpeg до 10 МБ, размером не менее 400 x 400 пикселей', + }, + alert: 'Помните, если вы не выберете лого, то jenda автоматически создаст его для вас', + form: { + name: { + label: 'Название', + placeholder: 'Введите название', + }, + url: { + label: 'Удобная ссылка', + }, + }, + }, + choosing: { + title: 'Выберите воркспейс', + description: 'Чем активнее ваша команда использует Jenda, тем эффективнее становятся ваши рабочие пространства. | Jenda преображает совместную работу в общих рабочих пространствах. От организации проектов и управления задачами до объединения обсуждений и создания эффективных процессов.', + label: 'Доступные воркспейсы', + }, + route: 'Продолжить', + }, + form: { + email: 'Почта', + password: 'Пароль', + otp: 'Вам нужно ввести последовательность односимвольных числовых значений.', + }, + line: 'Или продолжите с', + privacy: [ + 'Нажав на продолжить, вы соглашаетесь с нашими', + 'Нажав на кнопку "войти", вы соглашаетесь с нашими', + 'Условиями предоставления услуг', + 'и', + 'Политикой конфиденциальности.', + ], + back: 'На главную', + auth_alert: + 'aвторизация через google будет заменена на gitlab после 28.02.2025', + }, + kanban: { + sorting: { + all: 'Все задачи', + activity: 'Aктивность', + workload: 'Нагрузка', + }, + active: 'aктивно', + archived: 'в архиве', + new: 'Добавить новую колонку', + cards: { + add: 'Добавить карточку', + }, + sheet: { + column: { + title: 'Добавить новую колонку', + description: + 'При создании новой колонки вы можете создавать карточки и использовать доску канбан.', + form: { + label: 'Название колонки', + placeholder: 'Введите название колонки', + submit: 'Создать колонку', + }, + }, + }, + configuration: { + title: 'Настройте свою доску', + description: 'Настройте доску для вашего удобства', + name: 'Название и цветовой индикатор вашего проекта', + users: 'Участники', + status: 'Выберите статус вашего проекта', + clear: 'Очистить доску', + update: 'Применить изменения', + }, + }, + welcome: { + header: { + links: [ + 'О нас', + 'Канбан', + 'Совместная работа', + 'Активность', + 'Участники', + 'Чаты', + ], + login: 'Войти', + reg: 'Зарегистрироваться', + }, + about: { + tagline: 'Ваш путь к совершенству', + description: + 'Jenda — это облачная программа для эффективного управления проектами и задачами как в команде, так и индивидуально.', + badge: 'Основные функции: Доски задач, Отслеживание в реальном времени', + badge_mobile: 'Ядро: Проекты, Коллаборации, ...', + btn: 'Зарегистрироваться', + }, + marketing: { + heading: 'Всё удобство в одном месте', + cards: [ + { + title: 'Канбан 🧑‍💻', + description: + 'Организуйте и визуализируйте свои проекты с помощью нашей интуитивно понятной доски Канбан.', + }, + { + title: 'Совместная работа 👥', + description: 'Улучшите свою командную работу!', + }, + { + title: 'Участники 🌐', + description: + 'Пригласите новых участников в ваши проекты. Наслаждайтесь разработкой вместе!', + }, + { + title: 'Активность 👔', + description: 'Анализируйте вашу активность и двигайтесь вперед.', + }, + { + title: 'Чаты 💬', + description: 'Общайтесь, делитесь, обсуждайте. Удобство для вас 😉', + }, + ], + }, + marquee: [ + 'Мы знаем', + 'Вы наслаждаетесь', + '👑 Лучший аналог в РФ', + 'Open source', + '👻 У нас проще', + 'Без рекламы', + 'Совершенство рядом 🤟', + ], + kanban: { + name: 'Пользуйтесь kanban\'ом', + heading: 'Что вы знаете о нашем kanban? 👀', + about: `Kanban – это простой и эффективный способ управлять проектами, задачами и временем. Визуализируя + ваши задачи на доске, вы легко сможете отслеживать прогресс и видеть, что нужно сделать дальше.`, + writer: 'Попробуйте освоить мир на полную с нашим kanban', + review: { + name: 'Александр Пешков', + status: 'Frontend-разработчик, Open source энтузиаст', + comment: + 'Я использую канбан каждый день и советую всем попробовать. Это потрясающе!', + }, + }, + collaborative: { + name: 'Вместе легче', + heading: 'Каждая задача – это шаг к 🤝 общему успеху!', + about: `Наш сервис создан для команд, которые стремятся к успеху через сотрудничество. С его помощью каждый участник может легко отслеживать задачи, делиться идеями и вместе с коллегами двигаться к общей цели.`, + writer: 'Подключайтесь и двигайтесь к успеху всей командой!', + review: { + name: 'Багаутдинов Айрат', + status: 'Backend-разработчик', + comment: + 'Работать совместно это круто! Jenda предоставляет такую шикарную возможность.', + }, + }, + activity: { + heading: 'Прогресс в действии', + about: + 'Вы можете просматривать статистику по завершённым задачам и открытым доскам. Это позволяет отслеживать прогресс команды, видеть выполненные задачи, а также управлять текущими задачами и проектами. ', + }, + members: { + name: 'Команда участников', + heading: 'Работа в команде дружных 😉 разработчиков', + about: + 'Наш сервис дает возможность создавать и управлять командами, добавлять участников, делиться задачами и получать обратную связь в реальном времени.', + writer: 'Участвуй и будь в центре внимания!', + review: { + name: 'Александр Пешков', + status: 'Frontend-разработчик, Open source энтузиаст', + comment: + 'Прекрасный инструмент для совместной работы. Удобный интерфейс и полезные функции! 👍 Работаю вместе с друзьями', + }, + }, + chats: { + name: 'Легкость общения', + heading: 'Как часто вы коммуницируете со своей командой ? 🤔', + about: `Наши чаты позволяют вам легко обмениваться сообщениями, делиться идеями и оставаться на одной волне с командой. Оставайтесь на связи и работайте более эффективно!`, + writer: 'Делитесь своими мыслями и идеями в наших удобных чатах!', + review: { + name: 'Багаутдинов Айрат', + status: 'Backend-разработчик', + comment: + 'Чаты здесь действительно удобные! Легко обмениваться идеями и быстро решать вопросы с командой.', + }, + }, + footer: + 'облачная программа для управления задачами. Исходный код доступен на', + mobile: { + section_blocks: [ + { + title: 'О нас', + description: 'Познакомьтесь с Jenda', + }, + { + title: 'Канбан', + description: 'Организуйте проекты с канбан', + }, + { + title: 'Совместная работа', + description: 'Улучшите свою командную работу!', + }, + { + title: 'Участники', + description: 'Пригласите участников в проекты', + }, + { + title: 'Активность', + description: 'Анализируйте вашу активность', + }, + { + title: 'Чаты', + description: 'Общайтесь, делитесь, обсуждайте', + }, + ], + links: [ + { + title: 'продукт', + }, + { + title: 'свяжитесь с нами в Телеграм', + }, + ], + }, + }, + not_found: { + title: 'Не найдено', + description: 'Страница, которую вы искали, не найдена', + btn: 'На главную', + }, + dialogs: { + share: { + title: 'Поделиться ссылкой', + description: 'Каждый, у кого есть эта ссылка, сможет присоединиться', + close: 'Отмена', + }, + hotkeys: { + title: 'Хоткеи', + badges: 'Открыть поиск в рабочей области | Перейти к главной странице | Выйти из сервиса', + alert: 'Вы часто будете видеть { badge } ниже. Это обозначает cmd на Mac и ctrl на Windows и Linux.', + }, + }, + validations: { + email: 'Почта', + password: 'Пароль', + pin: 'Код подтверждения', + name: 'Название', + url: 'Удобная ссылка', + }, + search: { + placeholder: 'Введите что-нибудь для поиска...', + empty: 'Ничего не найдено', + recently: 'Недавние', + links: 'Ссылки', + select: 'Выбрать', + open: 'Открыть', + close: 'Закрыть', + }, + workspace: { + popover: { + members: '{n} участников | {n} участник | {n} участника', + section: 'Управление', + pay: 'Тарифы и оплата', + invite: 'Пригласить в проект', + team: 'Команда', + settings: 'Поменять информацию', + active: 'Активно', + archive: 'В архиве', + }, + }, + table: { + row_from_all: 'из', + row_selected: 'строк выбрано(а)', + rows_on_page: 'Строк на странице', + page: 'Страница', + empty: 'Таблица пуста', + }, +} diff --git a/apps/client/src/shared/libs/i18n/locales/zh-CN.ts b/apps/client/src/shared/libs/i18n/locales/zh-CN.ts new file mode 100644 index 00000000..f84c78d4 --- /dev/null +++ b/apps/client/src/shared/libs/i18n/locales/zh-CN.ts @@ -0,0 +1,502 @@ +export default { + sidebar: { + badge: '免费', + input: '搜索', + section: '工作区', + projects: '项目', + boards: '看板', + notes: '笔记', + templates: '模板', + analytics: '分析', + members: '成员', + settings: '设置', + integrations: '集成', + help: { + title: '帮助', + items: '支持开发者 | 键盘快捷键 | 加入我们', + }, + plan: { + btn: '购买订阅', + title: '高级计划', + description: '剩余看板', + tooltip: '1/3 免费工作区', + }, + soon: '敬请期待', + }, + header: { + navigator: { + question: '快捷键', + messages: '消息', + }, + user: { + welcome: '前往欢迎页面', + logout: '登出', + }, + share: '分享', + }, + boards: { + create: '添加看板 | 添加新看板', + filters: { + sort: { + title: '排序依据', + name: '名称', + tasks: '任务', + date: '创建日期', + default: '默认', + }, + advanced: { + title: '高级筛选', + empty: '没有这样的筛选器 :/', + statuses: { + title: '状态', + arr: [ + { + value: 'active', + label: '活跃', + }, + { + value: 'archived', + label: '已归档', + }, + ], + }, + labels: { + title: '标签', + }, + }, + search: '搜索看板...', + }, + forms: { + creating: { + title: '创建看板', + description: '您可以创建一个新的看板来管理任务', + name: { + label: '名称', + placeholder: '请输入看板的名称', + }, + labels: { + label: '标签', + placeholder: '输入标签', + description: '开始输入标签并按 \'Enter\' 键,将其保存到现有标签中', + alert: '📝 如何更改标签颜色?指定标签名称后跟 [#所需颜色]。例如:jenda [#000000]。', + }, + btns: '取消 | 创建', + }, + }, + empty: { + title: '没有看板', + description: '创建您的第一个看板并开始工作', + }, + card: { + date_updated: '更新时间', + }, + chart: { + title_boards: '创建的看板', + description_boards: '在 Jenda 上创建、分享、工作,然后分析你的活动', + title_tasks: '你的任务', + description_tasks: '解决并完成你开始的任务', + }, + columns: ['看板名称', '状态', '标签', '参与者', '任务', '估算', '创建日期'], + }, + picker: { + placeholder: '自定义颜色', + tabs: '纯色 | 渐变', + }, + templates: { + title: '模板', + description: '选择一个可用模板来创建您的项目', + import: '点击或拖拽文件导入。支持文件', + community: '社区模板', + create: '创建模板', + user: '创建者', + items: [ + { + title: '基础看板', + tag: '推荐', + description: '使用 "基础看板" 模板创建一个基本项目', + date: '2024年6月12日', + }, + { + title: '路线图', + tag: '新', + description: '基于我们的 "路线图" 模板创建一个项目', + date: '2024年6月18日', + }, + { + title: '网页开发', + tag: '新', + description: '使用 "网页开发" 模板创建一个基本项目', + date: '2024年7月16日', + }, + ], + }, + members: { + title: '工作区参与者', + description: '成员可以查看和加入工作区的白板,并在该空间中创建新的白板。', + content: { + all: { + title: '邀请用户', + description: + '任何拥有邀请链接的人都可以加入这个免费的工作区。链接可以在任何时候被禁用和重新创建。待处理的邀请计入10名参与者的限制。', + btn: '所有参与者', + }, + guests: { + title: '访客', + description: '访客只能查看和编辑他们被添加的板。', + btn: '访客', + }, + }, + role: { + member: '成员', + admin: '管理员', + }, + btns: { + leave: '离开', + view: '查看白板', + }, + }, + analytics: { + title: '工作区分析', + description: '查看并分析您在 Jenda 上的活动', + charts: { + users: { + name: '用户数量', + description: '工作区中的用户数量', + }, + online: { + name: '工作区活动', + }, + tasks: { + name: '任务数量', + dataset: { + names: ['未完成', '已完成'], + }, + }, + boards: { + name: '看板数量', + }, + }, + badges: ['方便', '信息丰富', '随时'], + share: { + btn: '分享', + description: '您可以轻松地截图并将其发送给任何人。', + }, + }, + settings: { + title: '设置', + description: '管理您的帐户设置和设定一些偏好。', + lang: { + label: '语言', + about: '这是将在仪表板中使用的语言。', + }, + theme: { + label: '主题', + about: '选择仪表板的主题。', + variants: { + light: '浅色', + dark: '深色', + auto: '系统', + }, + btn: '更新偏好设置', + }, + }, + sheet: { + title: '创建看板', + description: '新看板将允许你创建任务以进行解决。', + form: { + name: { + label: '看板名称', + placeholder: '例如 “Jenda”', + }, + description: { + label: '看板描述', + placeholder: '例如 我们公司的开发看板', + }, + submit: '创建看板', + }, + }, + authentication: { + login: { + title: '欢迎回来', + description: '请输入下面的信息以登录您的帐户', + btn: '用电子邮件登录', + proposal: '还没有帐户?', + route: '立即登录', + }, + registration: { + title: '开始使用', + description: '请输入下面的信息以创建您的帐户', + btn: '继续', + proposal: '已有帐户?', + route: '立即登录', + }, + confirm: { + title: '确认您的邮箱', + description: '请输入发送到您邮箱的验证码', + btn: '确认', + alert: '我们重视您的安全,您可以放心信任我们!', + proposal: '没有收到验证码?', + route: '重新发送', + }, + workspace: { + creating: { + title: '创建工作区', + logo: { + label: '为您的工作区选择一个标志', + btn: '选择图片 | 删除', + description: '文件 *.png, *.jpeg,最大 10MB,尺寸至少为 400x400 像素', + }, + alert: '请记住,如果您未选择标志,Jenda 将自动为您生成一个', + form: { + name: { + label: '名称', + placeholder: '输入您的工作区名称', + }, + url: { + label: '自定义链接', + }, + }, + }, + choosing: { + title: '选择工作空间', + description: '您的团队使用 Jenda 越积极,您的工作空间就会越高效。| Jenda 改变了在共享工作空间中的协作方式——从项目组织和任务管理到集中讨论和创建高效工作流程。', + label: '可用的工作空间', + }, + route: '立即登录', + }, + form: { + email: '电子邮件', + password: '密码', + otp: '您应输入一串单个数字的值。', + }, + line: '或继续使用', + privacy: ['点击继续即表示您同意我们的', '点击登录即表示您同意我们的', '服务条款', '和', '隐私政策。'], + back: '回到首页', + auth_alert: '通过 google 的授权将在 2025 年 02 月 28 日后被 gitlab 替代', + }, + kanban: { + sorting: { + all: '所有任务', + activity: '按活动排序', + workload: '按工作量排序', + }, + active: '活跃', + archived: '已归档', + new: '添加新列', + cards: { + add: '添加卡片', + }, + sheet: { + column: { + title: '添加新列', + description: '创建新列时,您可以创建卡片并使用看板。', + form: { + label: '列名称', + placeholder: '输入列名称', + submit: '创建列', + }, + }, + }, + configuration: { + title: '配置您的看板', + description: '自定义您的看板以方便使用', + name: '项目的名称和颜色指示器"', + users: '參與者', + status: '选择您的项目状态', + clear: '清空看板', + update: '应用更改', + }, + }, + welcome: { + header: { + links: ['关于', '看板', '协作', '活动', '成员', '聊天'], + login: '登录', + reg: '注册', + }, + about: { + tagline: '通往完美的道路', + description: + 'Jenda 是一款基于云的程序,用于高效协作和个人项目与任务管理。', + badge: '核心功能:任务板、实时跟踪', + badge_mobile: '核心功能:任务板、实时跟踪', + btn: '注册', + }, + marketing: { + heading: '所有便利都在一个地方', + cards: [ + { + title: '看板 🧑‍💻', + description: + '使用我们直观的看板来组织和可视化您的项目。轻松提升生产力。', + }, + { + title: '协作 👥', + description: '提升你的团队合作!', + }, + { + title: '成员 🌐', + description: '邀请新成员参与您的项目。一起享受开发的乐趣!', + }, + { + title: '活动 👔', + description: '分析您的活动,迈步向前。', + }, + { + title: '聊天 💬', + description: '交流、分享、讨论。我们为您提供便利 😉', + }, + ], + }, + marquee: [ + '我们知道', + '你享受', + '👑 俄罗斯最好的替代品', + '开源', + '👻 我们更简单', + '无广告', + '完美就在眼前 🤟', + ], + kanban: { + name: '使用Kanban', + heading: '你知道我们的Kanban吗?👀', + about: + 'Kanban 是一种简单高效的项目、任务和时间管理方式。通过在看板上可视化您的任务,您可以轻松跟踪进度,了解接下来需要做什么。', + writer: '尝试使用我们的Kanban,尽情征服世界', + review: { + name: '亚历克斯·佩什科夫', + status: '前端开发者,开源爱好者', + comment: '我每天都在使用看板,强烈建议大家尝试。这真是太棒了!', + }, + }, + collaborative: { + name: '一起更轻松', + heading: '每个任务都是通向 🤝 共同成功的一步!', + about: + '我们的服务是为那些通过协作追求成功的团队而创建的。借助它,每个参与者都可以轻松跟踪任务,分享想法,并与同事共同朝着共同目标迈进。', + writer: '加入我们,齐心协力迈向成功!', + review: { + name: '巴高丁诺夫·艾拉特', + status: '后端开发人员', + comment: '合作是很棒的!Jenda提供了如此美好的机会。', + }, + }, + activity: { + heading: '进行中的进展', + about: + '您可以查看已完成任务和开放看板的统计信息。这使您能够跟踪团队的进展,查看已完成的任务,以及管理当前的任务和项目。', + }, + members: { + name: '团队成员', + heading: '与友好的 😉 开发者团队一起工作', + about: + '我们的服务让您可以创建和管理团队,添加成员,分享任务并实时获取反馈。', + writer: '加入并成为焦点!', + review: { + name: '亚历山大·佩什科夫', + status: '前端开发者,开源爱好者', + comment: + '这是一个很棒的协作工具。界面方便,功能实用! 👍 和朋友们一起工作', + }, + }, + chats: { + name: '沟通的轻松', + heading: '您与团队沟通的频率有多高?🤔', + about: `我们的聊天功能让您可以轻松地交换消息、分享想法,与团队保持同步。保持联系,更高效地工作!`, + writer: '在我们便捷的聊天中分享您的想法和创意!', + review: { + name: '艾尔拉特·巴高特丁诺夫', + status: '后端开发者', + comment: '这里的聊天功能真的很方便!与团队轻松交换想法,快速解决问题。', + }, + }, + footer: '基于云的任务管理程序。源代码可在', + mobile: { + section_blocks: [ + { + title: '关于我们', + description: '了解 Jenda', + }, + { + title: '看板', + description: '使用看板管理项目', + }, + { + title: '协作', + description: '提高你的团队合作!', + }, + { + title: '成员', + description: '邀请成员加入项目', + }, + { + title: '活动', + description: '分析您的活动', + }, + { + title: '聊天', + description: '沟通、分享、讨论', + }, + ], + links: [ + { + title: '产品', + }, + { + title: '通过 Telegram 联系我们', + }, + ], + }, + }, + not_found: { + title: '未找到', + description: '找不到您要查找的页面', + btn: '返回首页', + }, + dialogs: { + share: { + title: '分享链接', + description: '任何拥有此链接的人都可以加入', + close: '取消', + }, + hotkeys: { + title: '快捷键', + badges: '打开工作区搜索 | 转到主页 | 退出服务', + alert: '你会在下面频繁看到 { badge }。它表示 Mac 上的 Command 键,Windows 和 Linux 上的 Control 键。', + }, + }, + validations: { + email: '邮箱', + password: '密码', + pin: '确认码', + name: '名称', + url: '自定义链接', + }, + search: { + placeholder: '输入内容进行搜索...', + empty: '未找到结果', + recently: '最近', + links: '链接', + select: '选择', + open: '打开', + close: '关闭', + }, + workspace: { + popover: { + members: '{n} 成员', + section: '管理', + pay: '套餐与付款', + invite: '邀请加入项目', + team: '团队', + settings: '更改信息', + active: '活跃', + archive: '已归档', + }, + }, + table: { + row_from_all: '从', + row_selected: '行已选择', + rows_on_page: '每页行数', + page: '页面', + empty: '表格为空', + }, +} diff --git a/apps/client/src/shared/libs/i18n/plurals.ts b/apps/client/src/shared/libs/i18n/plurals.ts new file mode 100644 index 00000000..552febad --- /dev/null +++ b/apps/client/src/shared/libs/i18n/plurals.ts @@ -0,0 +1,14 @@ +export function customPluralRule(choice: T, choicesLength: T): number { + if (choice === 0) { + return 0 + } + const teen = choice > 10 && choice < 20 + const endsWithOne = choice % 10 === 1 + if (!teen && endsWithOne) { + return 1 + } + if (!teen && choice % 10 >= 2 && choice % 10 <= 4) { + return 2 + } + return choicesLength < 4 ? 2 : 3 +} diff --git a/apps/client/src/shared/libs/shadcn/utils.ts b/apps/client/src/shared/libs/shadcn/utils.ts new file mode 100644 index 00000000..a83d31e9 --- /dev/null +++ b/apps/client/src/shared/libs/shadcn/utils.ts @@ -0,0 +1,15 @@ +import { clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' +import type { ClassValue } from 'clsx' +import type { Updater } from '@tanstack/vue-table' +import type { Ref } from 'vue' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} + +export function valueUpdater>(updaterOrValue: T, ref: Ref): void { + ref.value = typeof updaterOrValue === 'function' + ? updaterOrValue(ref.value) + : updaterOrValue +} diff --git a/apps/client/src/shared/libs/unocss/presets/presetUiKit.ts b/apps/client/src/shared/libs/unocss/presets/presetUiKit.ts new file mode 100644 index 00000000..00066db0 --- /dev/null +++ b/apps/client/src/shared/libs/unocss/presets/presetUiKit.ts @@ -0,0 +1,9 @@ +import { shortcuts } from '../../../ui/shortcuts' +import type { Preset } from 'unocss' + +export default function presetJendaUI(): Preset { + return { + name: '@jenda-ui/preset', + shortcuts, + } +} diff --git a/apps/client/src/shared/libs/vee-validate/index.ts b/apps/client/src/shared/libs/vee-validate/index.ts new file mode 100644 index 00000000..78965702 --- /dev/null +++ b/apps/client/src/shared/libs/vee-validate/index.ts @@ -0,0 +1,12 @@ +import { z } from 'zod' +import i18n, { type MessageSchema } from '../i18n' + +import { makeZodI18nMap } from './map' +import type { I18n } from 'vue-i18n' + +z.setErrorMap(makeZodI18nMap( + i18n as unknown as + I18n>, +)) + +export { z } diff --git a/apps/client/src/shared/libs/vee-validate/map.ts b/apps/client/src/shared/libs/vee-validate/map.ts new file mode 100644 index 00000000..9f7bc7c1 --- /dev/null +++ b/apps/client/src/shared/libs/vee-validate/map.ts @@ -0,0 +1,201 @@ +import { + defaultErrorMap, + type ErrorMapCtx, + z, + type ZodErrorMap, + ZodIssueCode, + type ZodIssueOptionalMessage, + ZodParsedType, +} from 'zod' +import { joinValues, jsonStringifyReplacer } from './utils' +import type { + ComposerDateTimeFormatting, + ComposerTranslation, + I18n, +} from 'vue-i18n' + +const zDate = z.string().regex(/(\d{4})-\d{2}-(\d{2})/) + +interface i18nOptions { + [key: string]: unknown +} + +function makeZodI18nMap(i18n: I18n, key = 'errors'): ZodErrorMap { + return (issue: ZodIssueOptionalMessage, ctx: ErrorMapCtx): { message: string } => { + let message: string + message = defaultErrorMap(issue, ctx).message + + let options = {} as i18nOptions + + const t = i18n.global.t as ComposerTranslation + const te = i18n.global.te + const d = i18n.global.d as ComposerDateTimeFormatting + + const translateLabel = (message: string, options: i18nOptions) => { + if (te(`${key}.${message}WithPath`)) { + return t(`${key}.${message}WithPath`, options) + } + if (te(`${key}.${message}`)) { + return t(`${key}.${message}`, options) + } + if (te(message)) { + return t(message, options) + } + return message + } + + switch (issue.code) { + case ZodIssueCode.invalid_type: + if (issue.received === ZodParsedType.undefined) { + message = 'invalidTypeReceivedUndefined' + options = { + validation: te(`validations.${issue.path.join('.')}`) + ? t(`validations.${issue.path.join('.')}`) + : issue.path.join('.'), + } + } + else { + message = 'invalidType' + options = { + expected: te(`types.${issue.expected}`) + ? t(`types.${issue.expected}`) + : issue.expected, + received: te(`types.${issue.received}`) + ? t(`types.${issue.received}`) + : issue.received, + } + } + break + case ZodIssueCode.invalid_literal: + message = 'invalidLiteral' + options = { + expected: JSON.stringify( + issue.expected, + jsonStringifyReplacer, + ), + } + break + case ZodIssueCode.unrecognized_keys: + message = 'unrecognizedKeys' + options = { + keys: joinValues(issue.keys, ', '), + } + break + case ZodIssueCode.invalid_union: + message = 'invalidUnion' + break + case ZodIssueCode.invalid_union_discriminator: + message = 'invalidUnionDiscriminator' + options = { + options: joinValues(issue.options), + } + break + case ZodIssueCode.invalid_enum_value: + message = 'invalidEnumValue' + options = { + options: joinValues(issue.options), + received: issue.received, + } + break + case ZodIssueCode.invalid_arguments: + message = 'invalidArguments' + break + case ZodIssueCode.invalid_return_type: + message = 'invalidReturnType' + break + case ZodIssueCode.invalid_date: + message = 'invalidDate' + break + + case ZodIssueCode.invalid_string: + if (typeof issue.validation === 'object') { + if ('startsWith' in issue.validation) { + message = `invalidString.startsWith` + options = { + startsWith: issue.validation.startsWith, + } + } + else if ('endsWith' in issue.validation) { + message = `invalidString.endsWith` + options = { + endsWith: issue.validation.endsWith, + } + } + } + else { + message = `invalidString.${issue.validation}` + options = { + validation: t(`validations.${issue.validation}`), + } + } + break + case ZodIssueCode.too_small: + message = `tooSmall.${issue.type}.${ + issue.exact + ? 'exact' + : issue.inclusive + ? 'inclusive' + : 'notInclusive' + }` + options = { + minimum: + issue.type === 'date' + ? d(new Date(issue.minimum as string | number)) + : issue.minimum, + } + + break + case ZodIssueCode.too_big: + message = `tooBig.${issue.type}.${ + issue.exact + ? 'exact' + : issue.inclusive + ? 'inclusive' + : 'notInclusive' + }` + options = { + maximum: + issue.type === 'date' + ? d(new Date(issue.maximum as string | number)) + : issue.maximum, + } + break + case ZodIssueCode.custom: + message = 'custom' + if (issue.params?.i18n) { + if (typeof issue.params.i18n === 'string') { + message = issue.params.i18n + break + } + if ( + typeof issue.params.i18n === 'object' + && issue.params.i18n?.key + ) { + message = issue.params.i18n.key + if (issue.params.i18n?.options) { + options = issue.params.i18n.options + } + } + } + break + case ZodIssueCode.invalid_intersection_types: + message = 'invalidIntersectionTypes' + break + case ZodIssueCode.not_multiple_of: + message = 'notMultipleOf' + options = { + multipleOf: issue.multipleOf, + } + break + case ZodIssueCode.not_finite: + message = 'notFinite' + break + } + options.path = issue.path.join('.') || '' + message = translateLabel(message, options) + + return { message } + } +} + +export { makeZodI18nMap, zDate } diff --git a/apps/client/src/shared/libs/vee-validate/rules/en.ts b/apps/client/src/shared/libs/vee-validate/rules/en.ts new file mode 100644 index 00000000..3244d4a9 --- /dev/null +++ b/apps/client/src/shared/libs/vee-validate/rules/en.ts @@ -0,0 +1,23 @@ +export default { + invalidTypeReceivedUndefined: '{validation} is a required field', + invalidString: { + email: '{validation} must be a valid', + url: '{validation} must be a valid', + }, + tooSmall: { + string: { + exact: '{validation} must be at least {minimum} characters', + inclusive: '{path} must be at least {minimum} characters', + }, + array: { + exact: '{path} must be at least {minimum} characters', + inclusive: '{validation} must be at least {minimum} characters', + }, + }, + tooBig: { + string: { + exact: '{path} must be at most {maximum} characters', + inclusive: '{path} must be at most {maximum} characters', + }, + }, +} diff --git a/apps/client/src/shared/libs/vee-validate/rules/index.ts b/apps/client/src/shared/libs/vee-validate/rules/index.ts new file mode 100644 index 00000000..6bc12aef --- /dev/null +++ b/apps/client/src/shared/libs/vee-validate/rules/index.ts @@ -0,0 +1,3 @@ +export { default as enZod } from './en' +export { default as ruZod } from './ru' +export { default as zhZod } from './zh' diff --git a/apps/client/src/shared/libs/vee-validate/rules/ru.ts b/apps/client/src/shared/libs/vee-validate/rules/ru.ts new file mode 100644 index 00000000..d05f3986 --- /dev/null +++ b/apps/client/src/shared/libs/vee-validate/rules/ru.ts @@ -0,0 +1,23 @@ +export default { + invalidTypeReceivedUndefined: 'Обязательное поле', + invalidString: { + email: '{validation} должна быть валидной', + url: '{validation} должен быть валидным', + }, + tooSmall: { + string: { + exact: 'длина должна быть не менее {minimum} символов', + inclusive: 'длина должна быть не менее {minimum} символов', + }, + array: { + exact: 'длина должна быть не менее {minimum} символов', + inclusive: 'длина должна быть не менее {minimum} символов', + }, + }, + tooBig: { + string: { + exact: 'длина должна быть не более {maximum} символов', + inclusive: 'длина должна быть не более {maximum} символов', + }, + }, +} diff --git a/apps/client/src/shared/libs/vee-validate/rules/zh.ts b/apps/client/src/shared/libs/vee-validate/rules/zh.ts new file mode 100644 index 00000000..323afbb1 --- /dev/null +++ b/apps/client/src/shared/libs/vee-validate/rules/zh.ts @@ -0,0 +1,23 @@ +export default { + invalidTypeReceivedUndefined: '此字段为必填项', + invalidString: { + email: '{validation} 必须是有效的', + url: '{validation} 必须是有效的', + }, + tooSmall: { + string: { + exact: '长度必须至少为 {minimum} 个字符', + inclusive: '长度必须至少为 {minimum} 个字符', + }, + array: { + exact: '长度必须至少为 {minimum} 个字符', + inclusive: '长度必须至少为 {minimum} 个字符', + }, + }, + tooBig: { + string: { + exact: '长度不得超过 {maximum} 个字符', + inclusive: '长度不得超过 {maximum} 个字符', + }, + }, +} diff --git a/apps/client/src/shared/libs/vee-validate/utils/index.ts b/apps/client/src/shared/libs/vee-validate/utils/index.ts new file mode 100644 index 00000000..95c0fcc8 --- /dev/null +++ b/apps/client/src/shared/libs/vee-validate/utils/index.ts @@ -0,0 +1,15 @@ +export function jsonStringifyReplacer(_: string, value: unknown): unknown { + if (typeof value === 'bigint') { + return value.toString() + } + return value +} + +export function joinValues( + array: T, + separator = ' | ', +): string { + return array + .map(val => (typeof val === 'string' ? `'${val}'` : val)) + .join(separator) +} diff --git a/apps/client/src/shared/libs/vitest-utils/cookiesI18n-mock.ts b/apps/client/src/shared/libs/vitest-utils/cookiesI18n-mock.ts new file mode 100644 index 00000000..7965a653 --- /dev/null +++ b/apps/client/src/shared/libs/vitest-utils/cookiesI18n-mock.ts @@ -0,0 +1,11 @@ +import { vi } from 'vitest' + +vi.mock('@vueuse/integrations/useCookies', () => { + return { + useCookies: () => ({ + get(key: string) { + return key === 'i18n' ? 'en-US' : undefined + }, + }), + } +}) diff --git a/apps/client/src/shared/ui/__docs__/!misc.mdx b/apps/client/src/shared/ui/__docs__/!misc.mdx new file mode 100644 index 00000000..c35c542d --- /dev/null +++ b/apps/client/src/shared/ui/__docs__/!misc.mdx @@ -0,0 +1,18 @@ +misc.mdx + +import { Markdown } from '@storybook/blocks'; + +# Miscellaneous Components + +This document describes components that are not included in the main documentation but may still be useful in specific contexts. These components are context-dependent and have been thoroughly tested in their respective `.spec.ts` files. + + + {` +| Component | Description | Notes | +|-------------|---------------------------------------------------------------------------------------------------|--------------------------------------------| +| UiPagination | A pagination component designed to manage and display paginated data dynamically. | Context-dependent, tested in .spec.ts. | +| DataTable and UiTable | A table component for displaying structured data with customizable columns and rows. | Context-dependent, tested in .spec.ts. | + `} + + +> **Note:** These components are excluded from the primary documentation because their usage scenarios are tightly coupled with specific contexts. Ensure to evaluate their fit for your use case before implementation. diff --git a/apps/client/src/shared/ui/__docs__/Alert.mdx b/apps/client/src/shared/ui/__docs__/Alert.mdx new file mode 100644 index 00000000..c6a97874 --- /dev/null +++ b/apps/client/src/shared/ui/__docs__/Alert.mdx @@ -0,0 +1,39 @@ +{/* Alert.mdx */} + +import { Canvas, Story, Meta, Markdown } from '@storybook/blocks'; +import * as AlertStories from '../alert/UiAlert.stories'; + + + +# UiAlert + +The **UiAlert** component is a flexible alert element that can adapt to various themes and styles, offering customizability for different UI requirements. + + + +### Props + + + {` +| Prop | Description | +|------------------|-------------------------------------------------------------------------------------------------------------------------------| +| variant | Defines the alert's style. Options are: __default__, __secondary__, __ghost__, __destructive__, __outline__, and __dashed__. | +| closable | Determines if the alert displays a close button. | +| content | Sets the main text or content for the alert. | +| class | Use for customize alert with unocss by type of HTMLAttributes['class'] | +`} + + + + +### Slots + + + {` +| Slot | Description | +|------------|----------------------------------------------------------------------------------------------------------- | +| default | Main content slot for the alert's message or other components. | +`} + + + diff --git a/apps/client/src/shared/ui/__docs__/Badge.mdx b/apps/client/src/shared/ui/__docs__/Badge.mdx new file mode 100644 index 00000000..eb81b170 --- /dev/null +++ b/apps/client/src/shared/ui/__docs__/Badge.mdx @@ -0,0 +1,25 @@ +{/* Badge.mdx */} + +import { Canvas, Story, Meta, Markdown } from '@storybook/blocks'; +import * as BadgeStories from '../badge/UiBadge.stories'; + + + +# UiBadge + +The **UiBadge** component is a flexible badge that supports various styles, making it suitable for different UI contexts. + + + +### Props + + + {` +| Prop | Description | +|----------|---------------------------------------------------------------------------------------------------------- | +| variant | Defines the badge style. Options are: __default__, __secondary__, __outline__, __solid__, __colorized(success and destructive)__.| +| class | Use for customize badge with unocss by type of HTMLAttributes['class'] | +`} + + + diff --git a/apps/client/src/shared/ui/__docs__/Button.mdx b/apps/client/src/shared/ui/__docs__/Button.mdx new file mode 100644 index 00000000..cbcd3dd0 --- /dev/null +++ b/apps/client/src/shared/ui/__docs__/Button.mdx @@ -0,0 +1,45 @@ +{/* Button.mdx */} + +import { Canvas, Story, Meta, Markdown } from '@storybook/blocks'; +import * as ButtonStories from '../button/UiButton.stories'; + + + +# UiButton + +The **UiButton** component is a versatile button that supports various sizes, themes, and icon placements, making it suitable for different UI use cases. + + + +### Props + + + {` +| Prop | Description | +|------------------|------------------------------------------------------------------------------------------------------------------------------- | +| variant | Defines the button's style. Options are: __default__, __secondary__, __ghost__, __destructive__, __outline__, __dashed__, __solid__, __success__ | +| size | Controls the button's size. Options are: __sm__ (small), __default__ (medium, default), __xs__(xs) and __lg__ (large). | +| loading | Shows a loading spinner when true, indicating a loading state. | +| disabled | Disables the button when true, setting it to an inactive state and reducing opacity to 0.6. | +| loadingPlacement | Defines where the loading spinner appears when __loading__ is true. Options are __leading__ (before content) and __trailing__. | +| class | Use for customize button with unocss by type of HTMLAttributes['class'] | +`} + + + + +### Slots + + + {` +| Slot | Description | +|------------|----------------------------------------------------------------------------------------------------------- | +| default | Main content slot for the button's label or other components. | +| leading | Slot for content to appear at the start of the button, such as an icon or loading spinner. | +| trailing | Slot for content to appear at the end of the button, like an icon or loading spinner. | +| loading | Slot for a custom loading spinner or icon to be displayed when the button is in a loading state. | +`} + + + + diff --git a/apps/client/src/shared/ui/__docs__/Checkbox.mdx b/apps/client/src/shared/ui/__docs__/Checkbox.mdx new file mode 100644 index 00000000..71d4e013 --- /dev/null +++ b/apps/client/src/shared/ui/__docs__/Checkbox.mdx @@ -0,0 +1,28 @@ +{/* Checkbox.mdx */} + +import { Canvas, Story, Meta, Markdown } from '@storybook/blocks'; +import * as CheckboxStories from '../checkbox/UiCheckbox.stories'; + + + +# UiCheckbox + +The **UiCheckbox** component is a control that allows the user to toggle between checked and not checked. + + + +### Props + + + {` +| Prop | Description | +|-------------|-----------------------------------------------------------------------------------------------------------------| +| checked | The checkbox field value. **Use __v-model__ to bind this prop to a data property.** | +| defaultValue| The default value | +| class | Use for customize checkbox with unocss by type of HTMLAttributes['class'] | + `} + + +### Another Props + +refs: https://www.radix-vue.com/components/checkbox diff --git a/apps/client/src/shared/ui/__docs__/Command.mdx b/apps/client/src/shared/ui/__docs__/Command.mdx new file mode 100644 index 00000000..970646ff --- /dev/null +++ b/apps/client/src/shared/ui/__docs__/Command.mdx @@ -0,0 +1,20 @@ +{/* Command.mdx */} + +import { Canvas, Story, Meta, Markdown } from '@storybook/blocks'; +import * as CommandStories from '../command/UiCommand.stories'; + + + +# UiCommand + +Fast, composable, unstyled command menu. + +### Main Components: + +`UiCommand`, `UiCommandItem`, `UiCommandList`, `UiCommandInput`, + + + +### Props + +refs: https://www.radix-vue.com/components/combobox diff --git a/apps/client/src/shared/ui/__docs__/Dialog.mdx b/apps/client/src/shared/ui/__docs__/Dialog.mdx new file mode 100644 index 00000000..0bd3698b --- /dev/null +++ b/apps/client/src/shared/ui/__docs__/Dialog.mdx @@ -0,0 +1,20 @@ +{/* Dialog.mdx */} + +import { Canvas, Story, Meta, Markdown } from '@storybook/blocks'; +import * as DialogStories from '../dialog/UiDialog.stories'; + + + +# UiDialog + +A window overlaid on either the primary window or another dialog window, rendering the content underneath inert. + +### Main Components: + +`UiDialog`, `UiDialogTrigger`, `UiDialogContent` + + + +### Props + +refs: https://www.radix-vue.com/components/dialog diff --git a/apps/client/src/shared/ui/__docs__/DropdownMenu.mdx b/apps/client/src/shared/ui/__docs__/DropdownMenu.mdx new file mode 100644 index 00000000..9f524bae --- /dev/null +++ b/apps/client/src/shared/ui/__docs__/DropdownMenu.mdx @@ -0,0 +1,20 @@ +{/* Dropdown.mdx */} + +import { Canvas, Story, Meta, Markdown } from '@storybook/blocks'; +import * as DropdownStories from '../dropdown-menu/UiDropdownMenu.stories'; + + + +# UiDropdown + +The **UiDropdown** component displays a menu to the user — such as a set of actions or functions — triggered by a button. + +### Main Components: + +`UiDropdown`, `UiDropdownTrigger`, `UiDropdownContent`, `UiDropdownItem`, + + + +### Props + +refs: https://www.radix-vue.com/components/dropdown-menu diff --git a/apps/client/src/shared/ui/__docs__/Form.mdx b/apps/client/src/shared/ui/__docs__/Form.mdx new file mode 100644 index 00000000..df430b04 --- /dev/null +++ b/apps/client/src/shared/ui/__docs__/Form.mdx @@ -0,0 +1,16 @@ +{/* Form.mdx */} + +import { Canvas, Story, Meta, Markdown } from '@storybook/blocks'; +import * as FormStories from '../form/UiForm.stories'; + + + +# UiForm + +Form component with with possible integration formkit and vee-validate. + + + +### Warning + + diff --git a/apps/client/src/shared/ui/__docs__/GettingStarted.mdx b/apps/client/src/shared/ui/__docs__/GettingStarted.mdx new file mode 100644 index 00000000..e100d6a3 --- /dev/null +++ b/apps/client/src/shared/ui/__docs__/GettingStarted.mdx @@ -0,0 +1,39 @@ +import { Meta } from '@storybook/blocks' + + + +# Get started 🖖 + +**Jenda UI Kit** is a custom, reusable component library designed to help developers create user interfaces faster with a cohesive design language. Created as part of the Jenda project, this UI kit provides pre-built, customizable Vue components tailored for seamless integration and intuitive use. + +### **Usage** + +Import components you want into your UI + +`import { UiButton, UiBadge, UiInput } from '@shared/ui';` + +and use them like so + +``` + + + +``` + +### **Run and develop UI Kit locally** + +Clone the [Jenda repository](https://github.com/mnenie/jenda) then start Storybook. + +`pnpm i && pnpm run storybook` + +__if you have an errors in console with `iframe` just reload page__ + + +### **Theme Support** + +Jenda UI Kit components support multiple themes: light, dark, and system (auto-switches based on user’s OS preferences). This allows for a more personalized and accessible UI experience across devices and environments. + diff --git a/apps/client/src/shared/ui/__docs__/Input.mdx b/apps/client/src/shared/ui/__docs__/Input.mdx new file mode 100644 index 00000000..e1c72742 --- /dev/null +++ b/apps/client/src/shared/ui/__docs__/Input.mdx @@ -0,0 +1,24 @@ +{/* Input.mdx */} + +import { Canvas, Story, Meta, Markdown } from '@storybook/blocks'; +import * as InputStories from '../input/UiInput.stories'; + + + +# UiInput + +The **UiInput** component is a versatile input field that supports various types (text, password, number, email) and can be easily integrated with `v-model` for value binding. + + + +### Props + + + {` +| Prop | Description | +|-------------|-----------------------------------------------------------------------------------------------------------------| +| modelValue | The input field value. **Use __v-model__ to bind this prop to a data property.** | +| defaultValue| The default value | +| class | Use for customize input with unocss by type of HTMLAttributes['class'] | + `} + diff --git a/apps/client/src/shared/ui/__docs__/PinInput.mdx b/apps/client/src/shared/ui/__docs__/PinInput.mdx new file mode 100644 index 00000000..e4d578a0 --- /dev/null +++ b/apps/client/src/shared/ui/__docs__/PinInput.mdx @@ -0,0 +1,26 @@ +{/* PinInput.mdx */} + +import { Canvas, Story, Meta, Markdown } from '@storybook/blocks'; +import * as PinInputStories from '../pin-input/UiPinInput.stories'; + + + +# UiPinInput + +Allows users to input a sequence of one-character alphanumeric inputs. + +### Main Components: + +`UiPinInput`, `UiPinInputInput`, `UiPinInputGroup` + + + +### With Separator + +`UiPinInput`, `UiPinInputInput`, `UiPinInputGroup`, `UiPinInputSeparator` + + + +### Props + +refs: https://www.radix-vue.com/components/pin-input diff --git a/apps/client/src/shared/ui/__docs__/Popover.mdx b/apps/client/src/shared/ui/__docs__/Popover.mdx new file mode 100644 index 00000000..4f484d65 --- /dev/null +++ b/apps/client/src/shared/ui/__docs__/Popover.mdx @@ -0,0 +1,20 @@ +{/* Popover.mdx */} + +import { Canvas, Story, Meta, Markdown } from '@storybook/blocks'; +import * as PopoverStories from '../popover/UiPopover.stories'; + + + +# UiPopover + +The **UiPopover** component displays a content in a portal to the user — such as a set of actions or functions — triggered by a button. + +### Components: + +`UiPopover`, `UiPopoverTrigger`, `UiPopoverContent` + + + +### Props + +refs: https://www.radix-vue.com/components/popover diff --git a/apps/client/src/shared/ui/__docs__/Select.mdx b/apps/client/src/shared/ui/__docs__/Select.mdx new file mode 100644 index 00000000..c68b548e --- /dev/null +++ b/apps/client/src/shared/ui/__docs__/Select.mdx @@ -0,0 +1,20 @@ +{/* Select.mdx */} + +import { Canvas, Story, Meta, Markdown } from '@storybook/blocks'; +import * as SelectStories from '../select/UiSelect.stories'; + + + +# UiSelect + +The **UiSelect** component displays a list of options for the user to pick from—triggered by a button. + +### Main Components: + +`UiSelect`, `UiSelectTrigger`, `UiSelectContent`, `UiSelectItem`, + + + +### Props + +refs: https://www.radix-vue.com/components/select diff --git a/apps/client/src/shared/ui/__docs__/Tabs.mdx b/apps/client/src/shared/ui/__docs__/Tabs.mdx new file mode 100644 index 00000000..42d5ace7 --- /dev/null +++ b/apps/client/src/shared/ui/__docs__/Tabs.mdx @@ -0,0 +1,20 @@ +{/* Tabs.mdx */} + +import { Canvas, Story, Meta, Markdown } from '@storybook/blocks'; +import * as TabsStories from '../tabs/UiTabs.stories'; + + + +# UiTabs + +The **UiTabs** component set of layered sections of content—known as tab panels—that are displayed one at a time. + +### Main Components: + +`UiTabs`, `UiTabsTrigger`, `UiTabsContent`, `UiTabsList`, + + + +### Props + +refs: https://www.radix-vue.com/components/tabs diff --git a/apps/client/src/shared/ui/__docs__/TagsInput.mdx b/apps/client/src/shared/ui/__docs__/TagsInput.mdx new file mode 100644 index 00000000..701b4228 --- /dev/null +++ b/apps/client/src/shared/ui/__docs__/TagsInput.mdx @@ -0,0 +1,24 @@ +{/* TagsInput.mdx */} + +import { Canvas, Story, Meta, Markdown } from '@storybook/blocks'; +import * as TagsInputStories from '../tags-input/UiTagsInput.stories'; + + + +# UiTagsInput + +Render tags inside an input, followed by an actual text input. + +### Main Components: + +`UiTagsInput`, `UiTagsInputInput`, `UiTagsInputItem` + + + +### Disabled + + + +### Props + +refs: https://www.radix-vue.com/components/tags-input diff --git a/apps/client/src/shared/ui/_shortcuts/alert.ts b/apps/client/src/shared/ui/_shortcuts/alert.ts new file mode 100644 index 00000000..d93e00be --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/alert.ts @@ -0,0 +1,20 @@ +type AlertPrefix = 'alert' + +export const staticAlert: Record<`${AlertPrefix}-${string}` | AlertPrefix, string> = { + 'alert': 'relative flex items-center justify-center border border-solid gap-2.5 py-1.25 pl-2.5 rounded-8px mb-5 min-h-22px', + 'alert-close': 'absolute top-1/2 transform -translate-y-1/2 right-2.5 text-sm text-neutral-700 cursor-pointer dark:text-neutral-200', + + 'alert-default': 'bg-neutral-50 border-neutral-200 text-neutral-800 dark:(bg-neutral-800 text-neutral-200 border-neutral-700)', + 'alert-success': 'bg-green-50 border-green-200 text-neutral-950 dark:(bg-#1b4731 text-neutral-200 border-green-800)', + 'alert-warning': 'bg-yellow-50 border-#f8d040 text-neutral-950 dark:(bg-#3d351a text-neutral-200 border-#9f8745)', + 'alert-important': 'bg-red-50 border-red-600 text-neutral-950 dark:(bg-#692525 text-neutral-200 border-#c73939)', +} + +export const dynamicAlert: [RegExp, (params: RegExpExecArray) => string][] = [ + [/^alert-(solid|outline)-(.*)$/, ([, c, v]) => `alert-${c}-${v}`], +] + +export const alert = [ + ...dynamicAlert, + staticAlert, +] diff --git a/apps/client/src/shared/ui/_shortcuts/badge.ts b/apps/client/src/shared/ui/_shortcuts/badge.ts new file mode 100644 index 00000000..e3eef4f2 --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/badge.ts @@ -0,0 +1,20 @@ +type BadgePrefix = 'badge' + +export const staticBadge: Record<`${BadgePrefix}-${string}` | BadgePrefix, string> = { + 'badge': 'inline-flex items-center rounded-md border border-solid border-neutral-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:(outline-none ring-2 ring-neutral-950 ring-offset-2) dark:(border-neutral-700 focus:ring-neutral-300)', + + 'badge-default': 'border-transparent bg-neutral-900 text-neutral-50 shadow dark:(bg-neutral-50 text-neutral-900)', + 'badge-secondary': 'border-transparent bg-neutral-100 text-neutral-900 dark:(bg-neutral-800 text-neutral-50)', + 'badge-outline': 'text-neutral-950 dark:text-neutral-50', + 'badge-solid': '!border-transparent bg-#266df0 text-neutral-50 shadow', +} + +export const dynamicBadge: [RegExp, (params: RegExpExecArray) => string][] = [ + [/^badge-colorized-(.*)$/, ([, c]) => `border-transparent bg-${c}-500 text-neutral-50 shadow dark:(bg-${c}-900 text-neutral-50 border-transparent)`], + [/^badge-soft(?:-(.+))?$/, ([, c = 'blue']) => `!border-transparent bg-${c}-100 text-${c}-700 dark:bg-${c}-900 dark:text-${c}-100`], +] + +export const badge = [ + ...dynamicBadge, + staticBadge, +] diff --git a/apps/client/src/shared/ui/_shortcuts/button.ts b/apps/client/src/shared/ui/_shortcuts/button.ts new file mode 100644 index 00000000..9aeaa643 --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/button.ts @@ -0,0 +1,21 @@ +type ButtonPrefix = 'btn' + +export const staticBtn: Record<`${ButtonPrefix}-${string}` | ButtonPrefix, string> = { + 'btn': 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:(outline-none ring-1 ring-neutral-950) disabled:(pointer-events-none opacity-50) [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:focus-visible:ring-neutral-300', + + 'btn-default': 'bg-neutral-900 text-neutral-50 shadow hover:bg-neutral-900/90 dark:(bg-neutral-50 text-neutral-900 hover:bg-neutral-50/90)', + 'btn-secondary': 'bg-neutral-100 text-neutral-900 shadow-sm hover:bg-neutral-100/80 dark:(bg-#2e2e2e text-neutral-50 hover:bg-#2e2e2e/80)', + 'btn-outline': 'border border-solid border-neutral-200 bg-white shadow-sm hover:(bg-neutral-50 text-neutral-900) dark:(border-neutral-700 bg-neutral-800 hover:bg-neutral-700/20 hover:text-neutral-50)', + 'btn-ghost': 'bg-transparent hover:(bg-neutral-100 text-neutral-900) dark:(hover:bg-neutral-700/40 hover:text-neutral-50)', + 'btn-dashed': 'border border-dashed border-neutral-200 bg-white shadow-sm hover:(bg-neutral-50 text-neutral-900) dark:(border-neutral-700 bg-neutral-800 hover:bg-neutral-700/20 hover:text-neutral-50)', +} + +export const dynamicBtn: [RegExp, (params: RegExpExecArray) => string][] = [ + [/^btn-solid(?:-(.+))?$/, ([, c = '#266df0']) => `bg-${c} text-neutral-50 shadow focus:ring-0 hover:bg-${c}/90`], + [/^btn-colorized-(.*)$/, ([, c]) => `bg-${c}-500 text-neutral-50 shadow-sm hover:bg-${c}-500/90 dark:(bg-${c}-900 text-neutral-50 hover:bg-${c}-900/90)`], +] + +export const btn = [ + ...dynamicBtn, + staticBtn, +] diff --git a/apps/client/src/shared/ui/_shortcuts/checkbox.ts b/apps/client/src/shared/ui/_shortcuts/checkbox.ts new file mode 100644 index 00000000..a3cc82e2 --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/checkbox.ts @@ -0,0 +1,19 @@ +type CheckboxPrefix = 'checkbox' + +export const staticCheckbox: Record<`${CheckboxPrefix}-${string}` | CheckboxPrefix, string> = { + 'checkbox': 'w-4 h-4 shrink-0 rounded-md bg-transparent border disabled:cursor-not-allowed disabled:opacity-50', + 'checkbox-indicator': 'flex h-full w-full items-center justify-center', + 'checkbox-indecator-icon': 'w-4 h-4', + + 'checkbox-default': 'border-neutral-950 data-[state=checked]:bg-neutral-950 data-[state=checked]:text-neutral-50 dark:border-neutral-100 dark:(focus-visible:ring-neutral-100 data-[state=checked]:bg-neutral-100 data-[state=checked]:text-neutral-900)', + +} + +export const dynamicCheckbox: [RegExp, (params: RegExpExecArray) => string][] = [ + [/^checkbox-solid(?:-(.+))?$/, ([, c = 'blue']) => `border-neutral-200 data-[state=checked]:!bg-${c}-500 data-[state=checked]:!text-neutral-50 dark:border-neutral-600`], +] + +export const checkbox = [ + staticCheckbox, + ...dynamicCheckbox, +] diff --git a/apps/client/src/shared/ui/_shortcuts/command.ts b/apps/client/src/shared/ui/_shortcuts/command.ts new file mode 100644 index 00000000..8be2b5f6 --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/command.ts @@ -0,0 +1,18 @@ +type CommandPrefix = 'command' + +export const staticCommand: Record<`${CommandPrefix}-${string}` | CommandPrefix, string> = { + 'command': 'flex h-full w-full flex-col rounded-md bg-white text-neutral-950 dark:(bg-neutral-800 text-neutral-50)', + 'command-dialog': 'overflow-hidden p-0 shadow-lg', + 'command-empty': 'py-6 text-center text-sm', + 'command-group': 'overflow-y-auto overflow-x-hidden p-1 text-neutral-950', + 'command-group-label': 'px-2 py-1.5 text-xs font-medium text-neutral-500 dark:text-neutral-400', + 'command-input': 'flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-neutral-500 disabled:(cursor-not-allowed opacity-50) dark:placeholder:text-neutral-400', + 'command-item': 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:(bg-neutral-100 text-neutral-900) data-[disabled]:(pointer-events-none opacity-50) dark:data-[highlighted]:(bg-neutral-700/40 text-neutral-50)', + 'command-list': 'max-h-[300px] overflow-y-auto overflow-x-hidden', + 'command-separator': '-mx-1 h-px bg-neutral-200 dark:bg-neutral-700/40', + 'command-shortcut': 'ml-auto text-xs tracking-widest text-neutral-500 dark:text-neutral-400', +} + +export const command = [ + staticCommand, +] diff --git a/apps/client/src/shared/ui/_shortcuts/dialog.ts b/apps/client/src/shared/ui/_shortcuts/dialog.ts new file mode 100644 index 00000000..4d65bc2f --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/dialog.ts @@ -0,0 +1,19 @@ +type DialogPrefix = 'dialog' + +export const staticDialog: Record<`${DialogPrefix}-${string}` | DialogPrefix, string> = { + 'dialog': '', + 'dialog-overlay': 'fixed inset-0 z-999 bg-black/60', + 'dialog-content': 'fixed left-1/2 top-1/2 z-1000 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border border-neutral-200 bg-white p-6 shadow-lg duration-200 sm:rounded-lg dark:(border-neutral-800 bg-neutral-800)', + 'dialog-close': 'absolute right-4 top-3.25 w-4 h-4 flex items-center justify-center rounded-sm opacity-70 ring-offset-0 transition-opacity hover:opacity-100 focus:outline-none focus:ring-0 disabled:pointer-events-none dark:(ring-offset-0 focus:ring-0)', + 'dialog-title': 'text-lg font-semibold leading-none tracking-tight', + 'dialog-description': 'text-sm text-neutral-500 dark:text-neutral-400', + 'dialog-footer': 'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2', + 'dialog-header': 'flex flex-col gap-y-1.5 text-center sm:text-left', + 'dialog-scroll-overlay': 'dialog-overlay grid place-items-center overflow-y-auto', + 'dialog-scroll-content': 'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-neutral-200 bg-white p-6 shadow-lg duration-200 sm:rounded-lg md:w-full dark:(border-neutral-800 bg-neutral-800)', + 'dialog-scroll-close': 'fixed inset-0 z-50 cursor-pointer opacity-0', +} + +export const dialog = [ + staticDialog, +] diff --git a/apps/client/src/shared/ui/_shortcuts/dropdown-menu.ts b/apps/client/src/shared/ui/_shortcuts/dropdown-menu.ts new file mode 100644 index 00000000..0c07c2e6 --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/dropdown-menu.ts @@ -0,0 +1,19 @@ +type DropdownMenuPrefix = 'dropdown-menu' + +export const staticDropdownMenu: Record<`${DropdownMenuPrefix}-${string}` | DropdownMenuPrefix, string> = { + 'dropdown-menu': '', + 'dropdown-menu-content': 'z-1000 min-w-32 overflow-hidden rounded-md border border-neutral-200 bg-white p-1 text-neutral-950 shadow-md dark:(border-neutral-700 bg-neutral-800 text-neutral-50)', + 'dropdown-menu-checkbox-item': 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:(pointer-events-none opacity-50) dark:(focus:bg-neutral-700/40 focus:text-neutral-50)', + 'dropdown-menu-checkbox-item-indicator': 'absolute left-2 flex h-3.5 w-3.5 items-center justify-center', + 'dropdown-menu-item': 'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:(pointer-events-none opacity-50) [&>svg]:size-4 [&>svg]:shrink-0 dark:(focus:bg-neutral-700/40 focus:text-neutral-50)', + 'dropdown-menu-label': 'px-2 py-1.5 text-sm font-semibold', + 'dropdown-menu-separator': '-mx-1 my-1 h-px bg-neutral-100 dark:bg-neutral-700/40', + 'dropdown-menu-shortcut': 'ml-auto text-xs tracking-widest opacity-60', + 'dropdown-menu-sub-content': 'z-50 min-w-32 overflow-hidden rounded-md border border-neutral-200 bg-white p-1 text-neutral-950 shadow-lg dark:(border-neutral-700/40 bg-neutral-800 text-neutral-50)', + 'dropdown-menu-sub-trigger': 'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-neutral-100 data-[state=open]:bg-neutral-100 dark:(focus:bg-neutral-800 data-[state=open]:bg-neutral-800)', + 'dropdown-menu-trigger': 'outline-none', +} + +export const dropdownMenu = [ + staticDropdownMenu, +] diff --git a/apps/client/src/shared/ui/_shortcuts/form.ts b/apps/client/src/shared/ui/_shortcuts/form.ts new file mode 100644 index 00000000..c44b528f --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/form.ts @@ -0,0 +1,13 @@ +type FormPrefix = 'form' + +export const staticForm: Record<`${FormPrefix}-${string}` | FormPrefix, string> = { + 'form': '', + 'form-field': 'grid gap-2 justify-items-start', + 'form-label': 'text-sm text-neutral-900 !fw500 dark:text-neutral-200 peer-disabled:(cursor-not-allowed opacity-70)', + 'form-message': 'flex items-center gap-2 capitalize text-xs normal-case !fw500', + 'form-text-underline': 'cursor-pointer underline underline-offset-4 duration-100 ease-in hover:text-neutral-900 dark:hover:text-neutral-400', +} + +export const form = [ + staticForm, +] diff --git a/apps/client/src/shared/ui/_shortcuts/index.ts b/apps/client/src/shared/ui/_shortcuts/index.ts new file mode 100644 index 00000000..62ea770e --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/index.ts @@ -0,0 +1,38 @@ +import { alert } from './alert' +import { badge } from './badge' +import { btn } from './button' +import { dialog } from './dialog' +import { dropdownMenu } from './dropdown-menu' +import { form } from './form' +import { input } from './input' +import { pinInput } from './pin-input' +import { select } from './select' +import { popover } from './popover' +import { command } from './command' +import { tabs } from './tabs' +import { pagination } from './pagination' +import { table } from './table' +import { checkbox } from './checkbox' +import { picker } from './picker' +import { tagsInput } from './tags-input' +import type { Preset, StaticShortcutMap } from '@unocss/core' + +export const shortcuts = [ + ...btn, + ...input, + ...badge, + ...alert, + ...dropdownMenu, + ...select, + ...dialog, + ...pinInput, + ...form, + ...popover, + ...command, + ...tabs, + ...pagination, + ...checkbox, + ...table, + ...picker, + ...tagsInput, +] as Exclude diff --git a/apps/client/src/shared/ui/_shortcuts/input.ts b/apps/client/src/shared/ui/_shortcuts/input.ts new file mode 100644 index 00000000..c449e7f3 --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/input.ts @@ -0,0 +1,9 @@ +type InputPrefix = 'input' + +export const staticInput: Record<`${InputPrefix}-${string}` | InputPrefix, string> = { + input: 'flex h-34px w-full rounded-md border border-solid border-neutral-200 bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:(border-0 bg-transparent text-sm font-medium) placeholder:text-neutral-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:(cursor-not-allowed opacity-50) dark:(border-neutral-700 placeholder:text-neutral-400 focus-visible:ring-neutral-300)', +} + +export const input = [ + staticInput, +] diff --git a/apps/client/src/shared/ui/_shortcuts/pagination.ts b/apps/client/src/shared/ui/_shortcuts/pagination.ts new file mode 100644 index 00000000..ec20b3ba --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/pagination.ts @@ -0,0 +1,11 @@ +type PaginationPrefix = 'pagination' + +export const staticPagination: Record<`${PaginationPrefix}-${string}` | PaginationPrefix, string> = { + 'pagination': '', + 'pagination-ellipsis': 'w-9 h-9 flex items-center justify-center', + 'pagination-item': 'w-9 h-9 p-0', +} + +export const pagination = [ + staticPagination, +] diff --git a/apps/client/src/shared/ui/_shortcuts/picker.ts b/apps/client/src/shared/ui/_shortcuts/picker.ts new file mode 100644 index 00000000..1dcb94c5 --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/picker.ts @@ -0,0 +1,13 @@ +type PickerPrefix = 'picker' + +export const staticPicker: Record<`${PickerPrefix}-${string}` | PickerPrefix, string> = { + 'picker': 'flex flex-wrap gap-1 mt-0', + 'picker-icon': 'h-3.5 w-3.5 rounded object-cover transition-all mr-px cursor-pointer text-neutral-800 dark:text-neutral-100', + 'picker-input': 'col-span-2 h-8 mt-4 focus:ring-0', + 'picker-tabs': 'w-full', + 'picker-item': 'rounded-md h-6 w-6 cursor-pointer active:scale-105', +} + +export const picker = [ + staticPicker, +] diff --git a/apps/client/src/shared/ui/_shortcuts/pin-input.ts b/apps/client/src/shared/ui/_shortcuts/pin-input.ts new file mode 100644 index 00000000..0116d9bc --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/pin-input.ts @@ -0,0 +1,11 @@ +type PinInputPrefix = 'pin-input' + +export const staticPinInput: Record<`${PinInputPrefix}-${string}` | PinInputPrefix, string> = { + 'pin-input': 'flex gap-2 items-center', + 'pin-input-group': 'flex items-center', + 'pin-input-input': 'relative placeholder:(absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2) align-middle text-center focus:(outline-none ring-2 ring-neutral-950 relative z-10) flex h-9 w-9 items-center justify-center border-y border-r border-neutral-200 text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md dark:(focus:ring-neutral-300 border-neutral-800)', +} + +export const pinInput = [ + staticPinInput, +] diff --git a/apps/client/src/shared/ui/_shortcuts/popover.ts b/apps/client/src/shared/ui/_shortcuts/popover.ts new file mode 100644 index 00000000..734a24c6 --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/popover.ts @@ -0,0 +1,10 @@ +type PopoverPrefix = 'popover' + +export const staticPopover: Record<`${PopoverPrefix}-${string}` | PopoverPrefix, string> = { + 'popover': '', + 'popover-content': 'z-1001 w-72 rounded-md border border-neutral-200 bg-white p-2 px-3 text-neutral-950 shadow outline-none dark:(border-neutral-700 bg-neutral-800 text-neutral-50)', +} + +export const popover = [ + staticPopover, +] diff --git a/apps/client/src/shared/ui/_shortcuts/select.ts b/apps/client/src/shared/ui/_shortcuts/select.ts new file mode 100644 index 00000000..53723393 --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/select.ts @@ -0,0 +1,17 @@ +type SelectPrefix = 'select' + +export const staticSelect: Record<`${SelectPrefix}-${string}` | SelectPrefix, string> = { + 'select': '', + 'select-content': 'relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border border-solid border-neutral-200 bg-white text-neutral-950 shadow-md dark:(border-neutral-700/40 bg-neutral-800 text-neutral-50)', + 'select-group': 'p-1 w-full', + 'select-item': 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:(pointer-events-none opacity-50) dark:(focus:bg-neutral-700/40 focus:text-neutral-50)', + 'select-item-indicator': 'absolute right-2 flex h-3.5 w-3.5 items-center justify-center', + 'select-label': 'px-2 py-1.5 text-sm font-semibold', + 'select-scroll-button': 'flex cursor-default items-center justify-center py-1', + 'select-separator': '-mx-1 my-1 h-px bg-neutral-100 dark:bg-neutral-800', + 'select-trigger': 'flex h-8 w-full items-center justify-between whitespace-nowrap rounded-md border border-solid border-neutral-200 bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-white data-[placeholder]:text-neutral-500 focus:outline-none focus:ring-1 focus:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start dark:(border-neutral-700 ring-offset-neutral-800 data-[placeholder]:text-neutral-400 focus:ring-neutral-300)', +} + +export const select = [ + staticSelect, +] diff --git a/apps/client/src/shared/ui/_shortcuts/table.ts b/apps/client/src/shared/ui/_shortcuts/table.ts new file mode 100644 index 00000000..70e70b5b --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/table.ts @@ -0,0 +1,18 @@ +type TablePrefix = 'table' + +export const staticTable: Record<`${TablePrefix}-${string}` | TablePrefix, string> = { + 'table': 'w-full caption-bottom text-13px 2xl:text-sm overflow-y-auto', + 'table-wrapper': 'relative w-full overflow-x-hidden overflow-y-auto border border-neutral-100 dark:border-neutral-700/50 max-h-[calc(100dvh-160px)] rounded-md', + 'table-body': '[&_tr:last-child]:border-0', + 'table-row': 'border-b border-neutral-100 dark:border-neutral-700/50 transition-colors data-[state=selected]:bg-blue-100 dark:data-[state=selected]:bg-blue-900 cursor-pointer', + 'table-cell': 'p-2 border-r overflow-x-auto first:border-none last:border-none border-neutral-100 dark:border-neutral-700/50 overflow-hidden text-ellipsis whitespace-nowrap px-8px h-12 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:flex [&>[role=checkbox]]:items-center dark:text-neutral-100', + 'table-head': 'h-12 px-8px first:pl-12px border-r first:border-none last:border-none border-neutral-100 dark:border-neutral-700/50 text-ellipsis text-left align-middle font-medium text-neutral-500 dark:text-neutral-400 [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:flex [&>[role=checkbox]]:items-center', + 'table-header': 'sticky top-0 z-33 border-b bg-neutral-50 dark:bg-#2e2e2e', + 'table-empty': 'flex items-center justify-center text-sm text-neutral-500 dark:text-neutral-400', + 'table-empty-cell': 'whitespace-nowrap align-middle text-sm text-neutral-950 dark:text-neutral-100', + 'table-caption': 'mt-4 text-sm text-neutral-500 dark:text-neutral-400', +} + +export const table = [ + staticTable, +] diff --git a/apps/client/src/shared/ui/_shortcuts/tabs.ts b/apps/client/src/shared/ui/_shortcuts/tabs.ts new file mode 100644 index 00000000..5d0b23e5 --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/tabs.ts @@ -0,0 +1,12 @@ +type TabsPrefix = 'tabs' + +export const staticTabs: Record<`${TabsPrefix}-${string}` | TabsPrefix, string> = { + 'tabs': '', + 'tabs-content': 'focus-visible:outline-none focus-visible:ring-0', + 'tabs-list': 'inline-flex items-center justify-center rounded-lg bg-neutral-100 h-9 2xl:h-8 border border-neutral-100 p-1 text-neutral-500 dark:(border-neutral-700 bg-neutral-800 text-neutral-400)', + 'tabs-trigger': 'inline-flex items-center bg-transparent justify-center whitespace-nowrap rounded-md px-3 py-1 2xl:(py-0.5 px-2.5) text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-neutral-950 data-[state=active]:shadow dark:(ring-offset-0 focus-visible:ring-0 data-[state=active]:bg-neutral-700/40 data-[state=active]:text-neutral-100)', +} + +export const tabs = [ + staticTabs, +] diff --git a/apps/client/src/shared/ui/_shortcuts/tags-input.ts b/apps/client/src/shared/ui/_shortcuts/tags-input.ts new file mode 100644 index 00000000..d5a7dac6 --- /dev/null +++ b/apps/client/src/shared/ui/_shortcuts/tags-input.ts @@ -0,0 +1,14 @@ +type TagsInputPrefix = 'tags-input' + +export const staticTagsInput: Record<`${TagsInputPrefix}-${string}` | TagsInputPrefix, string> = { + 'tags-input': 'flex flex-wrap gap-2 items-center rounded-md border border-neutral-200 px-3 py-1.5 text-sm dark:(border-neutral-700 focus-visible:ring-neutral-300)', + 'tags-input-input': 'text-sm min-h-5 focus:outline-none flex-1 bg-transparent placeholder:text-neutral-500 dark:placeholder:text-neutral-400', + 'tags-input-item': 'flex h-5 items-center rounded-md ring-offset-0 bg-neutral-100 text-neutral-800 dark:(bg-neutral-700/60 text-neutral-100) data-[state=active]:(ring-0 ring-offset-0)', + 'tags-input-item-delete': 'flex rounded bg-transparent mr-1', + 'tags-input-item-delete-icon': 'h-3.5 w-3.5', + 'tags-input-item-text': 'py-0.5 px-2 text-sm rounded bg-transparent', +} + +export const tagsInput = [ + staticTagsInput, +] diff --git a/apps/client/src/shared/ui/alert/UiAlert.stories.ts b/apps/client/src/shared/ui/alert/UiAlert.stories.ts new file mode 100644 index 00000000..b77fcc4f --- /dev/null +++ b/apps/client/src/shared/ui/alert/UiAlert.stories.ts @@ -0,0 +1,53 @@ +import UiAlert from './UiAlert.vue' +import type { Meta, StoryFn } from '@storybook/vue3' + +export default { + title: 'UiAlert', + component: UiAlert, + argTypes: { + variant: { + control: { type: 'select' }, + options: ['default', 'warning', 'success', 'important'], + defaultValue: 'default', + }, + closable: { + control: 'boolean', + }, + }, +} as Meta + +const Template: StoryFn = args => ({ + components: { UiAlert }, + setup() { + return { args } + }, + template: ` + + + + `, +}) + +export const DefaultAlert: StoryFn = Template.bind({}) + +DefaultAlert.args = { + variant: 'default', + content: 'hello from alert', +} + +export const AlertExampleProps: StoryFn = Template.bind({}) + +AlertExampleProps.args = { + variant: 'warning', + content: 'hello from alert', + closable: true, +} + +export const SlottedAlert: StoryFn = Template.bind({}) + +SlottedAlert.args = { + variant: 'success', + default: ` + alert with slot with success variant + `, +} diff --git a/apps/client/src/shared/ui/alert/UiAlert.vue b/apps/client/src/shared/ui/alert/UiAlert.vue new file mode 100644 index 00000000..5ad8b407 --- /dev/null +++ b/apps/client/src/shared/ui/alert/UiAlert.vue @@ -0,0 +1,42 @@ + + + diff --git a/apps/client/src/shared/ui/alert/__tests__/UiAlert.spec.ts b/apps/client/src/shared/ui/alert/__tests__/UiAlert.spec.ts new file mode 100644 index 00000000..bbb5ad0c --- /dev/null +++ b/apps/client/src/shared/ui/alert/__tests__/UiAlert.spec.ts @@ -0,0 +1,29 @@ +import { nextTick } from 'vue' +import { shallowMount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import UiAlert from '../UiAlert.vue' + +describe('tests for UiAlert', () => { + const wrapper = shallowMount(UiAlert, { + props: { + variant: 'default', + }, + }) + + it('should render correctly', () => { + expect(wrapper.html()) + }) + + it('should have content at slot and render x icon', async () => { + const _w = shallowMount(UiAlert, { + slots: { + default: 'hello from alert', + }, + props: { + closable: true, + }, + }) + await nextTick() + expect(_w.html()).toMatchSnapshot() + }) +}) diff --git a/apps/client/src/shared/ui/alert/__tests__/__snapshots__/UiAlert.spec.ts.snap b/apps/client/src/shared/ui/alert/__tests__/__snapshots__/UiAlert.spec.ts.snap new file mode 100644 index 00000000..deb6e7e1 --- /dev/null +++ b/apps/client/src/shared/ui/alert/__tests__/__snapshots__/UiAlert.spec.ts.snap @@ -0,0 +1,6 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`tests for UiAlert > should have content at slot and render x icon 1`] = ` +"
hello from alert
+
" +`; diff --git a/apps/client/src/shared/ui/alert/index.ts b/apps/client/src/shared/ui/alert/index.ts new file mode 100644 index 00000000..40c4f8bc --- /dev/null +++ b/apps/client/src/shared/ui/alert/index.ts @@ -0,0 +1,16 @@ +import { cva, type VariantProps } from 'class-variance-authority' + +export { default as UiAlert } from './UiAlert.vue' + +export const alertVariants = cva('alert', { + variants: { + variant: { + default: 'alert-default', + success: 'alert-success', + warning: 'alert-warning', + important: 'alert-important', + }, + }, +}) + +export type AlertVariants = VariantProps diff --git a/apps/client/src/shared/ui/badge/UiBadge.stories.ts b/apps/client/src/shared/ui/badge/UiBadge.stories.ts new file mode 100644 index 00000000..0eeff978 --- /dev/null +++ b/apps/client/src/shared/ui/badge/UiBadge.stories.ts @@ -0,0 +1,41 @@ +import UiBadge from './UiBadge.vue' +import type { Meta, StoryFn } from '@storybook/vue3' + +export default { + title: 'UiBadge', + component: UiBadge, + argTypes: { + variant: { + control: { type: 'select' }, + options: ['default', 'secondary', 'outline'], + }, + }, +} as Meta + +const Template: StoryFn = (args) => { + return { + components: { UiBadge }, + setup() { + return { args } + }, + template: ` + + + + `, + } +} + +export const DefaultBadge: StoryFn = Template.bind({}) + +DefaultBadge.args = { + variant: 'default', + default: `Badge`, +} + +export const BadgeExampleProps: StoryFn = Template.bind({}) + +BadgeExampleProps.args = { + variant: 'outline', + default: `new`, +} diff --git a/apps/client/src/shared/ui/badge/UiBadge.vue b/apps/client/src/shared/ui/badge/UiBadge.vue new file mode 100644 index 00000000..a03166f2 --- /dev/null +++ b/apps/client/src/shared/ui/badge/UiBadge.vue @@ -0,0 +1,16 @@ + + + diff --git a/apps/client/src/shared/ui/badge/__tests__/UiBadge.spec.ts b/apps/client/src/shared/ui/badge/__tests__/UiBadge.spec.ts new file mode 100644 index 00000000..4c412124 --- /dev/null +++ b/apps/client/src/shared/ui/badge/__tests__/UiBadge.spec.ts @@ -0,0 +1,44 @@ +import { shallowMount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import UiBadge from '../UiBadge.vue' +import type { BadgeVariants } from '..' + +describe('tests for UiBadge', () => { + const wrapper = shallowMount(UiBadge, { + props: { + variant: 'default', + }, + }) + + it('should render correctly', () => { + expect(wrapper.html()) + }) + + it('should have content at slot', () => { + const _w = shallowMount(UiBadge, { + slots: { + default: 'badge', + }, + }) + expect(_w.html()).toMatchSnapshot() + }) + + it('should apply correct styles for variants', () => { + const variants = [ + 'default', + 'secondary', + 'outline', + 'solid', + 'destructive', + ] as BadgeVariants['variant'][] + variants.forEach(() => { + const wrapper = shallowMount(UiBadge, { + props: { + variant: 'default', + }, + }) + const badge = wrapper.find('div') + expect(badge.attributes('class')).toContain(`badge-default`) + }) + }) +}) diff --git a/apps/client/src/shared/ui/badge/__tests__/__snapshots__/UiBadge.spec.ts.snap b/apps/client/src/shared/ui/badge/__tests__/__snapshots__/UiBadge.spec.ts.snap new file mode 100644 index 00000000..3db2b9f6 --- /dev/null +++ b/apps/client/src/shared/ui/badge/__tests__/__snapshots__/UiBadge.spec.ts.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`tests for UiBadge > should have content at slot 1`] = `"
badge
"`; diff --git a/apps/client/src/shared/ui/badge/index.ts b/apps/client/src/shared/ui/badge/index.ts new file mode 100644 index 00000000..360eea5c --- /dev/null +++ b/apps/client/src/shared/ui/badge/index.ts @@ -0,0 +1,19 @@ +import { cva, type VariantProps } from 'class-variance-authority' + +export { default as UiBadge } from './UiBadge.vue' + +export const badgeVariants = cva('badge', { + variants: { + variant: { + default: 'badge-default', + secondary: 'badge-secondary', + destructive: 'badge-colorized-red', + success: 'badge-colorized-green', + solid: 'badge-solid', + outline: 'badge-outline', + soft: 'badge-soft', + }, + }, +}) + +export type BadgeVariants = VariantProps diff --git a/apps/client/src/shared/ui/button/UiButton.stories.ts b/apps/client/src/shared/ui/button/UiButton.stories.ts new file mode 100644 index 00000000..15a55c28 --- /dev/null +++ b/apps/client/src/shared/ui/button/UiButton.stories.ts @@ -0,0 +1,69 @@ +import UiButton from './UiButton.vue' +import type { Meta, StoryFn } from '@storybook/vue3' + +export default { + title: 'UiButton', + component: UiButton, + argTypes: { + size: { + control: { type: 'radio' }, + options: ['sm', 'default', 'lg'], + defaultValue: 'md', + }, + variant: { + control: { type: 'select' }, + options: [ + 'default', + 'ghost', + 'secondary', + 'destructive', + 'outline', + 'dashed', + ], + defaultValue: 'default', + }, + }, +} as Meta + +const Template: StoryFn = (args) => { + return { + components: { UiButton, File }, + setup() { + return { args } + }, + template: ` + + + + + + `, + } +} + +export const DefaultButton: StoryFn = Template.bind({}) + +DefaultButton.args = { + size: 'default', + variant: 'default', + default: `Get Started`, +} + +export const ButtonExampleProps: StoryFn = Template.bind({}) + +ButtonExampleProps.args = { + size: 'sm', + variant: 'solid', + default: `Create task`, +} + +export const SlottedButton: StoryFn = Template.bind({}) + +SlottedButton.args = { + size: 'default', + variant: 'default', + leading: `
`, + default: ` + Download + `, +} diff --git a/apps/client/src/shared/ui/button/UiButton.vue b/apps/client/src/shared/ui/button/UiButton.vue new file mode 100644 index 00000000..526f3d4a --- /dev/null +++ b/apps/client/src/shared/ui/button/UiButton.vue @@ -0,0 +1,49 @@ + + + diff --git a/apps/client/src/shared/ui/button/__tests__/UiButton.spec.ts b/apps/client/src/shared/ui/button/__tests__/UiButton.spec.ts new file mode 100644 index 00000000..92164525 --- /dev/null +++ b/apps/client/src/shared/ui/button/__tests__/UiButton.spec.ts @@ -0,0 +1,66 @@ +import { mount, shallowMount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import UiButton from '../UiButton.vue' +import type { ButtonVariants } from '..' + +describe('tests for UiButton', () => { + // with + const wrapper = shallowMount(UiButton, { + props: { + variant: 'default', + size: 'default', + as: 'button', + }, + }) + + it('should render correctly', () => { + expect(wrapper.html()) + }) + + it('should have content at slot', () => { + const _w = shallowMount(UiButton, { + slots: { + default: 'ui button', + }, + }) + expect(_w.html()).toMatchSnapshot() + }) + + it('should apply correct styles for variants', () => { + const variants = [ + 'default', + 'secondary', + 'solid', + 'destructive', + 'ghost', + 'outline', + 'dashed', + ] as ButtonVariants['variant'][] + + variants.forEach(() => { + const wrapper = mount(UiButton, { + props: { + variant: 'solid', + size: 'default', + }, + }) + const button = wrapper.find('button') + expect(button.exists()).toBe(true) + expect(button.attributes('class')).toContain('btn-solid') + }) + }) + + it('should apply correct styles for size', () => { + const sizes = ['lg', 'default', 'sm'] as ButtonVariants['size'][] + sizes.forEach(() => { + const wrapper = mount(UiButton, { + props: { + variant: 'dashed', + size: 'sm', + }, + }) + const button = wrapper.find('button') + expect(button.attributes('class')).toContain(`h-8`) + }) + }) +}) diff --git a/apps/client/src/shared/ui/button/__tests__/__snapshots__/UiButton.spec.ts.snap b/apps/client/src/shared/ui/button/__tests__/__snapshots__/UiButton.spec.ts.snap new file mode 100644 index 00000000..0880a570 --- /dev/null +++ b/apps/client/src/shared/ui/button/__tests__/__snapshots__/UiButton.spec.ts.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`tests for UiButton > should have content at slot 1`] = `""`; diff --git a/apps/client/src/shared/ui/button/index.ts b/apps/client/src/shared/ui/button/index.ts new file mode 100644 index 00000000..840c2b51 --- /dev/null +++ b/apps/client/src/shared/ui/button/index.ts @@ -0,0 +1,27 @@ +import { cva, type VariantProps } from 'class-variance-authority' + +export { default as UiButton } from './UiButton.vue' + +export const buttonVariants = cva('btn', { + variants: { + variant: { + default: 'btn-default', + solid: 'btn-solid', + destructive: 'btn-colorized-red', + success: 'btn-colorized-green', + outline: 'btn-outline', + secondary: 'btn-secondary', + ghost: 'btn-ghost', + dashed: 'btn-dashed', + }, + size: { + default: 'h-9 px-4 py-2', + xs: 'h-8 rounded px-2 text-xs rounded-lg', + md: 'h-34px rounded-md px-3', + sm: 'h-34px 2xl:h-8 rounded-md px-3', + lg: 'h-38px rounded-md px-8', + }, + }, +}) + +export type ButtonVariants = VariantProps diff --git a/apps/client/src/shared/ui/card/UiCard.vue b/apps/client/src/shared/ui/card/UiCard.vue new file mode 100644 index 00000000..aa790725 --- /dev/null +++ b/apps/client/src/shared/ui/card/UiCard.vue @@ -0,0 +1,15 @@ + + + diff --git a/apps/client/src/shared/ui/card/index.ts b/apps/client/src/shared/ui/card/index.ts new file mode 100644 index 00000000..e49b307d --- /dev/null +++ b/apps/client/src/shared/ui/card/index.ts @@ -0,0 +1 @@ +export { default as UiCard } from './UiCard.vue' diff --git a/apps/client/src/shared/ui/checkbox/UiCheckbox.stories.ts b/apps/client/src/shared/ui/checkbox/UiCheckbox.stories.ts new file mode 100644 index 00000000..7df90e6e --- /dev/null +++ b/apps/client/src/shared/ui/checkbox/UiCheckbox.stories.ts @@ -0,0 +1,32 @@ +import UiCheckbox from './UiCheckbox.vue' +import type { Meta, StoryFn } from '@storybook/vue3' + +export default { + title: 'UiCheckbox', + component: UiCheckbox, + argTypes: { + checked: { + control: { type: 'boolean' }, + }, + }, +} as Meta + +const Template: StoryFn = (args) => { + return { + components: { UiCheckbox }, + setup() { + return { args } + }, + template: `
+ +
`, + } +} + +export const DefaultCheckbox: StoryFn = Template.bind({}) + +export const CheckboxWithVariant: StoryFn = Template.bind({}) + +CheckboxWithVariant.args = { + variant: 'solid', +} diff --git a/apps/client/src/shared/ui/checkbox/UiCheckbox.vue b/apps/client/src/shared/ui/checkbox/UiCheckbox.vue new file mode 100644 index 00000000..d3a5d9ca --- /dev/null +++ b/apps/client/src/shared/ui/checkbox/UiCheckbox.vue @@ -0,0 +1,39 @@ + + + diff --git a/apps/client/src/shared/ui/checkbox/__tests__/UiCheckbox.spec.ts b/apps/client/src/shared/ui/checkbox/__tests__/UiCheckbox.spec.ts new file mode 100644 index 00000000..1a0adfd1 --- /dev/null +++ b/apps/client/src/shared/ui/checkbox/__tests__/UiCheckbox.spec.ts @@ -0,0 +1,34 @@ +import { mount, shallowMount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import UiCheckbox from '../UiCheckbox.vue' +import type { CheckboxVariants } from '..' + +describe('test for UiCheckbox', () => { + // with + it('should render correctly', () => { + const wrapper = shallowMount(UiCheckbox, { + props: { + checked: true, + }, + }) + expect(wrapper.html()).toMatchSnapshot() + }) + it('should apply correct styles for checkbox variants', () => { + const variants = [ + 'default', + 'solid', + ] as CheckboxVariants['variant'][] + + variants.forEach(() => { + const wrapper = mount(UiCheckbox, { + props: { + variant: 'solid', + checked: true, + }, + }) + const checkbox = wrapper.find('.checkbox') + expect(checkbox.exists()).toBe(true) + expect(checkbox.attributes('class')).toContain('checkbox-solid') + }) + }) +}) diff --git a/apps/client/src/shared/ui/checkbox/__tests__/__snapshots__/UiCheckbox.spec.ts.snap b/apps/client/src/shared/ui/checkbox/__tests__/__snapshots__/UiCheckbox.spec.ts.snap new file mode 100644 index 00000000..99304529 --- /dev/null +++ b/apps/client/src/shared/ui/checkbox/__tests__/__snapshots__/UiCheckbox.spec.ts.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`test for UiCheckbox > should render correctly 1`] = `""`; diff --git a/apps/client/src/shared/ui/checkbox/index.ts b/apps/client/src/shared/ui/checkbox/index.ts new file mode 100644 index 00000000..9f13b2f1 --- /dev/null +++ b/apps/client/src/shared/ui/checkbox/index.ts @@ -0,0 +1,14 @@ +import { cva, type VariantProps } from 'class-variance-authority' + +export { default as UiCheckbox } from './UiCheckbox.vue' + +export const checkboxVariants = cva('checkbox', { + variants: { + variant: { + default: 'checkbox-default', + solid: 'checkbox-solid', + }, + }, +}) + +export type CheckboxVariants = VariantProps diff --git a/apps/client/src/shared/ui/command/UiCommand.stories.ts b/apps/client/src/shared/ui/command/UiCommand.stories.ts new file mode 100644 index 00000000..5f71290b --- /dev/null +++ b/apps/client/src/shared/ui/command/UiCommand.stories.ts @@ -0,0 +1,35 @@ +import { + UiCommand, + UiCommandInput, + UiCommandItem, + UiCommandList, +} from './index' +import type { Meta, StoryFn } from '@storybook/vue3' + +export default { + title: 'UiCommand', + component: UiCommand, +} as Meta + +const Template: StoryFn = args => ({ + components: { + UiCommand, + UiCommandItem, + UiCommandList, + UiCommandInput, + }, + setup() { + return { args } + }, + template: + ` + + + + Calendar + + + `, +}) + +export const DefaultCommand: StoryFn = Template.bind({}) diff --git a/apps/client/src/shared/ui/command/UiCommand.vue b/apps/client/src/shared/ui/command/UiCommand.vue new file mode 100644 index 00000000..7220e108 --- /dev/null +++ b/apps/client/src/shared/ui/command/UiCommand.vue @@ -0,0 +1,33 @@ + + + diff --git a/apps/client/src/shared/ui/command/UiCommandDialog.vue b/apps/client/src/shared/ui/command/UiCommandDialog.vue new file mode 100644 index 00000000..51ab4165 --- /dev/null +++ b/apps/client/src/shared/ui/command/UiCommandDialog.vue @@ -0,0 +1,28 @@ + + + diff --git a/apps/client/src/shared/ui/command/UiCommandEmpty.vue b/apps/client/src/shared/ui/command/UiCommandEmpty.vue new file mode 100644 index 00000000..968f3a34 --- /dev/null +++ b/apps/client/src/shared/ui/command/UiCommandEmpty.vue @@ -0,0 +1,26 @@ + + + diff --git a/apps/client/src/shared/ui/command/UiCommandGroup.vue b/apps/client/src/shared/ui/command/UiCommandGroup.vue new file mode 100644 index 00000000..0e8b4128 --- /dev/null +++ b/apps/client/src/shared/ui/command/UiCommandGroup.vue @@ -0,0 +1,31 @@ + + + diff --git a/apps/client/src/shared/ui/command/UiCommandInput.vue b/apps/client/src/shared/ui/command/UiCommandInput.vue new file mode 100644 index 00000000..3f6ab843 --- /dev/null +++ b/apps/client/src/shared/ui/command/UiCommandInput.vue @@ -0,0 +1,35 @@ + + + diff --git a/apps/client/src/shared/ui/command/UiCommandItem.vue b/apps/client/src/shared/ui/command/UiCommandItem.vue new file mode 100644 index 00000000..9415220a --- /dev/null +++ b/apps/client/src/shared/ui/command/UiCommandItem.vue @@ -0,0 +1,29 @@ + + + diff --git a/apps/client/src/shared/ui/command/UiCommandList.vue b/apps/client/src/shared/ui/command/UiCommandList.vue new file mode 100644 index 00000000..df8252ee --- /dev/null +++ b/apps/client/src/shared/ui/command/UiCommandList.vue @@ -0,0 +1,33 @@ + + + diff --git a/apps/client/src/shared/ui/command/UiCommandSeparator.vue b/apps/client/src/shared/ui/command/UiCommandSeparator.vue new file mode 100644 index 00000000..80faba1c --- /dev/null +++ b/apps/client/src/shared/ui/command/UiCommandSeparator.vue @@ -0,0 +1,26 @@ + + + diff --git a/apps/client/src/shared/ui/command/UiCommandShortcut.vue b/apps/client/src/shared/ui/command/UiCommandShortcut.vue new file mode 100644 index 00000000..567c8675 --- /dev/null +++ b/apps/client/src/shared/ui/command/UiCommandShortcut.vue @@ -0,0 +1,19 @@ + + + diff --git a/apps/client/src/shared/ui/command/__tests__/MockComponent.vue b/apps/client/src/shared/ui/command/__tests__/MockComponent.vue new file mode 100644 index 00000000..93d67d79 --- /dev/null +++ b/apps/client/src/shared/ui/command/__tests__/MockComponent.vue @@ -0,0 +1,27 @@ + + + diff --git a/apps/client/src/shared/ui/command/__tests__/UiCommand.spec.ts b/apps/client/src/shared/ui/command/__tests__/UiCommand.spec.ts new file mode 100644 index 00000000..466c6372 --- /dev/null +++ b/apps/client/src/shared/ui/command/__tests__/UiCommand.spec.ts @@ -0,0 +1,43 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import MockComponent from './MockComponent.vue' + +describe('tests for UiCommand', () => { + const wrapper = mount(MockComponent, { + props: { + open: true, + modelValue: ['value1', 'value2'], + }, + }) + + beforeEach(async () => { + await wrapper.setProps({ + filterFunction: (list: any[], term: string) => { + return list.filter(i => i.toLowerCase().includes(term.toLowerCase())) + }, + }) + }) + + it('should render correctly', () => { + expect(wrapper.html()).toMatchSnapshot() + }) + + it('should filter items', async () => { + const input = wrapper.findComponent({ name: 'UiCommandInput' }) + await input.setValue('te') + + const selection = wrapper.findAll('[data-highlighted]').filter(i => i.attributes('style') !== 'display: none;') + expect(selection.length).toBe(1) + expect(selection[0].element.innerHTML).contains('test item') + }) + + it('should work props/emits correctly', async () => { + wrapper.findComponent({ name: 'UiCommandItem' }).trigger('click') + expect(wrapper.emitted()['update:modelValue']).toBeTruthy() + + const selectedItems = wrapper.props().modelValue + expect(selectedItems).toEqual(['value1', 'value2']) + }) +}) + +// more tests: https://github.com/unovue/radix-vue/blob/main/packages/radix-vue/src/Combobox/Combobox.test.ts diff --git a/apps/client/src/shared/ui/command/__tests__/__snapshots__/UiCommand.spec.ts.snap b/apps/client/src/shared/ui/command/__tests__/__snapshots__/UiCommand.spec.ts.snap new file mode 100644 index 00000000..c015e831 --- /dev/null +++ b/apps/client/src/shared/ui/command/__tests__/__snapshots__/UiCommand.spec.ts.snap @@ -0,0 +1,15 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`tests for UiCommand > should render correctly 1`] = ` +"
+
+
+
+
+
+
test item
+
+
+ +
" +`; diff --git a/apps/client/src/shared/ui/command/index.ts b/apps/client/src/shared/ui/command/index.ts new file mode 100644 index 00000000..4702d497 --- /dev/null +++ b/apps/client/src/shared/ui/command/index.ts @@ -0,0 +1,9 @@ +export { default as UiCommand } from './UiCommand.vue' +export { default as UiCommandDialog } from './UiCommandDialog.vue' +export { default as UiCommandEmpty } from './UiCommandEmpty.vue' +export { default as UiCommandGroup } from './UiCommandGroup.vue' +export { default as UiCommandInput } from './UiCommandInput.vue' +export { default as UiCommandItem } from './UiCommandItem.vue' +export { default as UiCommandList } from './UiCommandList.vue' +export { default as UiCommandSeparator } from './UiCommandSeparator.vue' +export { default as UiCommandShortcut } from './UiCommandShortcut.vue' diff --git a/apps/client/src/shared/ui/data-table/DataTable.vue b/apps/client/src/shared/ui/data-table/DataTable.vue new file mode 100644 index 00000000..4249b685 --- /dev/null +++ b/apps/client/src/shared/ui/data-table/DataTable.vue @@ -0,0 +1,257 @@ + + + diff --git a/apps/client/src/shared/ui/data-table/index.ts b/apps/client/src/shared/ui/data-table/index.ts new file mode 100644 index 00000000..4336ee8b --- /dev/null +++ b/apps/client/src/shared/ui/data-table/index.ts @@ -0,0 +1 @@ +export { default as DataTable } from './DataTable.vue' diff --git a/apps/client/src/shared/ui/data-table/types.ts b/apps/client/src/shared/ui/data-table/types.ts new file mode 100644 index 00000000..13c93f4b --- /dev/null +++ b/apps/client/src/shared/ui/data-table/types.ts @@ -0,0 +1,66 @@ +import type { ColumnDef, GroupColumnDef } from '@tanstack/vue-table' +import type { HTMLAttributes } from 'vue' + +export interface TableProps extends TableRootProps { + data: TData[] | Set + columns: ColumnDef[] | GroupColumnDef[] + rowId?: string + autoResetAll?: boolean + enableRowSelection?: boolean + enableMultiRowSelection?: boolean + enableSubRowSelection?: boolean + enableColumnFilters?: boolean + enableSorting?: boolean + enableMultiSort?: boolean + enableMultiRemove?: boolean + enableSortingRemoval?: boolean + manualSorting?: boolean + maxMultiSortColCount?: number + manualPagination?: boolean + pageCount?: number + rowCount?: number + autoResetPageIndex?: boolean + sortingFns?: Record number> + sortDescFirst?: boolean + isMultiSortEvent?: (e: unknown) => boolean + + _tableHead?: TableHeadProps + _tableHeader?: TableHeaderProps + _tableBody?: TableBodyProps + _tableRow?: TableRowProps + _tableCell?: TableCellProps + _tableEmpty?: TableEmptyProps +} + +export interface TableBodyProps { + class?: HTMLAttributes['class'] +} + +export interface TableRootProps { + class?: HTMLAttributes['class'] +} + +export interface TableHeadProps { + class?: HTMLAttributes['class'] + dataPinned?: 'left' | 'right' | false +} + +export interface TableHeaderProps { + class?: HTMLAttributes['class'] +} + +export interface TableRowProps { + class?: HTMLAttributes['class'] +} + +export interface TableCellProps { + class?: HTMLAttributes['class'] + dataPinned?: 'left' | 'right' | false +} + +export interface TableEmptyProps { + class?: HTMLAttributes['class'] + colspan?: number + _tableCell?: TableCellProps + _tableRow?: TableRowProps +} diff --git a/apps/client/src/shared/ui/dialog/UIDialogDescription.vue b/apps/client/src/shared/ui/dialog/UIDialogDescription.vue new file mode 100644 index 00000000..ea0f4de8 --- /dev/null +++ b/apps/client/src/shared/ui/dialog/UIDialogDescription.vue @@ -0,0 +1,27 @@ + + + diff --git a/apps/client/src/shared/ui/dialog/UiDialog.stories.ts b/apps/client/src/shared/ui/dialog/UiDialog.stories.ts new file mode 100644 index 00000000..e6ac6e79 --- /dev/null +++ b/apps/client/src/shared/ui/dialog/UiDialog.stories.ts @@ -0,0 +1,36 @@ +import { UiButton } from '../button' +import { + UiDialog, + UiDialogContent, + UiDialogTrigger, +} from './index' +import type { Meta, StoryFn } from '@storybook/vue3' + +export default { + title: 'UiDialog', + component: UiDialog, +} as Meta + +const Template: StoryFn = args => ({ + components: { + UiDialog, + UiDialogTrigger, + UiDialogContent, + UiButton, + }, + setup() { + return { args } + }, + template: + ` + + Open + + + hello from dialog + + + `, +}) + +export const DefaultDialog: StoryFn = Template.bind({}) diff --git a/apps/client/src/shared/ui/dialog/UiDialog.vue b/apps/client/src/shared/ui/dialog/UiDialog.vue new file mode 100644 index 00000000..a04c0262 --- /dev/null +++ b/apps/client/src/shared/ui/dialog/UiDialog.vue @@ -0,0 +1,14 @@ + + + diff --git a/apps/client/src/shared/ui/dialog/UiDialogClose.vue b/apps/client/src/shared/ui/dialog/UiDialogClose.vue new file mode 100644 index 00000000..a64703e5 --- /dev/null +++ b/apps/client/src/shared/ui/dialog/UiDialogClose.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/client/src/shared/ui/dialog/UiDialogContent.vue b/apps/client/src/shared/ui/dialog/UiDialogContent.vue new file mode 100644 index 00000000..b42a9409 --- /dev/null +++ b/apps/client/src/shared/ui/dialog/UiDialogContent.vue @@ -0,0 +1,55 @@ + + + diff --git a/apps/client/src/shared/ui/dialog/UiDialogFooter.vue b/apps/client/src/shared/ui/dialog/UiDialogFooter.vue new file mode 100644 index 00000000..73fdee17 --- /dev/null +++ b/apps/client/src/shared/ui/dialog/UiDialogFooter.vue @@ -0,0 +1,19 @@ + + + diff --git a/apps/client/src/shared/ui/dialog/UiDialogHeader.vue b/apps/client/src/shared/ui/dialog/UiDialogHeader.vue new file mode 100644 index 00000000..7d05aa74 --- /dev/null +++ b/apps/client/src/shared/ui/dialog/UiDialogHeader.vue @@ -0,0 +1,19 @@ + + + diff --git a/apps/client/src/shared/ui/dialog/UiDialogScrollContent.vue b/apps/client/src/shared/ui/dialog/UiDialogScrollContent.vue new file mode 100644 index 00000000..3d3c6433 --- /dev/null +++ b/apps/client/src/shared/ui/dialog/UiDialogScrollContent.vue @@ -0,0 +1,61 @@ + + + diff --git a/apps/client/src/shared/ui/dialog/UiDialogTitle.vue b/apps/client/src/shared/ui/dialog/UiDialogTitle.vue new file mode 100644 index 00000000..891a060a --- /dev/null +++ b/apps/client/src/shared/ui/dialog/UiDialogTitle.vue @@ -0,0 +1,29 @@ + + + diff --git a/apps/client/src/shared/ui/dialog/UiDialogTrigger.vue b/apps/client/src/shared/ui/dialog/UiDialogTrigger.vue new file mode 100644 index 00000000..ee0c12ff --- /dev/null +++ b/apps/client/src/shared/ui/dialog/UiDialogTrigger.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/client/src/shared/ui/dialog/__tests__/MockComponent.vue b/apps/client/src/shared/ui/dialog/__tests__/MockComponent.vue new file mode 100644 index 00000000..de53d7f2 --- /dev/null +++ b/apps/client/src/shared/ui/dialog/__tests__/MockComponent.vue @@ -0,0 +1,34 @@ + + + diff --git a/apps/client/src/shared/ui/dialog/__tests__/UiDialog.spec.ts b/apps/client/src/shared/ui/dialog/__tests__/UiDialog.spec.ts new file mode 100644 index 00000000..2ede07f9 --- /dev/null +++ b/apps/client/src/shared/ui/dialog/__tests__/UiDialog.spec.ts @@ -0,0 +1,55 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import { UiDialog } from '..' +import DialogTest from './MockComponent.vue' +import type { ComponentPublicInstance } from 'vue' +import type { VueWrapper } from '@vue/test-utils' + +const OPEN_TEXT = 'open' +const TITLE_TEXT = 'title' + +type DialogTestInstance = ComponentPublicInstance< + {}, + {}, + { + isOpen: boolean + } +> + +describe('tests for UiDialog', () => { + it('should render correctly with default slot', () => { + const wrapper = mount(UiDialog, { + slots: { default: '
dialog
' }, + props: { defaultOpen: true }, + }) as VueWrapper + + expect(wrapper.html()).toContain('
dialog
') + }) + + it('should render dialog components correctly', async () => { + const wrapper = mount(DialogTest, { + global: { + stubs: { teleport: true }, + }, + }) as VueWrapper + + expect(wrapper.html()).toContain(OPEN_TEXT) + expect(wrapper.html()).not.toContain(TITLE_TEXT) + + wrapper.vm.isOpen = true + expect(wrapper.vm.isOpen).toBe(true) + }) + + it('should pass defaultOpen prop to DialogMenuRoot', () => { + const wrapper = mount(UiDialog, { props: { defaultOpen: true } }) + const root = wrapper.findComponent({ name: 'DialogRoot' }) + expect(root.props('defaultOpen')).toBe(true) + }) + + it('should emit openChange events correctly', () => { + const wrapper = mount(UiDialog) + wrapper.vm.$emit('openChange', true) + expect(wrapper.emitted('openChange')).toBeTruthy() + expect(wrapper.emitted('openChange')?.[0]).toEqual([true]) + }) +}) diff --git a/apps/client/src/shared/ui/dialog/index.ts b/apps/client/src/shared/ui/dialog/index.ts new file mode 100644 index 00000000..1b6d3312 --- /dev/null +++ b/apps/client/src/shared/ui/dialog/index.ts @@ -0,0 +1,9 @@ +export { default as UiDialog } from './UiDialog.vue' +export { default as UiDialogClose } from './UiDialogClose.vue' +export { default as UiDialogContent } from './UiDialogContent.vue' +export { default as UiDialogDescription } from './UIDialogDescription.vue' +export { default as UiDialogFooter } from './UiDialogFooter.vue' +export { default as UiDialogHeader } from './UiDialogHeader.vue' +export { default as UiDialogScrollContent } from './UiDialogScrollContent.vue' +export { default as UiDialogTitle } from './UiDialogTitle.vue' +export { default as UiDialogTrigger } from './UiDialogTrigger.vue' diff --git a/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenu.stories.ts b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenu.stories.ts new file mode 100644 index 00000000..b9869529 --- /dev/null +++ b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenu.stories.ts @@ -0,0 +1,41 @@ +import { UiButton } from '../button' +import { + UiDropdownMenu, + UiDropdownMenuContent, + UiDropdownMenuItem, + UiDropdownMenuTrigger, +} from './index' +import type { Meta, StoryFn } from '@storybook/vue3' + +export default { + title: 'UiDropdownMenu', + component: UiDropdownMenu, +} as Meta + +const Template: StoryFn = args => ({ + components: { + UiDropdownMenu, + UiDropdownMenuItem, + UiDropdownMenuTrigger, + UiDropdownMenuContent, + UiButton, + }, + setup() { + return { args } + }, + template: `
+ + + Open + + + Profile + Billing + Team + Subscription + + +
`, +}) + +export const DefaultDropdown: StoryFn = Template.bind({}) diff --git a/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenu.vue b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenu.vue new file mode 100644 index 00000000..b83d90b8 --- /dev/null +++ b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenu.vue @@ -0,0 +1,14 @@ + + + diff --git a/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuCheckboxItem.vue b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuCheckboxItem.vue new file mode 100644 index 00000000..6ecee08b --- /dev/null +++ b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuCheckboxItem.vue @@ -0,0 +1,39 @@ + + + diff --git a/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuContent.vue b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuContent.vue new file mode 100644 index 00000000..867b6136 --- /dev/null +++ b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuContent.vue @@ -0,0 +1,45 @@ + + + diff --git a/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuGroup.vue b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuGroup.vue new file mode 100644 index 00000000..3f201352 --- /dev/null +++ b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuGroup.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuItem.vue b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuItem.vue new file mode 100644 index 00000000..269a2c37 --- /dev/null +++ b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuItem.vue @@ -0,0 +1,28 @@ + + + diff --git a/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuLabel.vue b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuLabel.vue new file mode 100644 index 00000000..717fe40c --- /dev/null +++ b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuLabel.vue @@ -0,0 +1,28 @@ + + + diff --git a/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuSeparator.vue b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuSeparator.vue new file mode 100644 index 00000000..22f1dcf6 --- /dev/null +++ b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuSeparator.vue @@ -0,0 +1,28 @@ + + + diff --git a/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuShortcut.vue b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuShortcut.vue new file mode 100644 index 00000000..9db3e1fc --- /dev/null +++ b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuShortcut.vue @@ -0,0 +1,19 @@ + + + diff --git a/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuSub.vue b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuSub.vue new file mode 100644 index 00000000..e0f4bd77 --- /dev/null +++ b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuSub.vue @@ -0,0 +1,19 @@ + + + diff --git a/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuSubContent.vue b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuSubContent.vue new file mode 100644 index 00000000..0a8ee3bb --- /dev/null +++ b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuSubContent.vue @@ -0,0 +1,33 @@ + + + diff --git a/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuSubTrigger.vue b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuSubTrigger.vue new file mode 100644 index 00000000..4dfc4f91 --- /dev/null +++ b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuSubTrigger.vue @@ -0,0 +1,32 @@ + + + diff --git a/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuTrigger.vue b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuTrigger.vue new file mode 100644 index 00000000..c7d632a4 --- /dev/null +++ b/apps/client/src/shared/ui/dropdown-menu/UiDropdownMenuTrigger.vue @@ -0,0 +1,16 @@ + + + diff --git a/apps/client/src/shared/ui/dropdown-menu/__tests__/UiDropdownMenu.spec.ts b/apps/client/src/shared/ui/dropdown-menu/__tests__/UiDropdownMenu.spec.ts new file mode 100644 index 00000000..8ad33425 --- /dev/null +++ b/apps/client/src/shared/ui/dropdown-menu/__tests__/UiDropdownMenu.spec.ts @@ -0,0 +1,27 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import UiDropdownMenu from '../UiDropdownMenu.vue' + +describe('tests for UiDropdownMenu', () => { + const wrapper = mount(UiDropdownMenu, { + slots: { default: '
test
' }, + props: { defaultOpen: true }, + }) + + it('should render correctly', () => { + expect(wrapper.html()).toContain('
test
') + }) + + it('should pass defaultOpen prop to DropdownMenuRoot', () => { + const root = wrapper.findComponent({ name: 'DropdownMenuRoot' }) + expect(root.props('defaultOpen')).toBe(true) + }) + + it('should emit events correctly', () => { + wrapper.vm.$emit('openChange', true) + expect(wrapper.emitted('openChange')).toBeTruthy() + expect(wrapper.emitted('openChange')?.[0]).toEqual([true]) + }) +}) + +// more tests: https://github.com/unovue/radix-vue/blob/main/packages/radix-vue/src/DropdownMenu/DropdownMenu.test.ts diff --git a/apps/client/src/shared/ui/dropdown-menu/index.ts b/apps/client/src/shared/ui/dropdown-menu/index.ts new file mode 100644 index 00000000..b0cde6e4 --- /dev/null +++ b/apps/client/src/shared/ui/dropdown-menu/index.ts @@ -0,0 +1,13 @@ +export { default as UiDropdownMenu } from './UiDropdownMenu.vue' +export { default as UiDropdownMenuCheckboxItem } from './UiDropdownMenuCheckboxItem.vue' +export { default as UiDropdownMenuContent } from './UiDropdownMenuContent.vue' +export { default as UiDropdownMenuGroup } from './UiDropdownMenuGroup.vue' +export { default as UiDropdownMenuItem } from './UiDropdownMenuItem.vue' +export { default as UiDropdownMenuLabel } from './UiDropdownMenuLabel.vue' +export { default as UiDropdownMenuSeparator } from './UiDropdownMenuSeparator.vue' +export { default as UiDropdownMenuShortcut } from './UiDropdownMenuShortcut.vue' +export { default as UiDropdownMenuSub } from './UiDropdownMenuSub.vue' +export { default as UiDropdownMenuSubContent } from './UiDropdownMenuSubContent.vue' +export { default as UiDropdownMenuSubTrigger } from './UiDropdownMenuSubTrigger.vue' +export { default as UiDropdownMenuTrigger } from './UiDropdownMenuTrigger.vue' +export { DropdownMenuPortal } from 'radix-vue' diff --git a/apps/client/src/shared/ui/form/UiForm.stories.ts b/apps/client/src/shared/ui/form/UiForm.stories.ts new file mode 100644 index 00000000..2282a6d7 --- /dev/null +++ b/apps/client/src/shared/ui/form/UiForm.stories.ts @@ -0,0 +1,55 @@ +import { + UiButton, + UiFormField, + UiFormLabel, + UiFormMessage, + UiInput, +} from '..' +import type { Meta, StoryFn } from '@storybook/vue3' + +export default { + title: 'UiForm', +} as Meta + +const Template: StoryFn = (args) => { + return { + components: { UiFormField, UiFormLabel, UiFormMessage, UiButton, UiInput }, + setup() { + return { args } + }, + template: ` + + + Test + + `, + } +} + +export const DefaultFormField: StoryFn = Template.bind({}) + +DefaultFormField.args = { + default: ` +
+ + Label + + + +
+ `, +} + +export const WarningFormField: StoryFn = Template.bind({}) + +WarningFormField.args = { + default: ` +
+ + Label + + + +
+ `, +} diff --git a/apps/client/src/shared/ui/form/UiFormField.vue b/apps/client/src/shared/ui/form/UiFormField.vue new file mode 100644 index 00000000..4e3c775d --- /dev/null +++ b/apps/client/src/shared/ui/form/UiFormField.vue @@ -0,0 +1,16 @@ + + + diff --git a/apps/client/src/shared/ui/form/UiFormLabel.vue b/apps/client/src/shared/ui/form/UiFormLabel.vue new file mode 100644 index 00000000..db14f995 --- /dev/null +++ b/apps/client/src/shared/ui/form/UiFormLabel.vue @@ -0,0 +1,25 @@ + + + diff --git a/apps/client/src/shared/ui/form/UiFormMessage.vue b/apps/client/src/shared/ui/form/UiFormMessage.vue new file mode 100644 index 00000000..435c57d5 --- /dev/null +++ b/apps/client/src/shared/ui/form/UiFormMessage.vue @@ -0,0 +1,41 @@ + + + diff --git a/apps/client/src/shared/ui/form/__tests__/MockComponent.vue b/apps/client/src/shared/ui/form/__tests__/MockComponent.vue new file mode 100644 index 00000000..fd4f0ac4 --- /dev/null +++ b/apps/client/src/shared/ui/form/__tests__/MockComponent.vue @@ -0,0 +1,21 @@ + + + diff --git a/apps/client/src/shared/ui/form/__tests__/UiForm.spec.ts b/apps/client/src/shared/ui/form/__tests__/UiForm.spec.ts new file mode 100644 index 00000000..27272090 --- /dev/null +++ b/apps/client/src/shared/ui/form/__tests__/UiForm.spec.ts @@ -0,0 +1,19 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import UiForm from './MockComponent.vue' + +describe('tests for UiForm', () => { + it('should render correctly', () => { + const wrapper = mount(UiForm) + + const label = wrapper.findComponent({ name: 'UiFormLabel' }) + expect(label.exists()).toBe(true) + expect(label.text()).toBe('Label') + + const message = wrapper.findComponent({ name: 'UiFormMessage' }) + expect(message.exists()).toBe(true) + expect(message.props('content')).toBe('test message') + }) +}) + +// more tests in stories diff --git a/apps/client/src/shared/ui/form/index.ts b/apps/client/src/shared/ui/form/index.ts new file mode 100644 index 00000000..6439979a --- /dev/null +++ b/apps/client/src/shared/ui/form/index.ts @@ -0,0 +1,3 @@ +export { default as UiFormField } from './UiFormField.vue' +export { default as UiFormLabel } from './UiFormLabel.vue' +export { default as UiFormMessage } from './UiFormMessage.vue' diff --git a/apps/client/src/shared/ui/index.ts b/apps/client/src/shared/ui/index.ts new file mode 100644 index 00000000..a068d4fc --- /dev/null +++ b/apps/client/src/shared/ui/index.ts @@ -0,0 +1,21 @@ +export * from './alert' +export * from './badge' +export * from './button' +export * from './card' +export * from './checkbox' +export * from './command' +export * from './data-table' +export * from './dialog' +export * from './dropdown-menu' +export * from './dropdown-menu' +export * from './form' +export * from './input' +export * from './pagination' +export * from './picker' +export * from './pin-input' +export * from './popover' +export * from './select' +export * from './shimmer-button' +export * from './table' +export * from './tabs' +export * from './tags-input' diff --git a/apps/client/src/shared/ui/input/UiInput.stories.ts b/apps/client/src/shared/ui/input/UiInput.stories.ts new file mode 100644 index 00000000..2a360bd4 --- /dev/null +++ b/apps/client/src/shared/ui/input/UiInput.stories.ts @@ -0,0 +1,32 @@ +import UiInput from './UiInput.vue' +import type { Meta, StoryFn } from '@storybook/vue3' + +export default { + title: 'UiInput', + component: UiInput, + argTypes: { + modelValue: { + control: { type: 'text' }, + }, + type: { + control: { type: 'select' }, + options: ['text', 'password', 'number', 'email'], + defaultValue: 'text', + }, + placeholder: { + control: { type: 'text' }, + }, + }, +} as Meta + +const Template: StoryFn = (args) => { + return { + components: { UiInput }, + setup() { + return { args } + }, + template: '', + } +} + +export const DefaultInput: StoryFn = Template.bind({}) diff --git a/apps/client/src/shared/ui/input/UiInput.vue b/apps/client/src/shared/ui/input/UiInput.vue new file mode 100644 index 00000000..9a707443 --- /dev/null +++ b/apps/client/src/shared/ui/input/UiInput.vue @@ -0,0 +1,30 @@ + + + diff --git a/apps/client/src/shared/ui/input/__tests__/UiInput.spec.ts b/apps/client/src/shared/ui/input/__tests__/UiInput.spec.ts new file mode 100644 index 00000000..5f69466c --- /dev/null +++ b/apps/client/src/shared/ui/input/__tests__/UiInput.spec.ts @@ -0,0 +1,23 @@ +import { shallowMount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import UiInput from '../UiInput.vue' + +describe('tests for UiInput', () => { + const wrapper = shallowMount(UiInput, { + props: { + modelValue: 'value', + placeholder: 'jenda', + type: 'text', + }, + }) + + it('should render correctly', () => { + expect(wrapper.html()).toMatchSnapshot() + }) + + it('should update value with @input', async () => { + const input = wrapper.find('input') + await input.setValue('new') + expect(wrapper.emitted('update:modelValue')![0]).toEqual(['new']) + }) +}) diff --git a/apps/client/src/shared/ui/input/__tests__/__snapshots__/UiInput.spec.ts.snap b/apps/client/src/shared/ui/input/__tests__/__snapshots__/UiInput.spec.ts.snap new file mode 100644 index 00000000..11d9a0c9 --- /dev/null +++ b/apps/client/src/shared/ui/input/__tests__/__snapshots__/UiInput.spec.ts.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`tests for UiInput > should render correctly 1`] = `""`; diff --git a/apps/client/src/shared/ui/input/index.ts b/apps/client/src/shared/ui/input/index.ts new file mode 100644 index 00000000..ad141f55 --- /dev/null +++ b/apps/client/src/shared/ui/input/index.ts @@ -0,0 +1 @@ +export { default as UiInput } from './UiInput.vue' diff --git a/apps/client/src/shared/ui/pagination/UiPaginationEllipsis.vue b/apps/client/src/shared/ui/pagination/UiPaginationEllipsis.vue new file mode 100644 index 00000000..cb70cb97 --- /dev/null +++ b/apps/client/src/shared/ui/pagination/UiPaginationEllipsis.vue @@ -0,0 +1,27 @@ + + + diff --git a/apps/client/src/shared/ui/pagination/UiPaginationFirst.vue b/apps/client/src/shared/ui/pagination/UiPaginationFirst.vue new file mode 100644 index 00000000..f7bccedb --- /dev/null +++ b/apps/client/src/shared/ui/pagination/UiPaginationFirst.vue @@ -0,0 +1,35 @@ + + + diff --git a/apps/client/src/shared/ui/pagination/UiPaginationLast.vue b/apps/client/src/shared/ui/pagination/UiPaginationLast.vue new file mode 100644 index 00000000..a965a69d --- /dev/null +++ b/apps/client/src/shared/ui/pagination/UiPaginationLast.vue @@ -0,0 +1,35 @@ + + + diff --git a/apps/client/src/shared/ui/pagination/UiPaginationNext.vue b/apps/client/src/shared/ui/pagination/UiPaginationNext.vue new file mode 100644 index 00000000..a79cc127 --- /dev/null +++ b/apps/client/src/shared/ui/pagination/UiPaginationNext.vue @@ -0,0 +1,35 @@ + + + diff --git a/apps/client/src/shared/ui/pagination/UiPaginationPrev.vue b/apps/client/src/shared/ui/pagination/UiPaginationPrev.vue new file mode 100644 index 00000000..96a7b2a6 --- /dev/null +++ b/apps/client/src/shared/ui/pagination/UiPaginationPrev.vue @@ -0,0 +1,35 @@ + + + diff --git a/apps/client/src/shared/ui/pagination/__tests__/MockComponent.vue b/apps/client/src/shared/ui/pagination/__tests__/MockComponent.vue new file mode 100644 index 00000000..1c63e03b --- /dev/null +++ b/apps/client/src/shared/ui/pagination/__tests__/MockComponent.vue @@ -0,0 +1,29 @@ + + + diff --git a/apps/client/src/shared/ui/pagination/__tests__/UiPagination.spec.ts b/apps/client/src/shared/ui/pagination/__tests__/UiPagination.spec.ts new file mode 100644 index 00000000..6d0e3ae3 --- /dev/null +++ b/apps/client/src/shared/ui/pagination/__tests__/UiPagination.spec.ts @@ -0,0 +1,26 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import MockComponent from './MockComponent.vue' + +describe('tests for UiPagination', () => { + const wrapper = mount(MockComponent, { + props: { + total: 50, + itemsPerPage: 10, + page: 1, + }, + }) + + it('should render correctly', () => { + expect(wrapper.html()).toMatchSnapshot() + }) + + it('should emit update:page when page is changed to + 1', async () => { + const nextBtn = wrapper.findComponent({ name: 'UiPaginationNext' }) + await nextBtn.trigger('click') + expect(wrapper.emitted('update:page')).toBeTruthy() + expect(wrapper.emitted('update:page')?.[0]).toEqual([2]) + }) +}) + +// more tests: https://github.com/unovue/radix-vue/blob/main/packages/radix-vue/src/Pagination/Pagination.test.ts diff --git a/apps/client/src/shared/ui/pagination/__tests__/__snapshots__/UiPagination.spec.ts.snap b/apps/client/src/shared/ui/pagination/__tests__/__snapshots__/UiPagination.spec.ts.snap new file mode 100644 index 00000000..d41039e5 --- /dev/null +++ b/apps/client/src/shared/ui/pagination/__tests__/__snapshots__/UiPagination.spec.ts.snap @@ -0,0 +1,15 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`tests for UiPagination > should render correctly 1`] = ` +"" +`; diff --git a/apps/client/src/shared/ui/pagination/index.ts b/apps/client/src/shared/ui/pagination/index.ts new file mode 100644 index 00000000..86bc4a3d --- /dev/null +++ b/apps/client/src/shared/ui/pagination/index.ts @@ -0,0 +1,10 @@ +export { default as UiPaginationEllipsis } from './UiPaginationEllipsis.vue' +export { default as UiPaginationFirst } from './UiPaginationFirst.vue' +export { default as UiPaginationLast } from './UiPaginationLast.vue' +export { default as UiPaginationNext } from './UiPaginationNext.vue' +export { default as UiPaginationPrev } from './UiPaginationPrev.vue' +export { + PaginationRoot as UiPagination, + PaginationList as UiPaginationList, + PaginationListItem as UiPaginationListItem, +} from 'radix-vue' diff --git a/apps/client/src/shared/ui/picker/UiColorPicker.vue b/apps/client/src/shared/ui/picker/UiColorPicker.vue new file mode 100644 index 00000000..7e788c49 --- /dev/null +++ b/apps/client/src/shared/ui/picker/UiColorPicker.vue @@ -0,0 +1,64 @@ + + + diff --git a/apps/client/src/shared/ui/picker/index.ts b/apps/client/src/shared/ui/picker/index.ts new file mode 100644 index 00000000..d91bd0ec --- /dev/null +++ b/apps/client/src/shared/ui/picker/index.ts @@ -0,0 +1 @@ +export { default as UiColorPicker } from './UiColorPicker.vue' diff --git a/apps/client/src/shared/ui/pin-input/UiPinInput.stories.ts b/apps/client/src/shared/ui/pin-input/UiPinInput.stories.ts new file mode 100644 index 00000000..953f4912 --- /dev/null +++ b/apps/client/src/shared/ui/pin-input/UiPinInput.stories.ts @@ -0,0 +1,71 @@ +import { + UiPinInput, + UiPinInputGroup, + UiPinInputInput, + UiPinInputSeparator, +} from '..' +import type { Meta, StoryFn } from '@storybook/vue3' + +export default { + title: 'UiPinInput', + component: UiPinInput, + argTypes: { + modelValue: { + control: { type: 'text' }, + }, + placeholder: { + control: { type: 'text' }, + }, + }, +} as Meta + +const Template: StoryFn = (args) => { + return { + components: { UiPinInput, UiPinInputGroup, UiPinInputInput, UiPinInputSeparator }, + setup() { + return { args } + }, + template: ` + + + + `, + } +} + +export const DefaultPinInput: StoryFn = Template.bind({}) + +DefaultPinInput.args = { + default: ` + + + + `, + placeholder: '○', +} + +export const PinInputWithSeparator: StoryFn = Template.bind({}) + +PinInputWithSeparator.args = { + default: ` + + + + `, + placeholder: '', +} diff --git a/apps/client/src/shared/ui/pin-input/UiPinInput.vue b/apps/client/src/shared/ui/pin-input/UiPinInput.vue new file mode 100644 index 00000000..47e9d465 --- /dev/null +++ b/apps/client/src/shared/ui/pin-input/UiPinInput.vue @@ -0,0 +1,29 @@ + + + diff --git a/apps/client/src/shared/ui/pin-input/UiPinInputGroup.vue b/apps/client/src/shared/ui/pin-input/UiPinInputGroup.vue new file mode 100644 index 00000000..08963a7f --- /dev/null +++ b/apps/client/src/shared/ui/pin-input/UiPinInputGroup.vue @@ -0,0 +1,24 @@ + + + diff --git a/apps/client/src/shared/ui/pin-input/UiPinInputInput.vue b/apps/client/src/shared/ui/pin-input/UiPinInputInput.vue new file mode 100644 index 00000000..6677c08e --- /dev/null +++ b/apps/client/src/shared/ui/pin-input/UiPinInputInput.vue @@ -0,0 +1,24 @@ + + + diff --git a/apps/client/src/shared/ui/pin-input/UiPinInputSeparator.vue b/apps/client/src/shared/ui/pin-input/UiPinInputSeparator.vue new file mode 100644 index 00000000..42681824 --- /dev/null +++ b/apps/client/src/shared/ui/pin-input/UiPinInputSeparator.vue @@ -0,0 +1,14 @@ + + + diff --git a/apps/client/src/shared/ui/pin-input/__tests__/MockComponent.vue b/apps/client/src/shared/ui/pin-input/__tests__/MockComponent.vue new file mode 100644 index 00000000..9103d8dd --- /dev/null +++ b/apps/client/src/shared/ui/pin-input/__tests__/MockComponent.vue @@ -0,0 +1,26 @@ + + + diff --git a/apps/client/src/shared/ui/pin-input/__tests__/UiPinInput.spec.ts b/apps/client/src/shared/ui/pin-input/__tests__/UiPinInput.spec.ts new file mode 100644 index 00000000..d1e59785 --- /dev/null +++ b/apps/client/src/shared/ui/pin-input/__tests__/UiPinInput.spec.ts @@ -0,0 +1,30 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import UiPinInput from './MockComponent.vue' + +describe('tests for UiPinInput', () => { + const wrapper = mount(UiPinInput, { + props: { + placeholder: '○', + type: 'text', + modelValue: ['', '', '', '', ''], + }, + }) + it('should render correctly', () => { + expect(wrapper.html()).toMatchSnapshot() + }) + + it('should emit the correct event on input', async () => { + wrapper.vm.$emit('update:modelValue', ['1', '2', '3']) + expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([['1', '2', '3']]) + }) + + it('should emit "complete" event', async () => { + const inputs = wrapper.findAll('.pin-input-input') + for (let i = 0; i < inputs.length; i++) { + await inputs[i].setValue((i + 1).toString()) + } + expect(wrapper.emitted('complete')).toBeTruthy() + expect(wrapper.emitted('complete')?.[0]).toEqual([['1', '2', '3', '4', '5']]) + }) +}) diff --git a/apps/client/src/shared/ui/pin-input/__tests__/__snapshots__/UiPinInput.spec.ts.snap b/apps/client/src/shared/ui/pin-input/__tests__/__snapshots__/UiPinInput.spec.ts.snap new file mode 100644 index 00000000..2f73e493 --- /dev/null +++ b/apps/client/src/shared/ui/pin-input/__tests__/__snapshots__/UiPinInput.spec.ts.snap @@ -0,0 +1,6 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`tests for UiPinInput > should render correctly 1`] = ` +"
+" +`; diff --git a/apps/client/src/shared/ui/pin-input/index.ts b/apps/client/src/shared/ui/pin-input/index.ts new file mode 100644 index 00000000..5a4144d7 --- /dev/null +++ b/apps/client/src/shared/ui/pin-input/index.ts @@ -0,0 +1,4 @@ +export { default as UiPinInput } from './UiPinInput.vue' +export { default as UiPinInputGroup } from './UiPinInputGroup.vue' +export { default as UiPinInputInput } from './UiPinInputInput.vue' +export { default as UiPinInputSeparator } from './UiPinInputSeparator.vue' diff --git a/apps/client/src/shared/ui/popover/UiPopover.stories.ts b/apps/client/src/shared/ui/popover/UiPopover.stories.ts new file mode 100644 index 00000000..328cbdb3 --- /dev/null +++ b/apps/client/src/shared/ui/popover/UiPopover.stories.ts @@ -0,0 +1,39 @@ +import { PopoverRoot } from 'radix-vue' +import { UiButton } from '../button' +import { + UiPopover, + UiPopoverContent, + UiPopoverTrigger, +} from './index' +import type { Meta, StoryFn } from '@storybook/vue3' + +export default { + title: 'UiPopover', + component: UiPopover, +} as Meta + +const Template: StoryFn = args => ({ + components: { + PopoverRoot, + UiPopover, + UiPopoverContent, + UiPopoverTrigger, + UiButton, + }, + setup() { + return { args } + }, + template: ` +
+ + + Open + + + hello + + +
`, +}) + +export const DefaultPopover: StoryFn = Template.bind({}) diff --git a/apps/client/src/shared/ui/popover/UiPopover.vue b/apps/client/src/shared/ui/popover/UiPopover.vue new file mode 100644 index 00000000..1a5873a3 --- /dev/null +++ b/apps/client/src/shared/ui/popover/UiPopover.vue @@ -0,0 +1,15 @@ + + + diff --git a/apps/client/src/shared/ui/popover/UiPopoverContent.vue b/apps/client/src/shared/ui/popover/UiPopoverContent.vue new file mode 100644 index 00000000..f2e53bc8 --- /dev/null +++ b/apps/client/src/shared/ui/popover/UiPopoverContent.vue @@ -0,0 +1,48 @@ + + + diff --git a/apps/client/src/shared/ui/popover/UiPopoverTrigger.vue b/apps/client/src/shared/ui/popover/UiPopoverTrigger.vue new file mode 100644 index 00000000..22f4772a --- /dev/null +++ b/apps/client/src/shared/ui/popover/UiPopoverTrigger.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/client/src/shared/ui/popover/__tests__/MockComponent.vue b/apps/client/src/shared/ui/popover/__tests__/MockComponent.vue new file mode 100644 index 00000000..2a254820 --- /dev/null +++ b/apps/client/src/shared/ui/popover/__tests__/MockComponent.vue @@ -0,0 +1,21 @@ + + + diff --git a/apps/client/src/shared/ui/popover/__tests__/UiPopover.spec.ts b/apps/client/src/shared/ui/popover/__tests__/UiPopover.spec.ts new file mode 100644 index 00000000..115c20e0 --- /dev/null +++ b/apps/client/src/shared/ui/popover/__tests__/UiPopover.spec.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import MockComponent from './MockComponent.vue' + +describe('tests for UiPopover', () => { + const wrapper = mount(MockComponent, { + props: { + defaultOpen: true, + modal: false, + }, + }) + it('should render correctly', () => { + expect(wrapper.html()) + }) + + it('should pass props to PopoverRoot', () => { + const root = wrapper.findComponent({ name: 'PopoverRoot' }) + expect(root.props('defaultOpen')).toBe(true) + expect(root.props('modal')).toBe(false) + }) + + it('should emit events correctly', () => { + wrapper.vm.$emit('openChange', true) + expect(wrapper.emitted('openChange')).toBeTruthy() + expect(wrapper.emitted('openChange')?.[0]).toEqual([true]) + }) +}) + +// more tests: https://github.com/unovue/radix-vue/blob/main/packages/radix-vue/src/Popover/Popover.test.ts diff --git a/apps/client/src/shared/ui/popover/index.ts b/apps/client/src/shared/ui/popover/index.ts new file mode 100644 index 00000000..c9459bf9 --- /dev/null +++ b/apps/client/src/shared/ui/popover/index.ts @@ -0,0 +1,4 @@ +export { default as UiPopover } from './UiPopover.vue' +export { default as UiPopoverContent } from './UiPopoverContent.vue' +export { default as UiPopoverTrigger } from './UiPopoverTrigger.vue' +export { PopoverAnchor } from 'radix-vue' diff --git a/apps/client/src/shared/ui/select/UiSelect.stories.ts b/apps/client/src/shared/ui/select/UiSelect.stories.ts new file mode 100644 index 00000000..ce580526 --- /dev/null +++ b/apps/client/src/shared/ui/select/UiSelect.stories.ts @@ -0,0 +1,49 @@ +import { UiButton } from '../button' +import { + UiSelect, + UiSelectContent, + UiSelectGroup, + UiSelectItem, + UiSelectLabel, + UiSelectTrigger, + UiSelectValue, +} from './index' +import type { Meta, StoryFn } from '@storybook/vue3' + +export default { + title: 'UiSelect', + component: UiSelect, +} as Meta + +const Template: StoryFn = args => ({ + components: { + UiSelect, + UiSelectContent, + UiSelectGroup, + UiSelectItem, + UiSelectLabel, + UiSelectTrigger, + UiSelectValue, + UiButton, + }, + setup() { + return { args } + }, + template: `
+ + + + + + + Fruits + + Apple + + + + +
`, +}) + +export const DefaultSelect: StoryFn = Template.bind({}) diff --git a/apps/client/src/shared/ui/select/UiSelect.vue b/apps/client/src/shared/ui/select/UiSelect.vue new file mode 100644 index 00000000..46597625 --- /dev/null +++ b/apps/client/src/shared/ui/select/UiSelect.vue @@ -0,0 +1,15 @@ + + + diff --git a/apps/client/src/shared/ui/select/UiSelectContent.vue b/apps/client/src/shared/ui/select/UiSelectContent.vue new file mode 100644 index 00000000..5b3ec53a --- /dev/null +++ b/apps/client/src/shared/ui/select/UiSelectContent.vue @@ -0,0 +1,54 @@ + + + diff --git a/apps/client/src/shared/ui/select/UiSelectGroup.vue b/apps/client/src/shared/ui/select/UiSelectGroup.vue new file mode 100644 index 00000000..dcc2e66e --- /dev/null +++ b/apps/client/src/shared/ui/select/UiSelectGroup.vue @@ -0,0 +1,25 @@ + + + diff --git a/apps/client/src/shared/ui/select/UiSelectItem.vue b/apps/client/src/shared/ui/select/UiSelectItem.vue new file mode 100644 index 00000000..33a68fc9 --- /dev/null +++ b/apps/client/src/shared/ui/select/UiSelectItem.vue @@ -0,0 +1,52 @@ + + + diff --git a/apps/client/src/shared/ui/select/UiSelectItemText.vue b/apps/client/src/shared/ui/select/UiSelectItemText.vue new file mode 100644 index 00000000..a0bb5c24 --- /dev/null +++ b/apps/client/src/shared/ui/select/UiSelectItemText.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/client/src/shared/ui/select/UiSelectLabel.vue b/apps/client/src/shared/ui/select/UiSelectLabel.vue new file mode 100644 index 00000000..4f136e90 --- /dev/null +++ b/apps/client/src/shared/ui/select/UiSelectLabel.vue @@ -0,0 +1,18 @@ + + + diff --git a/apps/client/src/shared/ui/select/UiSelectScrollDownButton.vue b/apps/client/src/shared/ui/select/UiSelectScrollDownButton.vue new file mode 100644 index 00000000..1835d4ef --- /dev/null +++ b/apps/client/src/shared/ui/select/UiSelectScrollDownButton.vue @@ -0,0 +1,29 @@ + + + diff --git a/apps/client/src/shared/ui/select/UiSelectScrollUpButton.vue b/apps/client/src/shared/ui/select/UiSelectScrollUpButton.vue new file mode 100644 index 00000000..256319d3 --- /dev/null +++ b/apps/client/src/shared/ui/select/UiSelectScrollUpButton.vue @@ -0,0 +1,29 @@ + + + diff --git a/apps/client/src/shared/ui/select/UiSelectSeparator.vue b/apps/client/src/shared/ui/select/UiSelectSeparator.vue new file mode 100644 index 00000000..a472251a --- /dev/null +++ b/apps/client/src/shared/ui/select/UiSelectSeparator.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/client/src/shared/ui/select/UiSelectTrigger.vue b/apps/client/src/shared/ui/select/UiSelectTrigger.vue new file mode 100644 index 00000000..8af5e166 --- /dev/null +++ b/apps/client/src/shared/ui/select/UiSelectTrigger.vue @@ -0,0 +1,27 @@ + + + diff --git a/apps/client/src/shared/ui/select/UiSelectValue.vue b/apps/client/src/shared/ui/select/UiSelectValue.vue new file mode 100644 index 00000000..4bc37dd8 --- /dev/null +++ b/apps/client/src/shared/ui/select/UiSelectValue.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/client/src/shared/ui/select/__tests__/UiSelect.spec.ts b/apps/client/src/shared/ui/select/__tests__/UiSelect.spec.ts new file mode 100644 index 00000000..fb05aa51 --- /dev/null +++ b/apps/client/src/shared/ui/select/__tests__/UiSelect.spec.ts @@ -0,0 +1,27 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import UiSelect from '../UiSelect.vue' + +describe('tests for UiSelect', () => { + const wrapper = mount(UiSelect, { + slots: { default: '
test select
' }, + props: { defaultOpen: true }, + }) + + it('should render correctly', () => { + expect(wrapper.html()).toContain('
test select
') + }) + + it('should pass defaultOpen prop to DropdownMenuRoot', () => { + const root = wrapper.findComponent({ name: 'SelectRoot' }) + expect(root.props('defaultOpen')).toBe(true) + }) + + it('should emit events correctly', () => { + wrapper.vm.$emit('openChange', true) + expect(wrapper.emitted('openChange')).toBeTruthy() + expect(wrapper.emitted('openChange')?.[0]).toEqual([true]) + }) +}) + +// more tests: https://github.com/unovue/radix-vue/blob/main/packages/radix-vue/src/Select/Select.test.ts diff --git a/apps/client/src/shared/ui/select/index.ts b/apps/client/src/shared/ui/select/index.ts new file mode 100644 index 00000000..a75c0864 --- /dev/null +++ b/apps/client/src/shared/ui/select/index.ts @@ -0,0 +1,11 @@ +export { default as UiSelect } from './UiSelect.vue' +export { default as UiSelectContent } from './UiSelectContent.vue' +export { default as UiSelectGroup } from './UiSelectGroup.vue' +export { default as UiSelectItem } from './UiSelectItem.vue' +export { default as UiSelectItemText } from './UiSelectItemText.vue' +export { default as UiSelectLabel } from './UiSelectLabel.vue' +export { default as UiSelectScrollDownButton } from './UiSelectScrollDownButton.vue' +export { default as UiSelectScrollUpButton } from './UiSelectScrollUpButton.vue' +export { default as UiSelectSeparator } from './UiSelectSeparator.vue' +export { default as UiSelectTrigger } from './UiSelectTrigger.vue' +export { default as UiSelectValue } from './UiSelectValue.vue' diff --git a/apps/client/src/shared/ui/shimmer-button/ShimmerButton.vue b/apps/client/src/shared/ui/shimmer-button/ShimmerButton.vue new file mode 100644 index 00000000..a65d03ca --- /dev/null +++ b/apps/client/src/shared/ui/shimmer-button/ShimmerButton.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/apps/client/src/shared/ui/shimmer-button/index.ts b/apps/client/src/shared/ui/shimmer-button/index.ts new file mode 100644 index 00000000..b0d44c77 --- /dev/null +++ b/apps/client/src/shared/ui/shimmer-button/index.ts @@ -0,0 +1 @@ +export { default as ShimmerButton } from './ShimmerButton.vue' diff --git a/apps/client/src/shared/ui/shortcuts.ts b/apps/client/src/shared/ui/shortcuts.ts new file mode 100644 index 00000000..b7c5a7c4 --- /dev/null +++ b/apps/client/src/shared/ui/shortcuts.ts @@ -0,0 +1 @@ +export * from './_shortcuts' diff --git a/apps/client/src/shared/ui/table/UiTableBody.vue b/apps/client/src/shared/ui/table/UiTableBody.vue new file mode 100644 index 00000000..90b26390 --- /dev/null +++ b/apps/client/src/shared/ui/table/UiTableBody.vue @@ -0,0 +1,18 @@ + + + diff --git a/apps/client/src/shared/ui/table/UiTableCaption.vue b/apps/client/src/shared/ui/table/UiTableCaption.vue new file mode 100644 index 00000000..7cbd8333 --- /dev/null +++ b/apps/client/src/shared/ui/table/UiTableCaption.vue @@ -0,0 +1,19 @@ + + + diff --git a/apps/client/src/shared/ui/table/UiTableCell.vue b/apps/client/src/shared/ui/table/UiTableCell.vue new file mode 100644 index 00000000..25b5d129 --- /dev/null +++ b/apps/client/src/shared/ui/table/UiTableCell.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/apps/client/src/shared/ui/table/UiTableEmpty.vue b/apps/client/src/shared/ui/table/UiTableEmpty.vue new file mode 100644 index 00000000..ea357620 --- /dev/null +++ b/apps/client/src/shared/ui/table/UiTableEmpty.vue @@ -0,0 +1,37 @@ + + + diff --git a/apps/client/src/shared/ui/table/UiTableHead.vue b/apps/client/src/shared/ui/table/UiTableHead.vue new file mode 100644 index 00000000..20359f6e --- /dev/null +++ b/apps/client/src/shared/ui/table/UiTableHead.vue @@ -0,0 +1,20 @@ + + + diff --git a/apps/client/src/shared/ui/table/UiTableHeader.vue b/apps/client/src/shared/ui/table/UiTableHeader.vue new file mode 100644 index 00000000..80e16a0c --- /dev/null +++ b/apps/client/src/shared/ui/table/UiTableHeader.vue @@ -0,0 +1,20 @@ + + + diff --git a/apps/client/src/shared/ui/table/UiTableRoot.vue b/apps/client/src/shared/ui/table/UiTableRoot.vue new file mode 100644 index 00000000..b7c0a875 --- /dev/null +++ b/apps/client/src/shared/ui/table/UiTableRoot.vue @@ -0,0 +1,26 @@ + + + diff --git a/apps/client/src/shared/ui/table/UiTableRow.vue b/apps/client/src/shared/ui/table/UiTableRow.vue new file mode 100644 index 00000000..5289c538 --- /dev/null +++ b/apps/client/src/shared/ui/table/UiTableRow.vue @@ -0,0 +1,20 @@ + + + diff --git a/apps/client/src/shared/ui/table/index.ts b/apps/client/src/shared/ui/table/index.ts new file mode 100644 index 00000000..ba90d281 --- /dev/null +++ b/apps/client/src/shared/ui/table/index.ts @@ -0,0 +1,9 @@ +// GLOBAL: we used all in DataTable from @/shared/ui/data-table +export { default as UiTableBody } from './UiTableBody.vue' +export { default as UiTableCaption } from './UiTableCaption.vue' +export { default as UiTableCell } from './UiTableCell.vue' +export { default as UiTableEmpty } from './UiTableEmpty.vue' +export { default as UiTableHead } from './UiTableHead.vue' +export { default as UiTableHeader } from './UiTableHeader.vue' +export { default as UiTableRoot } from './UiTableRoot.vue' +export { default as UiTableRow } from './UiTableRow.vue' diff --git a/apps/client/src/shared/ui/tabs/UiTabs.stories.ts b/apps/client/src/shared/ui/tabs/UiTabs.stories.ts new file mode 100644 index 00000000..6c9827a7 --- /dev/null +++ b/apps/client/src/shared/ui/tabs/UiTabs.stories.ts @@ -0,0 +1,44 @@ +import { + UiTabs, + UiTabsContent, + UiTabsList, + UiTabsTrigger, +} from './index' +import type { Meta, StoryFn } from '@storybook/vue3' + +export default { + title: 'UiTabs', + component: UiTabs, +} as Meta + +const Template: StoryFn = args => ({ + components: { + UiTabs, + UiTabsContent, + UiTabsList, + UiTabsTrigger, + }, + setup() { + return { args } + }, + template: ` + + + + Account + + + Password + + + + Make changes to your account here. + + + Change your password here. + + + `, +}) + +export const DefaultTabs: StoryFn = Template.bind({}) diff --git a/apps/client/src/shared/ui/tabs/UiTabs.vue b/apps/client/src/shared/ui/tabs/UiTabs.vue new file mode 100644 index 00000000..2fa0971f --- /dev/null +++ b/apps/client/src/shared/ui/tabs/UiTabs.vue @@ -0,0 +1,15 @@ + + + diff --git a/apps/client/src/shared/ui/tabs/UiTabsContent.vue b/apps/client/src/shared/ui/tabs/UiTabsContent.vue new file mode 100644 index 00000000..ebe0fab3 --- /dev/null +++ b/apps/client/src/shared/ui/tabs/UiTabsContent.vue @@ -0,0 +1,25 @@ + + + diff --git a/apps/client/src/shared/ui/tabs/UiTabsList.vue b/apps/client/src/shared/ui/tabs/UiTabsList.vue new file mode 100644 index 00000000..f527410d --- /dev/null +++ b/apps/client/src/shared/ui/tabs/UiTabsList.vue @@ -0,0 +1,25 @@ + + + diff --git a/apps/client/src/shared/ui/tabs/UiTabsTrigger.vue b/apps/client/src/shared/ui/tabs/UiTabsTrigger.vue new file mode 100644 index 00000000..20016690 --- /dev/null +++ b/apps/client/src/shared/ui/tabs/UiTabsTrigger.vue @@ -0,0 +1,29 @@ + + + diff --git a/apps/client/src/shared/ui/tabs/__tests__/MockComponent.vue b/apps/client/src/shared/ui/tabs/__tests__/MockComponent.vue new file mode 100644 index 00000000..380f2c97 --- /dev/null +++ b/apps/client/src/shared/ui/tabs/__tests__/MockComponent.vue @@ -0,0 +1,31 @@ + + + diff --git a/apps/client/src/shared/ui/tabs/__tests__/UiTabs.spec.ts b/apps/client/src/shared/ui/tabs/__tests__/UiTabs.spec.ts new file mode 100644 index 00000000..0d64d9e4 --- /dev/null +++ b/apps/client/src/shared/ui/tabs/__tests__/UiTabs.spec.ts @@ -0,0 +1,37 @@ +import { mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it } from 'vitest' +import MockComponent from './MockComponent.vue' + +describe('tests for UiTabs', () => { + const wrapper = mount(MockComponent, { + props: { + defaultValue: 'test1', + }, + }) + + it('should render correctly', () => { + expect(wrapper.html()) + }) + + it('should render default content', () => { + expect(wrapper.find('[role=tabpanel]').exists()).toBeTruthy() + expect(wrapper.html()).toContain('test 1 content') + }) + + describe('should move focus to next tab', () => { + beforeEach(() => { + const trigger = wrapper.find('#radix-vue-tabs-v-0-trigger-test2') + trigger.trigger('keydown', { key: 'ArrowRight' }) + }) + + it('should focus on next tab', () => { + expect(wrapper.find('[role=tabpanel]').exists()).toBeTruthy() + expect(wrapper.html()).toContain('Test 2') + }) + }) + + it('should apply props correctly', async () => { + expect(wrapper.props().defaultValue).toBe('test1') + expect(wrapper.props().class).toBeUndefined() + }) +}) diff --git a/apps/client/src/shared/ui/tabs/index.ts b/apps/client/src/shared/ui/tabs/index.ts new file mode 100644 index 00000000..fa70d45c --- /dev/null +++ b/apps/client/src/shared/ui/tabs/index.ts @@ -0,0 +1,4 @@ +export { default as UiTabs } from './UiTabs.vue' +export { default as UiTabsContent } from './UiTabsContent.vue' +export { default as UiTabsList } from './UiTabsList.vue' +export { default as UiTabsTrigger } from './UiTabsTrigger.vue' diff --git a/apps/client/src/shared/ui/tags-input/UiTagsInput.stories.ts b/apps/client/src/shared/ui/tags-input/UiTagsInput.stories.ts new file mode 100644 index 00000000..0d456a30 --- /dev/null +++ b/apps/client/src/shared/ui/tags-input/UiTagsInput.stories.ts @@ -0,0 +1,61 @@ +import { + UiTagsInput, + UiTagsInputInput, + UiTagsInputItem, + UiTagsInputItemDelete, + UiTagsInputItemText, +} from '..' +import type { Meta, StoryFn } from '@storybook/vue3' + +export default { + title: 'UiTagsInput', + component: UiTagsInput, + argTypes: { + modelValue: { + control: 'object', + }, + disabled: { + control: { type: 'boolean' }, + }, + }, +} as Meta + +const Template: StoryFn = (args) => { + return { + components: { + UiTagsInput, + UiTagsInputInput, + UiTagsInputItem, + UiTagsInputItemDelete, + UiTagsInputItemText, + }, + setup() { + return { args } + }, + template: ` + + + + + + + + + `, + } +} + +export const DefaultTagsInput: StoryFn = Template.bind({}) + +DefaultTagsInput.args = { + modelValue: ['test', 'test2'], +} + +export const DisabledTagsInput: StoryFn = Template.bind({}) + +DisabledTagsInput.args = { + disabled: true, + modelValue: ['test', 'test2'], +} diff --git a/apps/client/src/shared/ui/tags-input/UiTagsInput.vue b/apps/client/src/shared/ui/tags-input/UiTagsInput.vue new file mode 100644 index 00000000..bff04c79 --- /dev/null +++ b/apps/client/src/shared/ui/tags-input/UiTagsInput.vue @@ -0,0 +1,28 @@ + + + diff --git a/apps/client/src/shared/ui/tags-input/UiTagsInputInput.vue b/apps/client/src/shared/ui/tags-input/UiTagsInputInput.vue new file mode 100644 index 00000000..00b2ec54 --- /dev/null +++ b/apps/client/src/shared/ui/tags-input/UiTagsInputInput.vue @@ -0,0 +1,25 @@ + + + diff --git a/apps/client/src/shared/ui/tags-input/UiTagsInputItem.vue b/apps/client/src/shared/ui/tags-input/UiTagsInputItem.vue new file mode 100644 index 00000000..b5349521 --- /dev/null +++ b/apps/client/src/shared/ui/tags-input/UiTagsInputItem.vue @@ -0,0 +1,27 @@ + + + diff --git a/apps/client/src/shared/ui/tags-input/UiTagsInputItemDelete.vue b/apps/client/src/shared/ui/tags-input/UiTagsInputItemDelete.vue new file mode 100644 index 00000000..cda10ddc --- /dev/null +++ b/apps/client/src/shared/ui/tags-input/UiTagsInputItemDelete.vue @@ -0,0 +1,32 @@ + + + diff --git a/apps/client/src/shared/ui/tags-input/UiTagsInputItemText.vue b/apps/client/src/shared/ui/tags-input/UiTagsInputItemText.vue new file mode 100644 index 00000000..3260cbbf --- /dev/null +++ b/apps/client/src/shared/ui/tags-input/UiTagsInputItemText.vue @@ -0,0 +1,25 @@ + + + diff --git a/apps/client/src/shared/ui/tags-input/__tests__/MockComponent.vue b/apps/client/src/shared/ui/tags-input/__tests__/MockComponent.vue new file mode 100644 index 00000000..768b35e3 --- /dev/null +++ b/apps/client/src/shared/ui/tags-input/__tests__/MockComponent.vue @@ -0,0 +1,27 @@ + + + diff --git a/apps/client/src/shared/ui/tags-input/__tests__/UiTagsInput.spec.ts b/apps/client/src/shared/ui/tags-input/__tests__/UiTagsInput.spec.ts new file mode 100644 index 00000000..09486e8e --- /dev/null +++ b/apps/client/src/shared/ui/tags-input/__tests__/UiTagsInput.spec.ts @@ -0,0 +1,44 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import MockComponent from './MockComponent.vue' +import type { DOMWrapper } from '@vue/test-utils' + +describe('tests for UiTagsInput', () => { + const wrapper = mount(MockComponent, { + props: { + modelValue: ['value1', 'value2'], + }, + }) + + let input: DOMWrapper + let tags: DOMWrapper[] + + const addTag = async (text: string) => { + await input.setValue(text) + await input.trigger('keydown.enter') + tags = wrapper.findAll('[data-radix-vue-collection-item]') + } + + it('should render correctly', () => { + expect(wrapper.html()) + }) + + it('should emit the correct event on input', async () => { + wrapper.vm.$emit('update:modelValue', ['1', '2', '3']) + expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([['1', '2', '3']]) + }) + + describe('add tag', () => { + beforeEach(async () => { + input = wrapper.find('input') + input.element.focus() + await addTag('test1') + await addTag('test2') + }) + + it('should add a new tag', () => { + expect(wrapper.html()).contains('test1') + expect(wrapper.html()).contains('test2') + }) + }) +}) diff --git a/apps/client/src/shared/ui/tags-input/index.ts b/apps/client/src/shared/ui/tags-input/index.ts new file mode 100644 index 00000000..623d5449 --- /dev/null +++ b/apps/client/src/shared/ui/tags-input/index.ts @@ -0,0 +1,5 @@ +export { default as UiTagsInput } from './UiTagsInput.vue' +export { default as UiTagsInputInput } from './UiTagsInputInput.vue' +export { default as UiTagsInputItem } from './UiTagsInputItem.vue' +export { default as UiTagsInputItemDelete } from './UiTagsInputItemDelete.vue' +export { default as UiTagsInputItemText } from './UiTagsInputItemText.vue' diff --git a/apps/client/src/store/index.ts b/apps/client/src/store/index.ts new file mode 100644 index 00000000..c569ef7f --- /dev/null +++ b/apps/client/src/store/index.ts @@ -0,0 +1,3 @@ +import { createPinia } from 'pinia' + +export const pinia = createPinia() diff --git a/apps/client/src/styles/_base.css b/apps/client/src/styles/_base.css new file mode 100644 index 00000000..898bad19 --- /dev/null +++ b/apps/client/src/styles/_base.css @@ -0,0 +1,55 @@ +*, +*::before, +*::after { + padding: 0; + margin: 0; + box-sizing: border-box; +} + +html { + height: 100%; + min-height: 100%; + text-size-adjust: 100%; +} + +.dark { + background-color: var(--custom-jenda-dark); + color-scheme: dark; +} + +body { + height: 100%; + width: 100%; + font-family: Geist, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overscroll-behavior: none; +} + +.ui-container { + width: 100%; + max-width: 1600px; + margin-right: auto; + margin-left: auto; + padding-right: 20px; + padding-left: 20px; +} + +::view-transition-old(root), +::view-transition-new(root) { + animation: none; + mix-blend-mode: normal; +} + +.dropdown-enter-active, +.dropdown-leave-active { + transition: + opacity 0.15s ease, + transform 0.15s ease; +} + +.dropdown-enter-from, +.dropdown-leave-to { + opacity: 0; + transform: translateY(-10px); +} diff --git a/apps/client/src/styles/_exceptions.css b/apps/client/src/styles/_exceptions.css new file mode 100644 index 00000000..e2b43f9a --- /dev/null +++ b/apps/client/src/styles/_exceptions.css @@ -0,0 +1,60 @@ +.v-popper__inner { + font-size: 12px; + padding: 4px 10px !important; +} + +.v-popper__arrow-container { + display: none !important; +} + +canvas { + width: 100% !important; + height: 100% !important; +} + +/* tr, +thead { + clip-path: xywh(0 0 100% 100% round 0.5em); +} */ + +.splitpanes--vertical .splitpanes__pane { + transition: none; +} + +.splitpanes--vertical > .splitpanes__splitter { + display: none; +} + +[data-sonner-toaster][data-theme='dark'] { + --normal-bg: var(--custom-jenda-dark) !important; + --normal-border: hsl(0deg 0% 20%); + --normal-text: var(--gray1); + --success-border: hsl(147deg 100% 12%); + --info-bg: var(--custom-jenda-dark); +} + +.vue-ui-kpi { + background: transparent !important; +} + +.vue-ui-kpi-value { + @apply text-blue-500; +} + +.vue-data-ui-fulscreen--off { + circle { + fill: #fff !important; + } +} + +html.dark { + .v-popper__inner { + @apply bg-neutral-100 text-neutral-800; + } + + .vue-data-ui-fulscreen--off { + circle { + fill: #313131 !important; + } + } +} diff --git a/apps/client/src/styles/_families.css b/apps/client/src/styles/_families.css new file mode 100644 index 00000000..18b211ac --- /dev/null +++ b/apps/client/src/styles/_families.css @@ -0,0 +1,3 @@ +* { + font-family: Geist, sans-serif; +} diff --git a/apps/client/src/styles/_fonts.css b/apps/client/src/styles/_fonts.css new file mode 100644 index 00000000..0defed18 --- /dev/null +++ b/apps/client/src/styles/_fonts.css @@ -0,0 +1,6 @@ +@font-face { + font-family: Geist; + font-style: normal; + font-display: swap; + src: url('../shared/assets/fonts/geist/GeistVariableVF.woff2') format('woff2'); +} diff --git a/apps/client/src/styles/_properties.css b/apps/client/src/styles/_properties.css new file mode 100644 index 00000000..b93a57c5 --- /dev/null +++ b/apps/client/src/styles/_properties.css @@ -0,0 +1,8 @@ +:root { + /* priorities */ + --low-color: 34, 197, 94; + --medium-color: 245, 158, 11; + --high-color: 239, 68, 68; + + --custom-jenda-dark: rgb(38 38 38); +} diff --git a/apps/client/src/styles/_scrollbar.css b/apps/client/src/styles/_scrollbar.css new file mode 100644 index 00000000..8ff69384 --- /dev/null +++ b/apps/client/src/styles/_scrollbar.css @@ -0,0 +1,13 @@ +::-webkit-scrollbar { + background-color: transparent; + height: 0.6em; + width: 0.6em; +} + +::-webkit-scrollbar-track { + @apply bg-neutral-100 dark:bg-neutral-700/20; +} + +::-webkit-scrollbar-thumb { + @apply bg-neutral-300 rounded-xl dark:bg-neutral-700; +} diff --git a/apps/client/src/styles/index.css b/apps/client/src/styles/index.css new file mode 100644 index 00000000..d3c98fc9 --- /dev/null +++ b/apps/client/src/styles/index.css @@ -0,0 +1,6 @@ +@import './_properties'; +@import './_fonts'; +@import './_base'; +@import './_exceptions'; +@import './_families'; +@import './_scrollbar'; diff --git a/apps/client/tsconfig.app.json b/apps/client/tsconfig.app.json new file mode 100644 index 00000000..35da1403 --- /dev/null +++ b/apps/client/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "baseUrl": ".", + "moduleResolution": "Bundler", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "./typed-router.d.ts"], + "exclude": ["src/**/__tests__/*"] +} diff --git a/apps/client/tsconfig.json b/apps/client/tsconfig.json new file mode 100644 index 00000000..e44f9a16 --- /dev/null +++ b/apps/client/tsconfig.json @@ -0,0 +1,14 @@ +{ + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.vitest.json" + } + ], + "files": [] +} diff --git a/apps/client/tsconfig.node.json b/apps/client/tsconfig.node.json new file mode 100644 index 00000000..0fa71eed --- /dev/null +++ b/apps/client/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"], + "noEmit": true + }, + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "nightwatch.conf.*", + "playwright.config.*" + ] +} diff --git a/apps/client/tsconfig.vitest.json b/apps/client/tsconfig.vitest.json new file mode 100644 index 00000000..79721714 --- /dev/null +++ b/apps/client/tsconfig.vitest.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo", + + "lib": [], + "types": ["node", "jsdom"] + }, + "exclude": [] +} diff --git a/apps/client/typed-router.d.ts b/apps/client/typed-router.d.ts new file mode 100644 index 00000000..e064a3ae --- /dev/null +++ b/apps/client/typed-router.d.ts @@ -0,0 +1,38 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️ +// It's recommended to commit this file. +// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry. + +declare module 'vue-router/auto-routes' { + import type { + RouteRecordInfo, + ParamValue, + ParamValueOneOrMore, + ParamValueZeroOrMore, + ParamValueZeroOrOne, + } from 'vue-router' + + /** + * Route name map generated by unplugin-vue-router + */ + export interface RouteNamedMap { + 'welcome': RouteRecordInfo<'welcome', '/', Record, Record>, + 'not-found': RouteRecordInfo<'not-found', '/:path(.*)', { path: ParamValue }, { path: ParamValue }>, + 'analytics': RouteRecordInfo<'analytics', '/analytics', Record, Record>, + 'sign-in': RouteRecordInfo<'sign-in', '/auth/sign-in', Record, Record>, + 'sign-in-workspace': RouteRecordInfo<'sign-in-workspace', '/auth/sign-in/workspace', Record, Record>, + 'sign-up': RouteRecordInfo<'sign-up', '/auth/sign-up', Record, Record>, + 'confirm': RouteRecordInfo<'confirm', '/auth/sign-up/confirm', Record, Record>, + 'sign-up-workspace': RouteRecordInfo<'sign-up-workspace', '/auth/sign-up/workspace', Record, Record>, + 'boards': RouteRecordInfo<'boards', '/boards', Record, Record>, + 'board-id': RouteRecordInfo<'board-id', '/boards/:id', { id: ParamValue }, { id: ParamValue }>, + 'boards-new': RouteRecordInfo<'boards-new', '/boards/new', Record, Record>, + 'members': RouteRecordInfo<'members', '/members', Record, Record>, + 'notes': RouteRecordInfo<'notes', '/notes', Record, Record>, + 'settings': RouteRecordInfo<'settings', '/settings', Record, Record>, + 'templates': RouteRecordInfo<'templates', '/templates', Record, Record>, + 'community': RouteRecordInfo<'community', '/templates/community', Record, Record>, + } +} diff --git a/apps/client/uno.config.ts b/apps/client/uno.config.ts new file mode 100644 index 00000000..54b88819 --- /dev/null +++ b/apps/client/uno.config.ts @@ -0,0 +1,88 @@ +import { + defineConfig, + presetAttributify, + presetIcons, + presetTypography, + presetUno, + transformerDirectives, + transformerVariantGroup, +} from 'unocss' +import presetAnimations from 'unocss-preset-animations' +import { presetShadcn } from 'unocss-preset-shadcn' +import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders' +import presetJendaUI from './src/shared/libs/unocss/presets/presetUiKit' + +export default defineConfig({ + presets: [ + presetUno(), + presetAttributify({ + ignoreAttributes: [ + 'size', + 'variant', + ], + }), + presetAnimations(), + presetShadcn({ color: 'neutral' }, false), + presetIcons({ + extraProperties: { + 'display': 'inline-block', + 'vertical-align': 'middle', + }, + collections: { + lucid: () => import('@iconify-json/lucide/icons.json').then(i => i.default), + huge: () => import('@iconify-json/hugeicons/icons.json').then(i => i.default), + jenda: FileSystemIconLoader('./src/shared/assets/icons/custom-jenda'), + }, + }), + presetJendaUI(), + presetTypography(), + ], + shortcuts: [ + { + 'auth-page': 'h-full flex w-68% max-[1440px]:w-80% max-[1100px]:!w-full', + 'auth-slot-container': 'relative h-full w-full mx-auto px-2rem dark:bg-#1c1c1c', + 'form-container': 'relative flex w-460px flex-col gap-2 max-[520px]:!w-full max-[1100px]:!w-460px max-[1200px]:w-360px', + }, + { + 'bg-main': 'bg-white dark:bg-#262626', + 'border-layout': 'border-neutral-200 dark:border-#1c1c1c', + 'bg-sidebar': 'bg-neutral-50 dark:bg-#1c1c1c66', + }, + ], + configDeps: [ + './src/shared/lib/unocss/presets/presetUiKit.ts', + + './src/shared/ui/_shortcuts/button.ts', + './src/shared/ui/_shortcuts/badge.ts', + './src/shared/ui/_shortcuts/input.ts', + './src/shared/ui/_shortcuts/alert.ts', + './src/shared/ui/_shortcuts/dropdown-menu.ts', + './src/shared/ui/_shortcuts/select.ts', + './src/shared/ui/_shortcuts/dialog.ts', + './src/shared/ui/_shortcuts/pin-input.ts', + './src/shared/ui/_shortcuts/form.ts', + './src/shared/ui/_shortcuts/popover.ts', + './src/shared/ui/_shortcuts/picker.ts', + './src/shared/ui/_shortcuts/command.ts', + './src/shared/ui/_shortcuts/tabs.ts', + './src/shared/ui/_shortcuts/pagination.ts', + './src/shared/ui/_shortcuts/checkbox.ts', + './src/shared/ui/_shortcuts/table.ts', + './src/shared/ui/_shortcuts/tags-input.ts', + './src/shared/ui/_shortcuts/index.ts', + ], + content: { + pipeline: { + include: [ + /\.(vue|mdx?|html)($|\?)/, + '(components|src)/**/*.{js,ts}', + 'src/**/*.stories.{js,ts}', + 'stories/**/*', + ], + }, + }, + transformers: [ + transformerDirectives(), + transformerVariantGroup(), + ], +}) diff --git a/apps/client/vercel.json b/apps/client/vercel.json new file mode 100644 index 00000000..3a48e56b --- /dev/null +++ b/apps/client/vercel.json @@ -0,0 +1,3 @@ +{ + "rewrites": [{ "source": "/(.*)", "destination": "/" }] +} diff --git a/apps/client/vite.config.ts b/apps/client/vite.config.ts new file mode 100644 index 00000000..e82a921b --- /dev/null +++ b/apps/client/vite.config.ts @@ -0,0 +1,61 @@ +import { fileURLToPath, URL } from 'node:url' +import vue from '@vitejs/plugin-vue' +import UnoCSS from 'unocss/vite' +import { getNuxtStyleRouteName } from 'unplugin-vue-router-extend' +import UnpluginVueRouterExtend from 'unplugin-vue-router-extend/vite' +import UnpluginVueRouter from 'unplugin-vue-router/vite' +import { defineConfig } from 'vite' +import vueDevTools from 'vite-plugin-vue-devtools' +// import Sonda from 'sonda/vite' +import type { TreeNode } from 'unplugin-vue-router' + +const routeMap = new Map() +export default defineConfig({ + plugins: [ + UnpluginVueRouter({ + getRouteName: (node: TreeNode) => { + if (!routeMap.size) { + for (const [key, value] of (node.parent as any)?.map) + routeMap.set(key, value) + } + return getNuxtStyleRouteName(node) + }, + routesFolder: [ + './src/modules/auth/pages', + './src/modules/members/pages', + './src/modules/boards/pages', + './src/modules/workspace/pages', + './src/modules/notes/pages', + './src/modules/settings/pages', + './src/modules/templates/pages', + './src/modules/charts/pages', + './src/modules/welcome/pages', + './src/core/pages', + ], + dts: './typed-router.d.ts', + }), + UnpluginVueRouterExtend({ + routeMap, + }), + vueDevTools(), + UnoCSS(), + // Sonda(), + vue(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + build: { + // only with sonda active + // sourcemap: true, + }, + css: { + preprocessorOptions: { + scss: { + api: 'modern-compiler', + }, + }, + }, +}) diff --git a/apps/client/vitest.config.ts b/apps/client/vitest.config.ts new file mode 100644 index 00000000..d18ade7e --- /dev/null +++ b/apps/client/vitest.config.ts @@ -0,0 +1,20 @@ +import { fileURLToPath } from 'node:url' +import { configDefaults, defineConfig, mergeConfig } from 'vitest/config' +import viteConfig from './vite.config' + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + environment: 'jsdom', + exclude: [...configDefaults.exclude, 'e2e/**'], + setupFiles: ['./src/shared/libs/vitest-utils/cookiesI18n-mock.ts'], + root: fileURLToPath(new URL('./', import.meta.url)), + css: { + modules: { + classNameStrategy: 'non-scoped', + }, + }, + }, + }), +) diff --git a/apps/server/.gitkeep b/apps/server/.gitkeep new file mode 100644 index 00000000..e69de29b