@cavulsqa/create 2.9.3 → 2.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/bin/create.mjs +18 -25
- package/lib/templates.mjs +56 -0
- package/package.json +13 -5
- package/templates/f7-app/auto-imports.d.ts +9 -0
- package/templates/f7-app/package.json +6 -3
- package/templates/f7-app/src/env.d.ts +1 -0
- package/templates/f7-app/src/main.ts +3 -0
- package/templates/f7-app/src/plugins/bootstrapError.ts +2 -0
- package/templates/f7-app/src/plugins/recorder.plugin.ts +155 -0
- package/templates/f7-app/src/shared/recorder/capacitorSink.ts +124 -0
- package/templates/f7-app/vite.config.ts +1 -0
- package/templates/m3e-app/.claude/rules/data-fetching.md +68 -0
- package/templates/m3e-app/.claude/rules/database.md +106 -0
- package/templates/m3e-app/.claude/rules/m3e-ui.md +48 -0
- package/templates/m3e-app/.claude/rules/modules.md +43 -0
- package/templates/m3e-app/.claude/rules/native.md +60 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/SKILL.md +68 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/color.md +44 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/components.md +325 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/layout.md +42 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/motion.md +51 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/shapes-type.md +48 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/sources.md +53 -0
- package/templates/m3e-app/.claude/skills/module-architecture/SKILL.md +65 -0
- package/templates/m3e-app/.claude/skills/module-architecture/file-templates.md +178 -0
- package/templates/m3e-app/.claude/skills/reactive-data/SKILL.md +90 -0
- package/templates/m3e-app/.claude/skills/reactive-data/testing.md +55 -0
- package/templates/m3e-app/.env.example +32 -0
- package/templates/m3e-app/CLAUDE.md +93 -0
- package/templates/m3e-app/auto-imports.d.ts +851 -0
- package/templates/m3e-app/capacitor.config.ts +44 -0
- package/templates/m3e-app/components.d.ts +252 -0
- package/templates/m3e-app/index.html +16 -0
- package/templates/m3e-app/package.json +108 -0
- package/templates/m3e-app/src/App.vue +129 -0
- package/templates/m3e-app/src/app/pragmas.config.ts +35 -0
- package/templates/m3e-app/src/app/scroll.config.ts +20 -0
- package/templates/m3e-app/src/app/storage.config.ts +65 -0
- package/templates/m3e-app/src/app/tabs.ts +33 -0
- package/templates/m3e-app/src/app/theme.config.ts +51 -0
- package/templates/m3e-app/src/assets/css/app.css +18 -0
- package/templates/m3e-app/src/assets/css/base.css +53 -0
- package/templates/m3e-app/src/assets/css/layout/container-transform.css +113 -0
- package/templates/m3e-app/src/assets/css/layout/shell.css +69 -0
- package/templates/m3e-app/src/assets/css/layout/transitions.css +121 -0
- package/templates/m3e-app/src/assets/css/m3e.css +6 -0
- package/templates/m3e-app/src/assets/css/theme/framework7.css +18 -0
- package/templates/m3e-app/src/assets/css/theme/tailwind.css +365 -0
- package/templates/m3e-app/src/domains/benchmark/benchmark.dataset.ts +209 -0
- package/templates/m3e-app/src/domains/benchmark/benchmark.suite.ts +662 -0
- package/templates/m3e-app/src/domains/sales/sales.repository.ts +458 -0
- package/templates/m3e-app/src/env.d.ts +40 -0
- package/templates/m3e-app/src/locales/ar.json +1260 -0
- package/templates/m3e-app/src/locales/en.json +1260 -0
- package/templates/m3e-app/src/locales/fr.json +1260 -0
- package/templates/m3e-app/src/main.ts +41 -0
- package/templates/m3e-app/src/modules/demo/components/DemoBenchmark.vue +119 -0
- package/templates/m3e-app/src/modules/demo/components/DemoBusLog.vue +63 -0
- package/templates/m3e-app/src/modules/demo/components/DemoCreateOrderSheet.vue +203 -0
- package/templates/m3e-app/src/modules/demo/components/DemoMetricsSheet.vue +75 -0
- package/templates/m3e-app/src/modules/demo/components/DemoOrderList.vue +75 -0
- package/templates/m3e-app/src/modules/demo/components/DemoPipelineBenchmark.vue +65 -0
- package/templates/m3e-app/src/modules/demo/components/DemoStatCards.vue +71 -0
- package/templates/m3e-app/src/modules/demo/composables/useBenchmark.ts +139 -0
- package/templates/m3e-app/src/modules/demo/composables/useOrderStatus.ts +64 -0
- package/templates/m3e-app/src/modules/demo/composables/useReactiveDemo.ts +171 -0
- package/templates/m3e-app/src/modules/demo/router/routes/demo.routes.ts +22 -0
- package/templates/m3e-app/src/modules/demo/views/DemoView.vue +210 -0
- package/templates/m3e-app/src/modules/demo/views/OrderDetailView.vue +136 -0
- package/templates/m3e-app/src/modules/demo/views/OrderSearchView.vue +74 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryBlock.vue +16 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryCarouselTile.vue +71 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryCustomerForm.vue +76 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryProofOfDelivery.vue +69 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaDay.vue +106 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaMonth.vue +40 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaVisitList.vue +45 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatAttachSheet.vue +106 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatContactPicker.vue +85 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatInviteComposer.vue +131 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatPollComposer.vue +148 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatRecentPhotos.vue +62 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsAccount.vue +79 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsTags.vue +53 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsVerification.vue +73 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/PasswordStrength.vue +37 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryButtons.vue +128 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCarousels.vue +236 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCharts.vue +84 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryFabs.vue +70 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryInputs.vue +219 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryNavigation.vue +141 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryOverlays.vue +368 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryPickers.vue +175 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryProgress.vue +118 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryScale.vue +103 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GallerySelection.vue +219 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryShapes.vue +48 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GallerySurfaces.vue +213 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryTables.vue +185 -0
- package/templates/m3e-app/src/modules/gallery/components/surfaces/SurfacesContainerTransform.vue +46 -0
- package/templates/m3e-app/src/modules/gallery/composables/chatTeam.ts +18 -0
- package/templates/m3e-app/src/modules/gallery/composables/composeKind.ts +2 -0
- package/templates/m3e-app/src/modules/gallery/composables/currentPosition.ts +35 -0
- package/templates/m3e-app/src/modules/gallery/composables/demoOrders.ts +29 -0
- package/templates/m3e-app/src/modules/gallery/composables/featuredStories.ts +73 -0
- package/templates/m3e-app/src/modules/gallery/composables/fieldFormats.ts +36 -0
- package/templates/m3e-app/src/modules/gallery/composables/openExternal.ts +11 -0
- package/templates/m3e-app/src/modules/gallery/composables/routePlan.ts +90 -0
- package/templates/m3e-app/src/modules/gallery/composables/useAgenda.ts +61 -0
- package/templates/m3e-app/src/modules/gallery/composables/useChatCustomers.ts +31 -0
- package/templates/m3e-app/src/modules/gallery/composables/useChatDemo.ts +299 -0
- package/templates/m3e-app/src/modules/gallery/composables/useGallerySections.ts +230 -0
- package/templates/m3e-app/src/modules/gallery/composables/useLocalAttachments.ts +71 -0
- package/templates/m3e-app/src/modules/gallery/composables/useLocationShare.ts +38 -0
- package/templates/m3e-app/src/modules/gallery/composables/usePhotoScenes.ts +82 -0
- package/templates/m3e-app/src/modules/gallery/composables/useTreeDemo.ts +67 -0
- package/templates/m3e-app/src/modules/gallery/composables/wilayas.ts +67 -0
- package/templates/m3e-app/src/modules/gallery/router/routes/gallery.routes.ts +52 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryAgendaView.vue +117 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryChatView.vue +292 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryContactsView.vue +77 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryFeaturedView.vue +60 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryLoginView.vue +135 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryOnboardingView.vue +116 -0
- package/templates/m3e-app/src/modules/gallery/views/GallerySectionView.vue +27 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryTabsView.vue +135 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryView.vue +50 -0
- package/templates/m3e-app/src/modules/home/components/HomeHero.vue +57 -0
- package/templates/m3e-app/src/modules/home/composables/useHomeFeatures.ts +128 -0
- package/templates/m3e-app/src/modules/home/composables/useOpenFeature.ts +22 -0
- package/templates/m3e-app/src/modules/home/router/routes/home.routes.ts +21 -0
- package/templates/m3e-app/src/modules/home/views/FeatureDetailView.vue +66 -0
- package/templates/m3e-app/src/modules/home/views/HomeView.vue +47 -0
- package/templates/m3e-app/src/modules/settings/components/RolePalette.vue +38 -0
- package/templates/m3e-app/src/modules/settings/components/SeedSwatches.vue +41 -0
- package/templates/m3e-app/src/modules/settings/components/SettingsChoice.vue +43 -0
- package/templates/m3e-app/src/modules/settings/components/StudioPreview.vue +52 -0
- package/templates/m3e-app/src/modules/settings/router/routes/settings.routes.ts +17 -0
- package/templates/m3e-app/src/modules/settings/types.ts +7 -0
- package/templates/m3e-app/src/modules/settings/views/ColorStudioView.vue +166 -0
- package/templates/m3e-app/src/modules/settings/views/SettingsView.vue +157 -0
- package/templates/m3e-app/src/plugins/bootstrapError.ts +62 -0
- package/templates/m3e-app/src/plugins/capacitor/index.ts +14 -0
- package/templates/m3e-app/src/plugins/capacitor/useAndroidBackButton.ts +36 -0
- package/templates/m3e-app/src/plugins/capacitor/useKeyboard.ts +48 -0
- package/templates/m3e-app/src/plugins/capacitor/useSplashScreen.ts +43 -0
- package/templates/m3e-app/src/plugins/capacitor/useStatusBar.ts +16 -0
- package/templates/m3e-app/src/plugins/framework7.plugin.ts +36 -0
- package/templates/m3e-app/src/plugins/i18n.plugin.ts +87 -0
- package/templates/m3e-app/src/plugins/m3e.plugin.ts +22 -0
- package/templates/m3e-app/src/plugins/seed.plugin.ts +7 -0
- package/templates/m3e-app/src/plugins/sqlite.plugin.ts +32 -0
- package/templates/m3e-app/src/router/global/global.routes.ts +12 -0
- package/templates/m3e-app/src/router/index.ts +20 -0
- package/templates/m3e-app/src/shared/components/error/404.vue +13 -0
- package/templates/m3e-app/src/shared/components/layout/EmptyState.vue +30 -0
- package/templates/m3e-app/src/shared/components/layout/SectionHeader.vue +10 -0
- package/templates/m3e-app/src/shared/components/page/AppPage.vue +104 -0
- package/templates/m3e-app/src/shared/composables/navigation/useActiveTab.ts +39 -0
- package/templates/m3e-app/src/shared/composables/navigation/useContainerTransform.ts +117 -0
- package/templates/m3e-app/src/shared/composables/navigation/useNavigationGuard.ts +48 -0
- package/templates/m3e-app/src/shared/composables/navigation/useNavigationVisibility.ts +64 -0
- package/templates/m3e-app/src/shared/composables/navigation/useViewRouter.ts +25 -0
- package/templates/m3e-app/src/shared/composables/navigation/useWindowClass.ts +22 -0
- package/templates/m3e-app/src/shared/composables/theme/useThemeSettings.ts +155 -0
- package/templates/m3e-app/src/shared/database/candidates/capacitorSqlite.ts +36 -0
- package/templates/m3e-app/src/shared/database/candidates/index.ts +4 -0
- package/templates/m3e-app/src/shared/database/candidates/opfsSahPool.ts +25 -0
- package/templates/m3e-app/src/shared/database/candidates/types.ts +56 -0
- package/templates/m3e-app/src/shared/database/candidates/waSqlite.ts +56 -0
- package/templates/m3e-app/src/shared/database/database.ts +219 -0
- package/templates/m3e-app/src/shared/database/index.ts +3 -0
- package/templates/m3e-app/src/shared/database/migrations.ts +223 -0
- package/templates/m3e-app/src/shared/database/opfs.worker.ts +5 -0
- package/templates/m3e-app/src/shared/database/queries.ts +26 -0
- package/templates/m3e-app/src/shared/database/schema.ts +153 -0
- package/templates/m3e-app/src/shared/database/storage.ts +64 -0
- package/templates/m3e-app/src/shared/database/wa.worker.ts +5 -0
- package/templates/m3e-app/src/shared/utils/lazyRoute.ts +41 -0
- package/templates/m3e-app/src/shared/utils/resolvers/resolvers.ts +42 -0
- package/templates/m3e-app/src/shared/utils/textDirection.ts +22 -0
- package/templates/m3e-app/src/shared/utils/theme/themeSettings.ts +59 -0
- package/templates/m3e-app/src/shared/utils/tone.ts +8 -0
- package/templates/m3e-app/tests/benchmark.suite.test.ts +104 -0
- package/templates/m3e-app/tests/containerTransform.test.ts +58 -0
- package/templates/m3e-app/tests/fieldFormats.test.ts +25 -0
- package/templates/m3e-app/tests/lazyRoute.test.ts +30 -0
- package/templates/m3e-app/tests/locales.test.ts +73 -0
- package/templates/m3e-app/tests/migrations.test.ts +74 -0
- package/templates/m3e-app/tests/navigationGuard.test.ts +82 -0
- package/templates/m3e-app/tests/openDatabase.test.ts +54 -0
- package/templates/m3e-app/tests/sales.repository.test.ts +329 -0
- package/templates/m3e-app/tests/storage.test.ts +117 -0
- package/templates/m3e-app/tests/textDirection.test.ts +11 -0
- package/templates/m3e-app/tooling/linkedPackages.ts +112 -0
- package/templates/m3e-app/tsconfig.json +21 -0
- package/templates/m3e-app/vite.config.ts +144 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<M3AttachSheet
|
|
3
|
+
v-model:open="open"
|
|
4
|
+
:options="options"
|
|
5
|
+
:label="t('gallery.chat.attachTitle')"
|
|
6
|
+
:title="t('gallery.chat.attachTitle')"
|
|
7
|
+
@select="choose"
|
|
8
|
+
>
|
|
9
|
+
<template #recent>
|
|
10
|
+
<ChatRecentPhotos
|
|
11
|
+
v-if="photos.length"
|
|
12
|
+
:photos="photos"
|
|
13
|
+
:label="t('gallery.chat.recent')"
|
|
14
|
+
:send-label="(alt) => t('gallery.chat.sendPhoto', { name: alt })"
|
|
15
|
+
@pick="pickRecent"
|
|
16
|
+
/>
|
|
17
|
+
</template>
|
|
18
|
+
</M3AttachSheet>
|
|
19
|
+
<input ref="gallery" type="file" accept="image/*" multiple hidden @change="receive" />
|
|
20
|
+
<input ref="camera" type="file" accept="image/*" capture="environment" hidden @change="receive" />
|
|
21
|
+
<input ref="file" type="file" hidden @change="receive" />
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import type { AttachOption, MessageImage, PhotoItem } from "@cavulsqa/m3e-vue";
|
|
26
|
+
import { markRaw } from "vue";
|
|
27
|
+
import BallotIcon from "~icons/material-symbols/ballot-outline-rounded";
|
|
28
|
+
import CameraIcon from "~icons/material-symbols/photo-camera-outline-rounded";
|
|
29
|
+
import ContactIcon from "~icons/material-symbols/person-outline-rounded";
|
|
30
|
+
import EventIcon from "~icons/material-symbols/event-outline-rounded";
|
|
31
|
+
import FileIcon from "~icons/material-symbols/description-outline-rounded";
|
|
32
|
+
import GalleryIcon from "~icons/material-symbols/photo-library-outline-rounded";
|
|
33
|
+
import LocationIcon from "~icons/material-symbols/location-on-outline-rounded";
|
|
34
|
+
import ChatRecentPhotos from "@/modules/gallery/components/chat/ChatRecentPhotos.vue";
|
|
35
|
+
import type { ComposeKind } from "@/modules/gallery/composables/composeKind";
|
|
36
|
+
import { useLocalAttachments } from "@/modules/gallery/composables/useLocalAttachments";
|
|
37
|
+
|
|
38
|
+
defineProps<{ photos: readonly PhotoItem[] }>();
|
|
39
|
+
|
|
40
|
+
const open = defineModel<boolean>("open", { default: false });
|
|
41
|
+
const emit = defineEmits<{
|
|
42
|
+
image: [image: MessageImage];
|
|
43
|
+
text: [text: string];
|
|
44
|
+
recent: [index: number];
|
|
45
|
+
failed: [name: string];
|
|
46
|
+
compose: [kind: ComposeKind];
|
|
47
|
+
}>();
|
|
48
|
+
|
|
49
|
+
const { t, locale } = useI18n();
|
|
50
|
+
const { toImage, describe } = useLocalAttachments();
|
|
51
|
+
const pickers = {
|
|
52
|
+
gallery: useTemplateRef<HTMLInputElement>("gallery"),
|
|
53
|
+
camera: useTemplateRef<HTMLInputElement>("camera"),
|
|
54
|
+
file: useTemplateRef<HTMLInputElement>("file"),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const OPTIONS = [
|
|
58
|
+
{ id: "gallery", icon: GalleryIcon, tone: "primary", shape: "cookie9Sided" },
|
|
59
|
+
{ id: "camera", icon: CameraIcon, tone: "tertiary", shape: "clover4Leaf" },
|
|
60
|
+
{ id: "file", icon: FileIcon, tone: "secondary", shape: "gem" },
|
|
61
|
+
{ id: "location", icon: LocationIcon, tone: "primary", shape: "softBurst" },
|
|
62
|
+
{ id: "contact", icon: ContactIcon, tone: "secondary", shape: "puffy" },
|
|
63
|
+
{ id: "poll", icon: BallotIcon, tone: "tertiary", shape: "pentagon" },
|
|
64
|
+
{ id: "event", icon: EventIcon, tone: "primary", shape: "sunny" },
|
|
65
|
+
] as const;
|
|
66
|
+
|
|
67
|
+
const options = computed<AttachOption[]>(() =>
|
|
68
|
+
OPTIONS.map((option) => ({
|
|
69
|
+
...option,
|
|
70
|
+
icon: markRaw(option.icon),
|
|
71
|
+
label: t(`gallery.chat.attachOptions.${option.id}`),
|
|
72
|
+
})),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
function choose(id: string) {
|
|
76
|
+
if (id === "gallery" || id === "camera" || id === "file") {
|
|
77
|
+
const input = pickers[id].value;
|
|
78
|
+
if (input) {
|
|
79
|
+
input.value = "";
|
|
80
|
+
input.click();
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (id === "location" || id === "contact" || id === "poll" || id === "event") emit("compose", id);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function pickRecent(index: number) {
|
|
88
|
+
open.value = false;
|
|
89
|
+
emit("recent", index);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function receive(event: Event) {
|
|
93
|
+
const input = event.target as HTMLInputElement;
|
|
94
|
+
const files = [...(input.files ?? [])];
|
|
95
|
+
input.value = "";
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
if (file.type.startsWith("image/")) {
|
|
98
|
+
const image = await toImage(file);
|
|
99
|
+
if (image) emit("image", image);
|
|
100
|
+
else emit("failed", file.name);
|
|
101
|
+
} else {
|
|
102
|
+
emit("text", describe(file, locale.value));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
</script>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<M3BottomSheet v-model:open="open" :title="t('gallery.chat.contact.title')" @closed="filter = ''">
|
|
3
|
+
<div class="px-4 pb-2">
|
|
4
|
+
<M3SearchBar
|
|
5
|
+
v-model="filter"
|
|
6
|
+
:placeholder="t('gallery.chat.contact.search')"
|
|
7
|
+
:clear-label="t('gallery.inputs.clear')"
|
|
8
|
+
/>
|
|
9
|
+
</div>
|
|
10
|
+
<M3List variant="standard" :label="t('gallery.chat.contact.title')">
|
|
11
|
+
<M3ListItem
|
|
12
|
+
v-for="customer in matches"
|
|
13
|
+
:key="customer.id"
|
|
14
|
+
clickable
|
|
15
|
+
:headline="customer.name"
|
|
16
|
+
:supporting="customer.city"
|
|
17
|
+
@click="pick(customer)"
|
|
18
|
+
>
|
|
19
|
+
<template #leading>
|
|
20
|
+
<span
|
|
21
|
+
class="type-title-medium grid size-10 place-items-center rounded-full bg-secondary-container text-on-secondary-container"
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
>
|
|
24
|
+
{{ initial(customer.name) }}
|
|
25
|
+
</span>
|
|
26
|
+
</template>
|
|
27
|
+
</M3ListItem>
|
|
28
|
+
</M3List>
|
|
29
|
+
<p
|
|
30
|
+
v-if="!loading && matches.length === 0"
|
|
31
|
+
class="type-body-medium m-0 px-6 py-8 text-center text-on-surface-variant"
|
|
32
|
+
role="status"
|
|
33
|
+
>
|
|
34
|
+
{{ emptyText }}
|
|
35
|
+
</p>
|
|
36
|
+
</M3BottomSheet>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import type { MessageContact } from "@cavulsqa/m3e-vue";
|
|
41
|
+
|
|
42
|
+
interface Customer {
|
|
43
|
+
id: number;
|
|
44
|
+
name: string;
|
|
45
|
+
city: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const props = defineProps<{
|
|
49
|
+
customers: readonly Customer[];
|
|
50
|
+
loading?: boolean;
|
|
51
|
+
failed?: boolean;
|
|
52
|
+
}>();
|
|
53
|
+
const open = defineModel<boolean>("open", { default: false });
|
|
54
|
+
const emit = defineEmits<{ pick: [contact: MessageContact] }>();
|
|
55
|
+
const { t, locale } = useI18n();
|
|
56
|
+
|
|
57
|
+
const filter = ref("");
|
|
58
|
+
const needle = computed(() => filter.value.trim().toLocaleLowerCase(locale.value));
|
|
59
|
+
|
|
60
|
+
const matches = computed(() =>
|
|
61
|
+
needle.value
|
|
62
|
+
? props.customers.filter((customer) =>
|
|
63
|
+
`${customer.name} ${customer.city}`.toLocaleLowerCase(locale.value).includes(needle.value),
|
|
64
|
+
)
|
|
65
|
+
: props.customers,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const emptyText = computed(() => {
|
|
69
|
+
if (props.failed) return t("gallery.chat.contact.failed");
|
|
70
|
+
return needle.value ? t("gallery.inputs.noMatches") : t("gallery.chat.contact.empty");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/** First and last word's first letter, so "Customer 2" and "Customer 8" do not share a monogram. */
|
|
74
|
+
function initial(name: string) {
|
|
75
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
76
|
+
const first = [...(words[0] ?? "")][0] ?? "";
|
|
77
|
+
const last = words.length > 1 ? ([...(words.at(-1) ?? "")][0] ?? "") : "";
|
|
78
|
+
return `${first}${last}`.toLocaleUpperCase(locale.value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function pick(customer: Customer) {
|
|
82
|
+
emit("pick", { name: customer.name, detail: customer.city });
|
|
83
|
+
open.value = false;
|
|
84
|
+
}
|
|
85
|
+
</script>
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<M3BottomSheet v-model:open="open" :title="t('gallery.chat.invite.title')">
|
|
3
|
+
<div class="flex flex-col gap-3 px-4 pb-2">
|
|
4
|
+
<M3TextField
|
|
5
|
+
v-model="title"
|
|
6
|
+
:label="t('gallery.chat.invite.name')"
|
|
7
|
+
:maxlength="TITLE_MAX"
|
|
8
|
+
autocomplete="off"
|
|
9
|
+
/>
|
|
10
|
+
<M3List variant="segmented">
|
|
11
|
+
<M3ListItem
|
|
12
|
+
clickable
|
|
13
|
+
:overline="t('gallery.chat.invite.date')"
|
|
14
|
+
:headline="dateLabel"
|
|
15
|
+
@click="dateOpen = true"
|
|
16
|
+
>
|
|
17
|
+
<template #leading><i-ms-calendar-today-outline-rounded /></template>
|
|
18
|
+
</M3ListItem>
|
|
19
|
+
<M3ListItem
|
|
20
|
+
clickable
|
|
21
|
+
:overline="t('gallery.chat.invite.time')"
|
|
22
|
+
:headline="timeLabel"
|
|
23
|
+
@click="timeOpen = true"
|
|
24
|
+
>
|
|
25
|
+
<template #leading><i-ms-schedule-outline-rounded /></template>
|
|
26
|
+
</M3ListItem>
|
|
27
|
+
</M3List>
|
|
28
|
+
<M3TextField
|
|
29
|
+
v-model="place"
|
|
30
|
+
:label="t('gallery.chat.invite.place')"
|
|
31
|
+
:maxlength="PLACE_MAX"
|
|
32
|
+
autocomplete="off"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<template #footer>
|
|
37
|
+
<div class="flex justify-end">
|
|
38
|
+
<M3Button size="m" :disabled="!ready" @click="submit">
|
|
39
|
+
<template #icon><i-ms-send-rounded class="rtl:-scale-x-100" /></template>
|
|
40
|
+
{{ t("gallery.chat.send") }}
|
|
41
|
+
</M3Button>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
</M3BottomSheet>
|
|
45
|
+
|
|
46
|
+
<M3DatePicker
|
|
47
|
+
v-model="date"
|
|
48
|
+
v-model:open="dateOpen"
|
|
49
|
+
:min="today"
|
|
50
|
+
:locale="locale"
|
|
51
|
+
:title="t('gallery.pickers.selectDate')"
|
|
52
|
+
:empty-headline="t('gallery.pickers.selectedDate')"
|
|
53
|
+
:confirm-label="t('gallery.pickers.ok')"
|
|
54
|
+
:dismiss-label="t('gallery.overlays.cancel')"
|
|
55
|
+
:input-label="t('gallery.pickers.typeDate')"
|
|
56
|
+
:calendar-label="t('gallery.pickers.useCalendar')"
|
|
57
|
+
:invalid-label="t('gallery.pickers.invalid')"
|
|
58
|
+
/>
|
|
59
|
+
<M3TimePicker
|
|
60
|
+
v-model="time"
|
|
61
|
+
v-model:open="timeOpen"
|
|
62
|
+
:minute-step="15"
|
|
63
|
+
:locale="locale"
|
|
64
|
+
:title="t('gallery.pickers.selectTime')"
|
|
65
|
+
:confirm-label="t('gallery.pickers.ok')"
|
|
66
|
+
:dismiss-label="t('gallery.overlays.cancel')"
|
|
67
|
+
:dial-label="t('gallery.pickers.useDial')"
|
|
68
|
+
:input-label="t('gallery.pickers.useKeyboard')"
|
|
69
|
+
/>
|
|
70
|
+
</template>
|
|
71
|
+
|
|
72
|
+
<script setup lang="ts">
|
|
73
|
+
import {
|
|
74
|
+
addDays,
|
|
75
|
+
formatIso,
|
|
76
|
+
formatIsoTime,
|
|
77
|
+
todayIso,
|
|
78
|
+
type IsoDate,
|
|
79
|
+
type IsoTime,
|
|
80
|
+
type MessageInvite,
|
|
81
|
+
} from "@cavulsqa/m3e-vue";
|
|
82
|
+
|
|
83
|
+
const TITLE_MAX = 80;
|
|
84
|
+
const PLACE_MAX = 80;
|
|
85
|
+
const DEFAULT_TIME: IsoTime = "09:00";
|
|
86
|
+
|
|
87
|
+
const open = defineModel<boolean>("open", { default: false });
|
|
88
|
+
const emit = defineEmits<{ send: [invite: MessageInvite] }>();
|
|
89
|
+
const { t, locale } = useI18n();
|
|
90
|
+
|
|
91
|
+
const today = todayIso();
|
|
92
|
+
const title = ref("");
|
|
93
|
+
const place = ref("");
|
|
94
|
+
const date = ref<IsoDate | null>(addDays(today, 1));
|
|
95
|
+
const time = ref<IsoTime | null>(DEFAULT_TIME);
|
|
96
|
+
const dateOpen = ref(false);
|
|
97
|
+
const timeOpen = ref(false);
|
|
98
|
+
|
|
99
|
+
const dateLabel = computed(() =>
|
|
100
|
+
date.value
|
|
101
|
+
? formatIso(date.value, locale.value, { weekday: "long", day: "numeric", month: "long" })
|
|
102
|
+
: t("gallery.pickers.pick"),
|
|
103
|
+
);
|
|
104
|
+
const timeLabel = computed(() =>
|
|
105
|
+
time.value ? formatIsoTime(time.value, locale.value) : t("gallery.pickers.pickTime"),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const start = computed(() => {
|
|
109
|
+
if (!date.value || !time.value) return null;
|
|
110
|
+
const at = new Date(`${date.value}T${time.value}:00`);
|
|
111
|
+
return Number.isNaN(at.getTime()) ? null : at;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const ready = computed(() => Boolean(title.value.trim()) && start.value !== null);
|
|
115
|
+
|
|
116
|
+
function submit() {
|
|
117
|
+
if (!ready.value || !start.value) return;
|
|
118
|
+
const where = place.value.trim();
|
|
119
|
+
emit("send", {
|
|
120
|
+
title: title.value.trim(),
|
|
121
|
+
start: start.value,
|
|
122
|
+
...(where ? { place: where } : {}),
|
|
123
|
+
answers: { going: 0, maybe: 0, no: 0 },
|
|
124
|
+
});
|
|
125
|
+
title.value = "";
|
|
126
|
+
place.value = "";
|
|
127
|
+
date.value = addDays(todayIso(), 1);
|
|
128
|
+
time.value = DEFAULT_TIME;
|
|
129
|
+
open.value = false;
|
|
130
|
+
}
|
|
131
|
+
</script>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<M3BottomSheet v-model:open="open" :title="t('gallery.chat.poll.title')">
|
|
3
|
+
<div class="flex flex-col gap-3 px-4 pb-2">
|
|
4
|
+
<M3TextField
|
|
5
|
+
v-model="question"
|
|
6
|
+
:label="t('gallery.chat.poll.question')"
|
|
7
|
+
:maxlength="QUESTION_MAX"
|
|
8
|
+
autocomplete="off"
|
|
9
|
+
/>
|
|
10
|
+
<TransitionGroup tag="div" name="poll-option" class="relative flex flex-col gap-3">
|
|
11
|
+
<div v-for="(option, index) in options" :key="option.key" class="flex items-center gap-1">
|
|
12
|
+
<div class="min-w-0 flex-1">
|
|
13
|
+
<M3TextField
|
|
14
|
+
v-model="option.label"
|
|
15
|
+
:label="t('gallery.chat.poll.option', { number: index + 1 })"
|
|
16
|
+
:error="repeated.has(index) ? t('gallery.chat.poll.repeated') : undefined"
|
|
17
|
+
autocomplete="off"
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
<M3IconButton
|
|
21
|
+
v-if="options.length > MIN_OPTIONS"
|
|
22
|
+
:label="t('gallery.chat.poll.remove', { number: index + 1 })"
|
|
23
|
+
@click="remove(index)"
|
|
24
|
+
>
|
|
25
|
+
<i-ms-close-rounded />
|
|
26
|
+
</M3IconButton>
|
|
27
|
+
</div>
|
|
28
|
+
</TransitionGroup>
|
|
29
|
+
<M3Button v-if="options.length < MAX_OPTIONS" variant="text" class="self-start" @click="add">
|
|
30
|
+
<template #icon><i-ms-add-rounded /></template>
|
|
31
|
+
{{ t("gallery.chat.poll.add") }}
|
|
32
|
+
</M3Button>
|
|
33
|
+
<div class="flex items-center gap-4 py-2">
|
|
34
|
+
<div class="flex min-w-0 flex-1 flex-col">
|
|
35
|
+
<span class="type-body-large text-on-surface">{{ t("gallery.chat.poll.multiple") }}</span>
|
|
36
|
+
<span class="type-body-medium text-on-surface-variant">
|
|
37
|
+
{{ t("gallery.chat.poll.multipleHint") }}
|
|
38
|
+
</span>
|
|
39
|
+
</div>
|
|
40
|
+
<M3Switch v-model="multiple" :label="t('gallery.chat.poll.multiple')" />
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<template #footer>
|
|
45
|
+
<div class="flex justify-end">
|
|
46
|
+
<M3Button size="m" :disabled="!ready" @click="submit">
|
|
47
|
+
<template #icon><i-ms-send-rounded class="rtl:-scale-x-100" /></template>
|
|
48
|
+
{{ t("gallery.chat.send") }}
|
|
49
|
+
</M3Button>
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
</M3BottomSheet>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<script setup lang="ts">
|
|
56
|
+
import type { MessagePoll } from "@cavulsqa/m3e-vue";
|
|
57
|
+
|
|
58
|
+
const MIN_OPTIONS = 2;
|
|
59
|
+
const MAX_OPTIONS = 6;
|
|
60
|
+
const QUESTION_MAX = 140;
|
|
61
|
+
|
|
62
|
+
const open = defineModel<boolean>("open", { default: false });
|
|
63
|
+
const emit = defineEmits<{ send: [poll: MessagePoll] }>();
|
|
64
|
+
const { t } = useI18n();
|
|
65
|
+
|
|
66
|
+
let keys = 0;
|
|
67
|
+
const blankOption = () => ({ key: (keys += 1), label: "" });
|
|
68
|
+
|
|
69
|
+
const question = ref("");
|
|
70
|
+
const options = ref([blankOption(), blankOption()]);
|
|
71
|
+
const multiple = ref(false);
|
|
72
|
+
|
|
73
|
+
const normalised = computed(() =>
|
|
74
|
+
options.value.map((option) => option.label.trim().toLocaleLowerCase()),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
/** Indexes of answers that repeat an earlier one: two identical answers split one opinion in two. */
|
|
78
|
+
const repeated = computed(() => {
|
|
79
|
+
const seen = new Set<string>();
|
|
80
|
+
const indexes = new Set<number>();
|
|
81
|
+
normalised.value.forEach((label, index) => {
|
|
82
|
+
if (!label) return;
|
|
83
|
+
if (seen.has(label)) indexes.add(index);
|
|
84
|
+
seen.add(label);
|
|
85
|
+
});
|
|
86
|
+
return indexes;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const filled = computed(() => options.value.filter((option) => option.label.trim()));
|
|
90
|
+
const ready = computed(
|
|
91
|
+
() =>
|
|
92
|
+
Boolean(question.value.trim()) && filled.value.length >= MIN_OPTIONS && !repeated.value.size,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
function add() {
|
|
96
|
+
if (options.value.length < MAX_OPTIONS) options.value.push(blankOption());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function remove(index: number) {
|
|
100
|
+
if (options.value.length > MIN_OPTIONS) options.value.splice(index, 1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function submit() {
|
|
104
|
+
if (!ready.value) return;
|
|
105
|
+
emit("send", {
|
|
106
|
+
question: question.value.trim(),
|
|
107
|
+
multiple: multiple.value,
|
|
108
|
+
options: filled.value.map((option, index) => ({
|
|
109
|
+
id: `o${index + 1}`,
|
|
110
|
+
label: option.label.trim(),
|
|
111
|
+
votes: 0,
|
|
112
|
+
})),
|
|
113
|
+
});
|
|
114
|
+
question.value = "";
|
|
115
|
+
options.value = [blankOption(), blankOption()];
|
|
116
|
+
multiple.value = false;
|
|
117
|
+
open.value = false;
|
|
118
|
+
}
|
|
119
|
+
</script>
|
|
120
|
+
|
|
121
|
+
<style scoped>
|
|
122
|
+
.poll-option-enter-active,
|
|
123
|
+
.poll-option-leave-active {
|
|
124
|
+
transition:
|
|
125
|
+
opacity var(--md-sys-motion-spring-fast-effects-duration)
|
|
126
|
+
var(--md-sys-motion-spring-fast-effects),
|
|
127
|
+
translate var(--md-sys-motion-spring-fast-spatial-duration)
|
|
128
|
+
var(--md-sys-motion-spring-fast-spatial);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.poll-option-enter-from,
|
|
132
|
+
.poll-option-leave-to {
|
|
133
|
+
opacity: 0;
|
|
134
|
+
translate: 0 -8px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.poll-option-leave-active {
|
|
138
|
+
position: absolute;
|
|
139
|
+
inset-inline: 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@media (prefers-reduced-motion: reduce) {
|
|
143
|
+
.poll-option-enter-active,
|
|
144
|
+
.poll-option-leave-active {
|
|
145
|
+
transition: none;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
</style>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section :aria-label="label">
|
|
3
|
+
<h3 class="type-title-small m-0 px-1 pb-2 text-on-surface-variant">{{ label }}</h3>
|
|
4
|
+
<ul class="chat-recent m-0 flex list-none gap-2 overflow-x-auto p-0">
|
|
5
|
+
<li v-for="(photo, index) in photos" :key="photo.src" class="shrink-0">
|
|
6
|
+
<button
|
|
7
|
+
type="button"
|
|
8
|
+
class="chat-recent__photo m3-focus-ring rounded-lg"
|
|
9
|
+
:aria-label="sendLabel(photo.alt)"
|
|
10
|
+
@click="emit('pick', index)"
|
|
11
|
+
>
|
|
12
|
+
<img
|
|
13
|
+
:src="photo.src"
|
|
14
|
+
:alt="photo.alt"
|
|
15
|
+
class="size-full object-cover"
|
|
16
|
+
loading="lazy"
|
|
17
|
+
decoding="async"
|
|
18
|
+
draggable="false"
|
|
19
|
+
/>
|
|
20
|
+
</button>
|
|
21
|
+
</li>
|
|
22
|
+
</ul>
|
|
23
|
+
</section>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup lang="ts">
|
|
27
|
+
import type { PhotoItem } from "@cavulsqa/m3e-vue";
|
|
28
|
+
|
|
29
|
+
defineProps<{
|
|
30
|
+
photos: readonly PhotoItem[];
|
|
31
|
+
label: string;
|
|
32
|
+
sendLabel: (alt: string) => string;
|
|
33
|
+
}>();
|
|
34
|
+
|
|
35
|
+
const emit = defineEmits<{ pick: [index: number] }>();
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<style scoped>
|
|
39
|
+
.chat-recent {
|
|
40
|
+
scrollbar-width: none;
|
|
41
|
+
overscroll-behavior-x: contain;
|
|
42
|
+
scroll-snap-type: x proximity;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.chat-recent__photo {
|
|
46
|
+
display: block;
|
|
47
|
+
width: 88px;
|
|
48
|
+
height: 88px;
|
|
49
|
+
padding: 0;
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
border: 0;
|
|
52
|
+
background: var(--md-sys-color-surface-container-highest);
|
|
53
|
+
scroll-snap-align: start;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
transition: scale var(--md-sys-motion-spring-fast-spatial-duration)
|
|
56
|
+
var(--md-sys-motion-spring-fast-spatial);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.chat-recent__photo:active {
|
|
60
|
+
scale: 0.94;
|
|
61
|
+
}
|
|
62
|
+
</style>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<GalleryBlock :title="t('gallery.inputs.account')" :note="t('gallery.inputs.accountNote')" stack>
|
|
3
|
+
<div class="flex flex-col gap-2">
|
|
4
|
+
<M3TextField
|
|
5
|
+
v-model="password"
|
|
6
|
+
variant="outlined"
|
|
7
|
+
:label="t('gallery.inputs.password')"
|
|
8
|
+
:type="revealed ? 'text' : 'password'"
|
|
9
|
+
autocomplete="new-password"
|
|
10
|
+
autocapitalize="off"
|
|
11
|
+
spellcheck="false"
|
|
12
|
+
>
|
|
13
|
+
<template #leading><i-ms-lock-outline-rounded /></template>
|
|
14
|
+
<template #trailing>
|
|
15
|
+
<M3IconButton
|
|
16
|
+
:label="t(revealed ? 'gallery.inputs.hidePassword' : 'gallery.inputs.showPassword')"
|
|
17
|
+
:aria-pressed="revealed"
|
|
18
|
+
@pointerdown.prevent
|
|
19
|
+
@click="revealed = !revealed"
|
|
20
|
+
>
|
|
21
|
+
<i-ms-visibility-off-outline-rounded v-if="revealed" />
|
|
22
|
+
<i-ms-visibility-outline-rounded v-else />
|
|
23
|
+
</M3IconButton>
|
|
24
|
+
</template>
|
|
25
|
+
</M3TextField>
|
|
26
|
+
<PasswordStrength
|
|
27
|
+
:score="score"
|
|
28
|
+
:label="score ? t(`gallery.inputs.strength.${STRENGTH[score]}`) : ''"
|
|
29
|
+
class="px-1"
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
<M3TextField
|
|
33
|
+
v-model="phone"
|
|
34
|
+
variant="outlined"
|
|
35
|
+
:label="t('gallery.inputs.phone')"
|
|
36
|
+
prefix="+213"
|
|
37
|
+
inputmode="tel"
|
|
38
|
+
autocomplete="tel-national"
|
|
39
|
+
:supporting="t('gallery.inputs.phoneHint')"
|
|
40
|
+
:error="phoneError"
|
|
41
|
+
@blur="touched = true"
|
|
42
|
+
>
|
|
43
|
+
<template #leading><i-ms-call-outline-rounded /></template>
|
|
44
|
+
</M3TextField>
|
|
45
|
+
</GalleryBlock>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<script setup lang="ts">
|
|
49
|
+
import GalleryBlock from "@/modules/gallery/components/GalleryBlock.vue";
|
|
50
|
+
import PasswordStrength from "@/modules/gallery/components/inputs/PasswordStrength.vue";
|
|
51
|
+
import {
|
|
52
|
+
formatMobile,
|
|
53
|
+
isMobile,
|
|
54
|
+
mobileDigits,
|
|
55
|
+
passwordScore,
|
|
56
|
+
} from "@/modules/gallery/composables/fieldFormats";
|
|
57
|
+
|
|
58
|
+
const STRENGTH = [null, "weak", "fair", "good", "strong"] as const;
|
|
59
|
+
|
|
60
|
+
const { t } = useI18n();
|
|
61
|
+
const password = ref("");
|
|
62
|
+
const revealed = ref(false);
|
|
63
|
+
const digits = ref("");
|
|
64
|
+
const touched = ref(false);
|
|
65
|
+
|
|
66
|
+
const score = computed(() => (password.value ? passwordScore(password.value) : 0));
|
|
67
|
+
const phone = computed({
|
|
68
|
+
get: () => formatMobile(digits.value),
|
|
69
|
+
set: (raw: string) => {
|
|
70
|
+
digits.value = mobileDigits(raw);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
const phoneError = computed(() => {
|
|
74
|
+
const complete = digits.value.length === 9;
|
|
75
|
+
if (complete && !isMobile(digits.value)) return t("gallery.inputs.phoneInvalid");
|
|
76
|
+
if (touched.value && digits.value.length > 0 && !complete) return t("gallery.inputs.phoneShort");
|
|
77
|
+
return undefined;
|
|
78
|
+
});
|
|
79
|
+
</script>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<GalleryBlock :title="t('gallery.inputs.tags')" :note="t('gallery.inputs.tagsNote')" stack>
|
|
3
|
+
<M3ChipField
|
|
4
|
+
v-model="tags"
|
|
5
|
+
:label="t('gallery.inputs.tagsLabel')"
|
|
6
|
+
:placeholder="t('gallery.inputs.tagsPlaceholder')"
|
|
7
|
+
:supporting="t('gallery.inputs.tagsHint')"
|
|
8
|
+
:max="MAX"
|
|
9
|
+
:validate="valid"
|
|
10
|
+
:remove-label="(tag: string) => t('gallery.inputs.removeTag', { tag })"
|
|
11
|
+
/>
|
|
12
|
+
<div
|
|
13
|
+
v-if="suggestions.length"
|
|
14
|
+
class="flex flex-wrap gap-2"
|
|
15
|
+
role="group"
|
|
16
|
+
:aria-label="t('gallery.inputs.suggested')"
|
|
17
|
+
>
|
|
18
|
+
<M3Chip
|
|
19
|
+
v-for="suggestion in suggestions"
|
|
20
|
+
:key="suggestion"
|
|
21
|
+
kind="suggestion"
|
|
22
|
+
:label="suggestion"
|
|
23
|
+
@click="add(suggestion)"
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
</GalleryBlock>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script setup lang="ts">
|
|
30
|
+
import { hasEntry } from "@cavulsqa/m3e-vue";
|
|
31
|
+
import GalleryBlock from "@/modules/gallery/components/GalleryBlock.vue";
|
|
32
|
+
|
|
33
|
+
const MAX = 6;
|
|
34
|
+
|
|
35
|
+
const { t } = useI18n();
|
|
36
|
+
const tags = ref<string[]>(["Wholesale", "Oran"]);
|
|
37
|
+
const SUGGESTED = computed(() =>
|
|
38
|
+
["nightDelivery", "pharmacy", "cashOnly", "keyAccount"].map((key) =>
|
|
39
|
+
t(`gallery.inputs.suggestedTags.${key}`),
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
const suggestions = computed(() =>
|
|
43
|
+
tags.value.length >= MAX ? [] : SUGGESTED.value.filter((tag) => !hasEntry(tags.value, tag)),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
function valid(value: string) {
|
|
47
|
+
return value.length >= 2 && value.length <= 24;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function add(tag: string) {
|
|
51
|
+
if (tags.value.length < MAX && !hasEntry(tags.value, tag)) tags.value = [...tags.value, tag];
|
|
52
|
+
}
|
|
53
|
+
</script>
|