diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 8f04671..9640921 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -6,7 +6,7 @@ module.exports = { es2021: true, }, plugins: ['prettier', 'unicorn', 'import', 'waterfall'], - ignorePatterns: ['node_modules/', 'dist/', 'build/', '*.min.js'], + ignorePatterns: ['node_modules/', 'dist/', 'build/', '*.min.js', 'src/components/icons/icon-names.d.ts'], parserOptions: { ecmaVersion: 2021, sourceType: 'module', diff --git a/jsconfig.json b/jsconfig.json index 686295e..85cbc8d 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,9 +1,11 @@ { "compilerOptions": { "baseUrl": ".", + "checkJs": false, "paths": { "@/*": ["src/*"] } }, + "include": ["src/**/*", "src/**/*.d.ts"], "exclude": ["node_modules", "dist"] } diff --git a/package-lock.json b/package-lock.json index 08f0882..fd14004 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,11 @@ "@floating-ui/dom": "^1.7.6", "@tanstack/vue-query": "^5.62.2", "@tanstack/vue-query-devtools": "^5.62.2", + "@tinymce/tinymce-vue": "^4.0.7", "axios": "^1.12.2", "lodash": "^4.18.1", "pinia": "^3.0.3", + "tinymce": "^8.5.0", "vue": "^3.5.21", "vue-advanced-cropper": "^2.8.9", "vue-router": "^4.5.1", @@ -1671,6 +1673,24 @@ } } }, + "node_modules/@tinymce/tinymce-vue": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@tinymce/tinymce-vue/-/tinymce-vue-4.0.7.tgz", + "integrity": "sha512-1esB8wGWrjPCY+rK8vy3QB1cxwXo7HLJWuNrcyPl6LOVR+QJjub0OiV/C+TUEsLN6OpCtRv+QnIqMC5vXz783Q==", + "license": "Apache-2.0", + "dependencies": { + "tinymce": "^5.5.0" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@tinymce/tinymce-vue/node_modules/tinymce": { + "version": "5.10.9", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-5.10.9.tgz", + "integrity": "sha512-5bkrors87X9LhYX2xq8GgPHrIgJYHl87YNs+kBcjQ5I3CiUgzo/vFcGvT3MZQ9QHsEeYMhYO6a5CLGGffR8hMg==", + "license": "LGPL-2.1" + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -7201,6 +7221,12 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinymce": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-8.5.0.tgz", + "integrity": "sha512-DnKEfPNQnOJc8Ca1roZBs/GSbkAZyIIbC4p8eHZyZQi85OSAXtiVNYMaRxo4mzsGKpa0sA4/Us4KXQkX8q7w2A==", + "license": "SEE LICENSE IN license.md" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index 0297ccd..bc83a12 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,11 @@ "@floating-ui/dom": "^1.7.6", "@tanstack/vue-query": "^5.62.2", "@tanstack/vue-query-devtools": "^5.62.2", + "@tinymce/tinymce-vue": "^4.0.7", "axios": "^1.12.2", "lodash": "^4.18.1", "pinia": "^3.0.3", + "tinymce": "^8.5.0", "vue": "^3.5.21", "vue-advanced-cropper": "^2.8.9", "vue-router": "^4.5.1", diff --git a/scripts/vite-icon-names.js b/scripts/vite-icon-names.js new file mode 100644 index 0000000..1ec9211 --- /dev/null +++ b/scripts/vite-icon-names.js @@ -0,0 +1,52 @@ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const ICONS_DIR = path.resolve(__dirname, '../src/assets/icons') +const OUTPUT_FILE = path.resolve(__dirname, '../src/components/icons/icon-names.d.ts') + +const readNames = () => + fs + .readdirSync(ICONS_DIR) + .filter((f) => f.endsWith('.svg')) + .map((f) => f.replace(/\.svg$/, '')) + .sort() + +const buildContent = (names) => { + const union = names.length > 0 ? names.map((n) => ` | '${n}'`).join('\n') : ' | never' + return `// AUTO-GENERATED — do not edit by hand. +// Regenerated whenever src/assets/icons/*.svg changes (via scripts/vite-icon-names.js). + +export type IconName = +${union} + +export const iconNames: readonly IconName[] +` +} + +const writeIfChanged = () => { + const content = buildContent(readNames()) + const prev = fs.existsSync(OUTPUT_FILE) ? fs.readFileSync(OUTPUT_FILE, 'utf8') : '' + if (prev !== content) fs.writeFileSync(OUTPUT_FILE, content) +} + +export default function viteIconNames() { + return { + name: 'vite-icon-names', + buildStart() { + writeIfChanged() + }, + configureServer(server) { + writeIfChanged() + server.watcher.add(path.join(ICONS_DIR, '*.svg')) + const handler = (file) => { + if (file.startsWith(ICONS_DIR) && file.endsWith('.svg')) writeIfChanged() + } + server.watcher.on('add', handler) + server.watcher.on('unlink', handler) + }, + } +} + +export { writeIfChanged as generateIconNames } diff --git a/src/assets/icons/chat-centered-dots.svg b/src/assets/icons/chat-centered-dots.svg new file mode 100644 index 0000000..36aeefa --- /dev/null +++ b/src/assets/icons/chat-centered-dots.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/icons/heart.svg b/src/assets/icons/heart.svg new file mode 100644 index 0000000..9857326 --- /dev/null +++ b/src/assets/icons/heart.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/link.svg b/src/assets/icons/link.svg new file mode 100644 index 0000000..1eba376 --- /dev/null +++ b/src/assets/icons/link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/map-pin-simple-area.svg b/src/assets/icons/map-pin-simple-area.svg new file mode 100644 index 0000000..d118d44 --- /dev/null +++ b/src/assets/icons/map-pin-simple-area.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/icons/microphone.svg b/src/assets/icons/microphone.svg new file mode 100644 index 0000000..cc7f90d --- /dev/null +++ b/src/assets/icons/microphone.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/icons/mood.svg b/src/assets/icons/mood.svg index e146bf6..4c152d5 100644 --- a/src/assets/icons/mood.svg +++ b/src/assets/icons/mood.svg @@ -1,3 +1,3 @@ - + diff --git a/src/assets/icons/paper-plane-right.svg b/src/assets/icons/paper-plane-right.svg index 64b1606..8200eed 100644 --- a/src/assets/icons/paper-plane-right.svg +++ b/src/assets/icons/paper-plane-right.svg @@ -1,4 +1,4 @@ - + diff --git a/src/assets/icons/scroll.svg b/src/assets/icons/scroll.svg new file mode 100644 index 0000000..fc05661 --- /dev/null +++ b/src/assets/icons/scroll.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/components/BaseButton.vue b/src/components/BaseButton.vue index f203790..8d0b7b4 100644 --- a/src/components/BaseButton.vue +++ b/src/components/BaseButton.vue @@ -79,6 +79,7 @@ const handleClick = (event) => { font-family: var(--font-family-fa); text-decoration: none; padding: 0 1rem; + white-space: nowrap; &__text { line-height: 1.25rem; diff --git a/src/components/blocks/TabsBlock.vue b/src/components/blocks/TabsBlock.vue index 70d8cfa..281d142 100644 --- a/src/components/blocks/TabsBlock.vue +++ b/src/components/blocks/TabsBlock.vue @@ -1,23 +1,57 @@
- +
+ +
- - {{ tab.label }} - + + + +
+ +
+ + + +
+
@@ -26,7 +60,7 @@ diff --git a/src/components/form/CheckboxField.vue b/src/components/form/CheckboxField.vue new file mode 100644 index 0000000..cf3a19e --- /dev/null +++ b/src/components/form/CheckboxField.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/src/components/form/ImageUploader.vue b/src/components/form/ImageUploader.vue new file mode 100644 index 0000000..105ea10 --- /dev/null +++ b/src/components/form/ImageUploader.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/src/components/form/VoiceRecorder.vue b/src/components/form/VoiceRecorder.vue new file mode 100644 index 0000000..d13ab3b --- /dev/null +++ b/src/components/form/VoiceRecorder.vue @@ -0,0 +1,422 @@ + + + + + diff --git a/src/components/icons/SvgIcon.vue b/src/components/icons/SvgIcon.vue index 4bdac10..fc37433 100644 --- a/src/components/icons/SvgIcon.vue +++ b/src/components/icons/SvgIcon.vue @@ -14,10 +14,13 @@ import { computed } from 'vue' import { iconRegistry } from '@/components/icons/registry' +/** @typedef {import('@/components/icons/icon-names').IconName} IconName */ + const props = defineProps({ + /** @type {import('vue').PropType} */ name: { type: String, required: true }, size: { type: [String, Number], default: '1.25rem' }, - color: { type: String, default: 'currentColor' }, + color: { type: String, default: 'transparent' }, }) const component = computed(() => { @@ -25,6 +28,7 @@ const component = computed(() => { if (!c && import.meta.env.DEV) { console.warn(`[SvgIcon] unknown icon "${props.name}"`) } + return c }) diff --git a/src/components/icons/icon-names.d.ts b/src/components/icons/icon-names.d.ts new file mode 100644 index 0000000..c261638 --- /dev/null +++ b/src/components/icons/icon-names.d.ts @@ -0,0 +1,48 @@ +// AUTO-GENERATED — do not edit by hand. +// Regenerated whenever src/assets/icons/*.svg changes (via scripts/vite-icon-names.js). + +export type IconName = + | 'arrow-left' + | 'arrows-clockwise' + | 'attach-file' + | 'bell' + | 'book' + | 'calendar' + | 'caret-down' + | 'caret-left' + | 'caret-right' + | 'chat' + | 'chat-centered-dots' + | 'check' + | 'check-square' + | 'close' + | 'copy' + | 'eye' + | 'eye-slash' + | 'file' + | 'funnel' + | 'heart' + | 'instagram' + | 'link' + | 'list-bullets' + | 'map-pin-simple-area' + | 'menu' + | 'microphone' + | 'mood' + | 'paper-plane' + | 'paper-plane-right' + | 'pencil' + | 'phone' + | 'plus' + | 'scroll' + | 'spinner' + | 'square' + | 'telegram' + | 'trash' + | 'upload' + | 'user' + | 'users' + | 'users-three' + | 'warning' + +export const iconNames: readonly IconName[] diff --git a/src/features/admin/courses/pages/CoursesListPage.vue b/src/features/admin/courses/pages/CoursesListPage.vue index 752817a..f38afe4 100644 --- a/src/features/admin/courses/pages/CoursesListPage.vue +++ b/src/features/admin/courses/pages/CoursesListPage.vue @@ -17,26 +17,6 @@ @reset="onFilterReset" /> -
- - - - - - -
-