contactstudiocstools 1.0.224

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.
Files changed (91) hide show
  1. package/README.md +94 -0
  2. package/dist/module.cjs +5 -0
  3. package/dist/module.d.ts +7 -0
  4. package/dist/module.json +5 -0
  5. package/dist/module.mjs +72 -0
  6. package/dist/runtime/components/Atom.Alert.vue +46 -0
  7. package/dist/runtime/components/Atom.Auth.vue +37 -0
  8. package/dist/runtime/components/Atom.BannerChatEmpty.vue +18 -0
  9. package/dist/runtime/components/Atom.BannerPage404.vue +28 -0
  10. package/dist/runtime/components/Atom.BannerPageUnauthorized.vue +18 -0
  11. package/dist/runtime/components/Atom.Breadcrumb.vue +26 -0
  12. package/dist/runtime/components/Atom.ChatContact.vue +136 -0
  13. package/dist/runtime/components/Atom.ChatContactSchedule.vue +87 -0
  14. package/dist/runtime/components/Atom.ChatMessageFooter.vue +25 -0
  15. package/dist/runtime/components/Atom.DarkMode.vue +67 -0
  16. package/dist/runtime/components/Atom.DraggableWindow.vue +102 -0
  17. package/dist/runtime/components/Atom.Dropdown.vue +9 -0
  18. package/dist/runtime/components/Atom.DropdownSearchable.vue +25 -0
  19. package/dist/runtime/components/Atom.Fetch.vue +46 -0
  20. package/dist/runtime/components/Atom.Field.vue +43 -0
  21. package/dist/runtime/components/Atom.FieldDate.vue +19 -0
  22. package/dist/runtime/components/Atom.FieldNumber.vue +19 -0
  23. package/dist/runtime/components/Atom.FieldPhone.vue +92 -0
  24. package/dist/runtime/components/Atom.FieldSelect.vue +28 -0
  25. package/dist/runtime/components/Atom.FieldSelectMultiple.vue +49 -0
  26. package/dist/runtime/components/Atom.FieldText.vue +19 -0
  27. package/dist/runtime/components/Atom.FieldTextarea.vue +41 -0
  28. package/dist/runtime/components/Atom.Loading.vue +80 -0
  29. package/dist/runtime/components/Atom.Notification.vue +48 -0
  30. package/dist/runtime/components/Atom.Ringtone.vue +23 -0
  31. package/dist/runtime/components/Atom.SelectTreeField.vue +49 -0
  32. package/dist/runtime/components/Atom.Snapshot.vue +33 -0
  33. package/dist/runtime/components/Atom.Tabs.vue +60 -0
  34. package/dist/runtime/components/Molecule.ChatMessageFile.vue +102 -0
  35. package/dist/runtime/components/Molecule.ChatMessageOption.vue +85 -0
  36. package/dist/runtime/components/Molecule.ChatMessageText.vue +36 -0
  37. package/dist/runtime/components/Molecule.ClientHistory.vue +62 -0
  38. package/dist/runtime/components/Molecule.DropdownDDI.vue +333 -0
  39. package/dist/runtime/components/Molecule.FieldGroup.vue +73 -0
  40. package/dist/runtime/components/Molecule.FieldSelectMultiple.vue +19 -0
  41. package/dist/runtime/components/Molecule.File.vue +84 -0
  42. package/dist/runtime/components/Molecule.SelectTreeSearchable.vue +126 -0
  43. package/dist/runtime/components/Molecule.Status.vue +154 -0
  44. package/dist/runtime/components/Molecule.TimeDaily.vue +9 -0
  45. package/dist/runtime/components/Organism.Attachments.vue +139 -0
  46. package/dist/runtime/components/Organism.ChatMessages.vue +31 -0
  47. package/dist/runtime/components/Organism.ChatRoom.vue +342 -0
  48. package/dist/runtime/components/Organism.ChatSchedule.vue +110 -0
  49. package/dist/runtime/components/Organism.ClientHistoryTable.vue +85 -0
  50. package/dist/runtime/components/Organism.ClientHistoryTimeline.vue +77 -0
  51. package/dist/runtime/components/Organism.FAQ.vue +88 -0
  52. package/dist/runtime/components/Organism.Form.vue +67 -0
  53. package/dist/runtime/components/Organism.FormMailing.vue +112 -0
  54. package/dist/runtime/components/Organism.HeaderMain.vue +79 -0
  55. package/dist/runtime/components/Organism.Manifestation.vue +146 -0
  56. package/dist/runtime/components/Organism.Nav.vue +27 -0
  57. package/dist/runtime/components/Organism.NavMain.vue +187 -0
  58. package/dist/runtime/components/Organism.PageContainer.vue +22 -0
  59. package/dist/runtime/components/Organism.Schedule.vue +170 -0
  60. package/dist/runtime/components/Organism.Tabulation.vue +237 -0
  61. package/dist/runtime/components/types/dto.d.ts +16 -0
  62. package/dist/runtime/components/types/dto.mjs +236 -0
  63. package/dist/runtime/components/types/helpers.d.ts +39 -0
  64. package/dist/runtime/components/types/helpers.mjs +295 -0
  65. package/dist/runtime/components/types/index.d.ts +4 -0
  66. package/dist/runtime/components/types/index.mjs +4 -0
  67. package/dist/runtime/components/types/types.d.ts +198 -0
  68. package/dist/runtime/components/types/types.mjs +35 -0
  69. package/dist/runtime/index.css +1 -0
  70. package/dist/runtime/plugins/clickOutside.d.ts +2 -0
  71. package/dist/runtime/plugins/clickOutside.mjs +16 -0
  72. package/dist/runtime/plugins/emitter.d.ts +2 -0
  73. package/dist/runtime/plugins/emitter.mjs +17 -0
  74. package/dist/runtime/public/192x192.png +0 -0
  75. package/dist/runtime/public/404.svg +1 -0
  76. package/dist/runtime/public/512x512.png +0 -0
  77. package/dist/runtime/public/chat.svg +138 -0
  78. package/dist/runtime/public/chatbg.png +0 -0
  79. package/dist/runtime/public/dev-sw.d.ts +0 -0
  80. package/dist/runtime/public/dev-sw.mjs +0 -0
  81. package/dist/runtime/public/empty.svg +1 -0
  82. package/dist/runtime/public/loading.svg +1 -0
  83. package/dist/runtime/public/messages.svg +1 -0
  84. package/dist/runtime/public/privacy.svg +1 -0
  85. package/dist/runtime/public/ringtone.mp3 +0 -0
  86. package/dist/runtime/public/security.svg +188 -0
  87. package/dist/runtime/public/snapshot.d.ts +15 -0
  88. package/dist/runtime/public/snapshot.mjs +77 -0
  89. package/dist/runtime/public/unauthorized.svg +1 -0
  90. package/dist/types.d.ts +10 -0
  91. package/package.json +50 -0
@@ -0,0 +1,102 @@
1
+ <template>
2
+ <div
3
+ v-if="visible"
4
+ class="draggable absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col overflow-hidden scrollbar z-50 shadow-lg rounded-md !bg-white dark:!bg-slate-900 chatbg"
5
+ >
6
+ <header class="dragzone flex flex-col sticky top-0 z-10 bg-slate-700">
7
+ <ul class="tabs pl-2 mt-3 whitespace-nowrap">
8
+ <li class="tab active bg-white dark:bg-slate-800 normal-case">
9
+ Histórico de Conversas
10
+ </li>
11
+ <li class="tab flex-1 justify-end">
12
+ <i
13
+ class="bi-x-lg text-error text-sm leading-3 px-2 pb-2"
14
+ @click="hide"
15
+ />
16
+ </li>
17
+ </ul>
18
+ </header>
19
+ <article
20
+ class="p-2 h-full overflow-auto scrollbar z-10 border dark:border-slate-800"
21
+ >
22
+ <div class="flex justify-center">
23
+ <span
24
+ class="text-center text- my-3 badge badge-outline-secondary w-fit bg-white dark:bg-slate-900"
25
+ >{{ title }}</span>
26
+ </div>
27
+ <slot />
28
+ </article>
29
+ <footer class="dragzone cursor-pointer w-full h-12 bg-slate-700 z-10" />
30
+ </div>
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ import { ref } from "vue";
35
+
36
+ // data
37
+ const visible = ref<boolean>(false);
38
+ const title = ref<string>("");
39
+
40
+ // methods
41
+ function show(text: string = "") {
42
+ visible.value = true;
43
+ title.value = text;
44
+
45
+ setTimeout(() => {
46
+ const draggable = document.querySelector(".draggable");
47
+ for (const dragzone of document.querySelectorAll(".dragzone")) {
48
+ dragElement(draggable, dragzone);
49
+ }
50
+ }, 500);
51
+ }
52
+ function hide() {
53
+ visible.value = false;
54
+ }
55
+
56
+ const dragElement = (element: any, dragzone: any) => {
57
+ let pos1 = 0,
58
+ pos2 = 0,
59
+ pos3 = 0,
60
+ pos4 = 0;
61
+ const dragMouseUp = () => {
62
+ document.onmouseup = null;
63
+ document.onmousemove = null;
64
+ };
65
+
66
+ const dragMouseMove = (event: any) => {
67
+ event.preventDefault();
68
+ pos1 = pos3 - event.clientX;
69
+ pos2 = pos4 - event.clientY;
70
+ pos3 = event.clientX;
71
+ pos4 = event.clientY;
72
+ element.style.top = `${element.offsetTop - pos2}px`;
73
+ element.style.left = `${element.offsetLeft - pos1}px`;
74
+ };
75
+
76
+ const dragMouseDown = (event: any) => {
77
+ event.preventDefault();
78
+
79
+ pos3 = event.clientX;
80
+ pos4 = event.clientY;
81
+
82
+ document.onmouseup = dragMouseUp;
83
+ document.onmousemove = dragMouseMove;
84
+ };
85
+
86
+ dragzone.onmousedown = dragMouseDown;
87
+ };
88
+
89
+ // expose
90
+ defineExpose({ show, hide });
91
+ </script>
92
+
93
+ <style scoped>
94
+ .chatbg::after {
95
+ content: "";
96
+ width: 100%;
97
+ height: 100%;
98
+ position: absolute;
99
+ background: url(../public/chatbg.png);
100
+ background-size: cover;
101
+ }
102
+ </style>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <ul class="dropdown text-black">
3
+ <slot />
4
+ </ul>
5
+ </template>
6
+
7
+ <script setup lang="ts"></script>
8
+
9
+ <style scoped></style>
@@ -0,0 +1,25 @@
1
+ <template>
2
+ <ul class="dropdown">
3
+ <form class="field-group mb-2">
4
+ <input
5
+ :value="modelValue"
6
+ type="search"
7
+ placeholder="Pesquise"
8
+ class="input bg-slate-100 dark:bg-slate-700"
9
+ @input=" $emit('update:modelValue', ($event.target as HTMLInputElement).value)"
10
+ >
11
+ </form>
12
+ <slot />
13
+ </ul>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ interface IProps {
18
+ modelValue?: string;
19
+ }
20
+ defineProps<IProps>();
21
+
22
+ defineEmits(["update:modelValue"]);
23
+ </script>
24
+
25
+ <style scoped></style>
@@ -0,0 +1,46 @@
1
+ <template>
2
+ <slot />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { useFetch, UseFetchOptions, useNuxtApp } from "#app"
7
+
8
+ // props
9
+ interface IProps {
10
+ url: string
11
+ success: Function
12
+ options?: UseFetchOptions<any>
13
+ error?: Function
14
+ finally?: Function
15
+ }
16
+ const props = defineProps<IProps>()
17
+
18
+ // app
19
+ const { $emit } = useNuxtApp()
20
+
21
+ await new Promise(resolve=>setTimeout(resolve, 2000))
22
+
23
+ async function load(): Promise<void> {
24
+ const { data, error, refresh } = await useFetch(props.url, props.options)
25
+
26
+ if (error.value && props.error) {
27
+ if (error.value.statusCode === 401) {
28
+ $emit("auth:logout")
29
+ return
30
+ }
31
+
32
+ props.error({ error: error.value, refresh })
33
+ return
34
+ }
35
+
36
+ props.success(data.value)
37
+
38
+ if (props.finally) {
39
+ props.finally({ data: data.value, error: error.value, refresh })
40
+ }
41
+ }
42
+
43
+ await load()
44
+ </script>
45
+
46
+ <style scoped></style>
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <component
3
+ :is="getComponentByType()"
4
+ :field="field"
5
+ />
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import { resolveComponent } from "vue";
10
+ import { EFieldTypes, IField } from "./types";
11
+
12
+ // props
13
+ interface IProps {
14
+ type?: EFieldTypes;
15
+ field?: IField
16
+ }
17
+ const props = defineProps<IProps>();
18
+
19
+ // method
20
+ function getComponentByType() {
21
+ if (props.type === EFieldTypes.date) {
22
+ return resolveComponent("AtomFieldDate");
23
+ }
24
+ if (props.type === EFieldTypes.text) {
25
+ return resolveComponent("AtomFieldText");
26
+ }
27
+ if (props.type === EFieldTypes.phone) {
28
+ return resolveComponent("AtomFieldPhone");
29
+ }
30
+ if (props.type === EFieldTypes.number) {
31
+ return resolveComponent("AtomFieldNumber");
32
+ }
33
+ if (props.type === EFieldTypes.textarea) {
34
+ return resolveComponent("AtomFieldTextarea");
35
+ }
36
+ if (props.type === EFieldTypes.select) {
37
+ return resolveComponent("AtomFieldSelect");
38
+ }
39
+ return resolveComponent("AtomFieldText");
40
+ }
41
+ </script>
42
+
43
+ <style scoped></style>
@@ -0,0 +1,19 @@
1
+ <template>
2
+ <input
3
+ :value="modelValue"
4
+ type="date"
5
+ class="input w-1"
6
+ @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
7
+ >
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ interface IProps {
12
+ modelValue?: string;
13
+ }
14
+ defineProps<IProps>();
15
+
16
+ defineEmits(["update:modelValue"]);
17
+ </script>
18
+
19
+ <style scoped></style>
@@ -0,0 +1,19 @@
1
+ <template>
2
+ <input
3
+ :value="modelValue"
4
+ type="number w-1"
5
+ class="input"
6
+ @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
7
+ >
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ interface IProps {
12
+ modelValue?: string;
13
+ }
14
+ defineProps<IProps>();
15
+
16
+ defineEmits(["update:modelValue"]);
17
+ </script>
18
+
19
+ <style scoped></style>
@@ -0,0 +1,92 @@
1
+ <template>
2
+ <div class="input-group">
3
+ <MoleculeDropdownDDI
4
+ ref="MoleculeDropdownDDIRef"
5
+ v-model="ddi"
6
+ class="addon"
7
+ :disabled="field?.disabled || loading"
8
+ />
9
+ <input
10
+ v-model="phone"
11
+ :disabled="field?.disabled || loading"
12
+ type="tel"
13
+ class="input w-1"
14
+ @blur="verifyNumberHasDDI"
15
+ >
16
+ </div>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import { ref, reactive, onMounted, watch } from "vue";
21
+ import { getDDI, IField, IDDI, getDefaultDDI, sleep } from "./types";
22
+ import { useNuxtApp } from "#app";
23
+ import MoleculeDropdownDDI from "./Molecule.DropdownDDI.vue";
24
+
25
+ // props
26
+ interface IProps {
27
+ modelValue?: any;
28
+ field?: IField;
29
+ }
30
+ const props = defineProps<IProps>();
31
+
32
+ // mounted
33
+ onMounted(setByNumber);
34
+
35
+ // data
36
+ const { $listen } = useNuxtApp();
37
+ const MoleculeDropdownDDIRef = ref<InstanceType<
38
+ typeof MoleculeDropdownDDI
39
+ > | null>(null);
40
+ const loading = ref<boolean>(false);
41
+ const ddi = reactive<IDDI>(getDefaultDDI());
42
+ const phone = ref<string>(props.modelValue);
43
+
44
+ // methods
45
+ async function setByNumber() {
46
+ if (!/^[+][0-9]{9,}$/.test(phone.value)) return;
47
+
48
+ try {
49
+ loading.value = true;
50
+ const { DDI, national_number } = await getDDI(phone.value);
51
+
52
+ MoleculeDropdownDDIRef.value?.set(DDI);
53
+ phone.value = national_number;
54
+ updatePhone();
55
+ } finally {
56
+ loading.value = false;
57
+ }
58
+ }
59
+ function updatePhone() {
60
+ emit("update:modelValue", getPhoneWithDDI());
61
+ }
62
+ function getPhoneWithDDI() {
63
+ return `${ddi.ddi}${phone.value}`;
64
+ }
65
+ async function verifyNumberHasDDI() {
66
+ if(!phone.value) return
67
+
68
+ if (phone.value[0]?.includes("+")) {
69
+ await setByNumber();
70
+ }
71
+
72
+ updatePhone();
73
+ }
74
+
75
+ watch(() => ddi.ddi, updatePhone);
76
+
77
+ // emits
78
+ const emit = defineEmits(["update:modelValue"]);
79
+ $listen("form:append", async () => {
80
+ await sleep(0);
81
+ phone.value = props.modelValue;
82
+ verifyNumberHasDDI();
83
+ });
84
+ </script>
85
+
86
+ <style scoped>
87
+ .input-phone {
88
+ width: 100%;
89
+ height: 100%;
90
+ outline: none;
91
+ }
92
+ </style>
@@ -0,0 +1,28 @@
1
+ <template>
2
+ <select
3
+ :value="modelValue"
4
+ class="input w-1"
5
+ @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
6
+ >
7
+ <option
8
+ v-for="({ label, value }, i) in field?.options"
9
+ :key="i"
10
+ :value="value"
11
+ v-text="label"
12
+ />
13
+ </select>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import type { IField } from "./types";
18
+
19
+ interface IProps {
20
+ field: IField;
21
+ modelValue?: string;
22
+ }
23
+
24
+ defineProps<IProps>();
25
+
26
+ defineEmits(["update:modelValue"]);
27
+ </script>
28
+ <style scoped></style>
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <label class="input max-h-none flex cursor-pointer justify-between">
3
+ <span
4
+ v-if="optionsIsEmpty"
5
+ class="pl-2"
6
+ >Selecione</span>
7
+
8
+ <div class="flex flex-wrap">
9
+ <span
10
+ v-for="(option, i) in options"
11
+ :key="i"
12
+ class="badge badge-soft-secondary mx-1 my-1 text-sm leading-3"
13
+ @click="del(option)"
14
+ >
15
+ <span v-text="option.label" />
16
+ <i class="bi-x-circle-fill text-red-500 ml-2" />
17
+ </span>
18
+ </div>
19
+
20
+ <i class="bi-chevron-down ml-4" />
21
+ </label>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ import { computed } from "vue";
26
+ import { IFieldSelectOption, IFieldSelectOptions } from "./types";
27
+
28
+ // props
29
+ interface IProps {
30
+ options: IFieldSelectOptions;
31
+ }
32
+ const props = defineProps<IProps>();
33
+
34
+ // emits
35
+ interface IEmits {
36
+ (e: "delete", option: IFieldSelectOption): void;
37
+ }
38
+ const emit = defineEmits<IEmits>();
39
+
40
+ // computed
41
+ const optionsIsEmpty = computed<boolean>(() => !props.options.length);
42
+
43
+ // methods
44
+ function del(option: IFieldSelectOption): void {
45
+ emit("delete", option);
46
+ }
47
+ </script>
48
+
49
+ <style scoped></style>
@@ -0,0 +1,19 @@
1
+ <template>
2
+ <input
3
+ :value="modelValue"
4
+ type="text"
5
+ class="input w-1"
6
+ @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
7
+ >
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ interface IProps {
12
+ modelValue?: string;
13
+ }
14
+ defineProps<IProps>();
15
+
16
+ defineEmits(["update:modelValue"]);
17
+ </script>
18
+
19
+ <style scoped></style>
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <div class="input-group">
3
+ <textarea
4
+ :value="modelValue"
5
+ class="input rounded-tr-md rounded-br-md w-1"
6
+ @input="
7
+ $emit('update:modelValue', ($event.target as HTMLInputElement).value)
8
+ "
9
+ />
10
+
11
+ <!-- maxlength -->
12
+ <span
13
+ v-if="field?.maxlength"
14
+ class="absolute top-0 right-0 text-[11px] text-secondary"
15
+ >
16
+ {{ counter({ maxlength: field.maxlength, value: modelValue }) }}
17
+ </span>
18
+ </div>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import { computed } from "vue";
23
+ import type { IField } from "./types";
24
+
25
+ interface IProps {
26
+ modelValue?: string;
27
+ field?: IField;
28
+ }
29
+
30
+ defineProps<IProps>();
31
+
32
+ // computed
33
+ const counter = computed<Function>(() => ({ value, maxlength }: any) => {
34
+ return `${value?.length ?? 0}/${maxlength}`;
35
+ });
36
+
37
+ // emits
38
+ defineEmits(["update:modelValue"]);
39
+ </script>
40
+
41
+ <style></style>
@@ -0,0 +1,80 @@
1
+ <template>
2
+ <section class="absolute left-0 top-0 w-full h-screen z-20 overflow-hidden">
3
+ <header
4
+ class="absolute flex items-center w-full h-16 bg-slate-800 z-10 pointer-events-none"
5
+ >
6
+ <img
7
+ src="../public/192x192.png"
8
+ class="w-8 min-w-2rem m-4"
9
+ alt="ContactStudio Logo"
10
+ >
11
+
12
+ <div>
13
+ <div class="w-96 h-3 bg-slate-700 rounded-full animate-pulse" />
14
+ <div
15
+ class="w-36 h-2 bg-slate-700 mt-2 rounded-full animate-pulse"
16
+ />
17
+ </div>
18
+ </header>
19
+ <nav
20
+ class="sidenav flex flex-col justify-between !transition-none absolute w-[60px] h-full pt-[72px] max-sm:hidden pointer-events-none"
21
+ >
22
+ <article>
23
+ <ul class="list sidenav-item-full">
24
+ <li class="item sidenav-item h-9">
25
+ <i
26
+ class="bi-exclamation-triangle-fill leading-none text-base text-warn -ml-1"
27
+ />
28
+ </li>
29
+ </ul>
30
+ <div class="divider" />
31
+ <ul
32
+ v-for="i in 3"
33
+ :key="i"
34
+ class="list sidenav-item-full mb-2"
35
+ >
36
+ <li
37
+ class="item sidenav-item bg-slate-200 dark:bg-slate-800 h-9 animate-pulse"
38
+ />
39
+ </ul>
40
+ <div class="divider" />
41
+ <ul class="list sidenav-item-full mb-2">
42
+ <li
43
+ class="item sidenav-item bg-slate-200 dark:bg-slate-800 h-9 animate-pulse"
44
+ />
45
+ </ul>
46
+ </article>
47
+ <aside>
48
+ <ul class="list sidenav-item-full mb-2">
49
+ <li
50
+ class="item sidenav-item bg-slate-200 dark:bg-slate-800 h-9 animate-pulse"
51
+ />
52
+ </ul>
53
+ <div class="divider" />
54
+ <ul class="list sidenav-item-full mb-2">
55
+ <li class="item sidenav-item">
56
+ <i
57
+ class="bi-box-arrow-in-left text-lg leading-none mr-5 text-error"
58
+ />
59
+ </li>
60
+ </ul>
61
+ </aside>
62
+ </nav>
63
+ <article class="absolute w-full h-screen p-24 max-sm:px-10 pointer-events-none">
64
+ <aside class="bg-slate-200 dark:bg-slate-800 w-full h-24 rounded-lg animate-pulse" />
65
+
66
+ <div
67
+ class="mt-8 flex max-sm:flex-wrap w-full gap-8"
68
+ style="height: calc(100vh - 300px);"
69
+ >
70
+ <aside class="bg-slate-200 dark:bg-slate-800 w-full sm:w-1/4 h-1/4 rounded-lg animate-pulse" />
71
+ <aside class="bg-slate-200 dark:bg-slate-800 w-full sm:w-2/4 h-1/2 rounded-lg animate-pulse" />
72
+ <aside class="bg-slate-200 dark:bg-slate-800 w-full sm:w-1/4 h-1/3 rounded-lg animate-pulse" />
73
+ </div>
74
+ </article>
75
+ </section>
76
+ </template>
77
+
78
+ <script setup lang="ts"></script>
79
+
80
+ <style scoped></style>
@@ -0,0 +1,48 @@
1
+ <template>
2
+ <div />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { onMounted, ref } from "vue";
7
+ import { useNuxtApp, useRuntimeConfig } from "#app";
8
+
9
+ // data
10
+ const { BASE_URL } = useRuntimeConfig().public;
11
+ const { $listen } = useNuxtApp();
12
+
13
+ // mounted
14
+ onMounted(async () => {
15
+ const filename = process.dev ? "dev-sw.js" : "sw.js";
16
+ sw.value = await navigator.serviceWorker.register(`${BASE_URL}/${filename}`);
17
+ });
18
+
19
+ // data
20
+ const sw = ref<ServiceWorkerRegistration | null>(null);
21
+
22
+ // methods
23
+ async function show({ title, message, image }: any): Promise<void> {
24
+ if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
25
+ console.error("Browser does not support notifications");
26
+ return;
27
+ }
28
+
29
+ if (!sw.value) return;
30
+
31
+ const permission = await Notification.requestPermission();
32
+
33
+ if (permission !== "granted") return;
34
+
35
+ sw.value.showNotification(title, {
36
+ body: message,
37
+ icon: image ?? `${BASE_URL}/192x192.png`,
38
+ vibrate: [200, 100, 200],
39
+ });
40
+ }
41
+
42
+ // emits
43
+ $listen("notification:show", ({ title, message, image }: any) => {
44
+ show({ title, message, image });
45
+ });
46
+ </script>
47
+
48
+ <style scoped></style>
@@ -0,0 +1,23 @@
1
+ <template>
2
+ <div />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import sound from "../public/ringtone.mp3";
7
+ import { useNuxtApp } from "#app";
8
+
9
+ const { $listen } = useNuxtApp();
10
+
11
+ $listen("ringtone:play", (tone?: string) => {
12
+ const audio = new Audio(tone ?? sound);
13
+ audio.oncanplaythrough = () => {
14
+ audio.play().catch(() => {
15
+ window.addEventListener("click", () => {
16
+ audio.play();
17
+ }, { once: true });
18
+ });
19
+ };
20
+ });
21
+ </script>
22
+
23
+ <style scoped></style>