Skip to content

Commit 432e812

Browse files
Add emails page in admin panel
1 parent 4f0ae29 commit 432e812

File tree

8 files changed

+432
-3
lines changed

8 files changed

+432
-3
lines changed

frontend/kubecloud-v2/assets/scss/global.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*,
1+
*:not([class*="ql-"]),
22
body {
33
font-family:
44
Inter,
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
<template>
2+
<div @click="stop()">
3+
<v-card
4+
class="md-editor bg-2 elevation-0 rounded-lg"
5+
:style="{ padding: '0 !important' }"
6+
:class="{ 'md-editor--focused': focused }"
7+
>
8+
<v-card-text class="md-editor-toolbar border-b bg-2">
9+
<div ref="toolbar" class="d-flex align-center">
10+
<span class="ql-formats d-flex align-center flex-wrap ga-4">
11+
<v-select
12+
v-model="font"
13+
:items="[
14+
{ text: 'Serif', value: 'serif' },
15+
{ text: 'Monospace', value: 'monospace' },
16+
]"
17+
item-title="text"
18+
item-value="value"
19+
density="compact"
20+
variant="solo"
21+
width="155px"
22+
max-width="155px"
23+
flat
24+
hide-details
25+
tabindex="-1"
26+
>
27+
<template #item="{ item, props }">
28+
<v-list-item v-bind="props" :title="undefined">
29+
<span :style="{ fontFamily: item.value + ' !important' }" v-text="item.title" />
30+
</v-list-item>
31+
</template>
32+
</v-select>
33+
34+
<v-select
35+
v-model="size"
36+
:items="[
37+
{ text: 'Small', value: '0.75em' },
38+
{ text: 'Normal', value: '1em' },
39+
{ text: 'Large', value: '1.5em' },
40+
{ text: 'Huge', value: '2em' },
41+
]"
42+
return-object
43+
item-title="text"
44+
density="compact"
45+
variant="solo"
46+
width="155px"
47+
max-width="155px"
48+
flat
49+
hide-details
50+
tabindex="-1"
51+
>
52+
<template #item="{ item, props }">
53+
<v-list-item v-bind="props" :title="undefined">
54+
<span :style="{ fontSize: item.value + ' !important' }" v-text="item.title" />
55+
</v-list-item>
56+
</template>
57+
</v-select>
58+
59+
<v-btn
60+
v-for="{ format, icon, action } in formats"
61+
:key="format"
62+
:class="{ 'px-0': true, ['ql-' + format]: !action }"
63+
:style="{ minWidth: 'auto !important' }"
64+
variant="plain"
65+
size="small"
66+
:ripple="false"
67+
tabindex="-1"
68+
@click="action"
69+
>
70+
<v-icon :icon="'mdi-' + icon" size="large" />
71+
</v-btn>
72+
</span>
73+
</div>
74+
</v-card-text>
75+
76+
<v-card-text class="overflow-auto" :style="{ height: '180px' }">
77+
<div class="position-relative h-100">
78+
<div ref="editor" class="h-100" :style="{ zIndex: 2 }" />
79+
<p
80+
v-if="text.length === 0"
81+
class="md-editor-placeholder position-absolute top-0 left-0 opacity-30"
82+
:style="{ zIndex: 1 }"
83+
v-text="label"
84+
/>
85+
</div>
86+
</v-card-text>
87+
</v-card>
88+
89+
<v-dialog :model-value="isRevealed" max-width="500" scrollable @update:model-value="cancel()">
90+
<v-form @submit.prevent="confirm($event.target)">
91+
<v-card :style="{ padding: '0 !important' }">
92+
<v-card-title class="px-6 py-4">
93+
<div class="d-flex align-center justify-space-between">
94+
<h3 class="text-h5 font-weight-bold">Add Link</h3>
95+
</div>
96+
</v-card-title>
97+
98+
<v-divider />
99+
100+
<v-card-text>
101+
<v-text-field
102+
name="link"
103+
placeholder="Enter the link"
104+
variant="outlined"
105+
prepend-inner-icon="mdi-link"
106+
hide-details
107+
autofocus
108+
/>
109+
</v-card-text>
110+
<v-divider />
111+
112+
<v-card-actions class="px-6 py-4 flex-row-reverse justify-start">
113+
<v-btn variant="text" color="primary" type="submit">Add</v-btn>
114+
<v-btn variant="plain" type="button" @click="cancel()">Cancel</v-btn>
115+
</v-card-actions>
116+
</v-card>
117+
</v-form>
118+
</v-dialog>
119+
</div>
120+
</template>
121+
122+
<script setup lang="ts">
123+
import Quill from "quill"
124+
import "quill/dist/quill.core.css"
125+
126+
defineProps<{ label: string }>()
127+
128+
const toolbar = ref<HTMLDivElement | null>(null)
129+
const editor = ref<HTMLDivElement | null>(null)
130+
const focused = ref(false)
131+
const text = ref("")
132+
133+
let quill: Quill | null = null
134+
135+
const { isRevealed, reveal, cancel, confirm } = useDialog<HTMLFormElement>()
136+
const formats = markRaw([
137+
{ format: "bold", icon: "format-bold" },
138+
{ format: "italic", icon: "format-italic" },
139+
{ format: "underline", icon: "format-underline" },
140+
{ format: "list", icon: "format-list-bulleted" },
141+
{
142+
format: "link",
143+
icon: "link",
144+
async action() {
145+
const { data: form, isCanceled } = await reveal()
146+
if (!isCanceled && form) {
147+
const data = new FormData(form)
148+
const link = data.get("link") as string
149+
quill?.format("link", link)
150+
}
151+
},
152+
},
153+
{ format: "strike", icon: "format-strikethrough-variant" },
154+
{ format: "blockquote", icon: "format-quote-close" },
155+
])
156+
157+
const font = ref("sans")
158+
watchImmediate(font, (f) => quill?.format("font", f))
159+
160+
const size = ref({ text: "Normal", value: "1em" })
161+
watchImmediate(size, (v) => {
162+
const s = v.text.toLowerCase()
163+
if (s === "normal") {
164+
return quill?.format("size", false)
165+
}
166+
quill?.format("size", s)
167+
})
168+
169+
const { start, stop } = useTimeoutFn(() => (focused.value = false), 150)
170+
171+
onMounted(() => {
172+
quill = new Quill(editor.value!, {
173+
modules: {
174+
toolbar: toolbar.value!,
175+
},
176+
})
177+
178+
quill.editor.scroll.domNode.setAttribute("spellcheck", "false")
179+
quill.editor.scroll.domNode.onfocus = () => (focused.value = true)
180+
quill.editor.scroll.domNode.onblur = start
181+
quill.on("text-change", () => {
182+
const md = quill!.getSemanticHTML()
183+
text.value = md === "<p></p>" ? "" : quill!.getSemanticHTML()
184+
})
185+
})
186+
187+
onUnmounted(() => {
188+
if (quill) {
189+
quill.off("text-change")
190+
quill.editor.scroll.domNode.onfocus = null
191+
quill.editor.scroll.domNode.onblur = null
192+
quill.disable()
193+
quill = null
194+
}
195+
})
196+
</script>
197+
198+
<style lang="scss">
199+
.md-editor {
200+
box-sizing: border-box !important;
201+
202+
&,
203+
.md-editor-toolbar {
204+
transition-duration: 0.28s !important;
205+
transition-property: border-color, box-shadow, opacity, background !important;
206+
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1) !important;
207+
}
208+
209+
.ql-editor {
210+
height: 100% !important;
211+
outline: none !important;
212+
border: none !important;
213+
padding: 0 !important;
214+
margin: 0 !important;
215+
color: rgba(255, 255, 255, 0.5) !important;
216+
}
217+
218+
&.md-editor--focused,
219+
&:focus,
220+
&:hover {
221+
&,
222+
.md-editor-toolbar {
223+
border-color: rgba(255, 255, 255, 0.5) !important;
224+
}
225+
}
226+
227+
&.md-editor--focused {
228+
border-width: 2px !important;
229+
230+
.md-editor-toolbar {
231+
border-width: 2px !important;
232+
}
233+
234+
.md-editor-placeholder,
235+
.md-editor-toolbar {
236+
margin-top: -1px !important;
237+
margin-left: -1px !important;
238+
}
239+
}
240+
}
241+
</style>

frontend/kubecloud-v2/composables/dialog.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,42 @@ export const useDialog = <T>() => {
1010
...reset,
1111
}
1212
}
13+
14+
export const useFilesDialog = (options?: Parameters<typeof useFileDialog>[0]) => {
15+
const { onChange, onCancel, open, reset } = useFileDialog(options)
16+
const files = ref<File[]>([])
17+
18+
onChange((newFiles) => {
19+
if (newFiles) {
20+
const fs = [...files.value]
21+
outer: for (const newFile of newFiles) {
22+
for (const file of fs) {
23+
if (
24+
file.name === newFile.name &&
25+
file.size === newFile.size &&
26+
file.lastModified === newFile.lastModified
27+
) {
28+
continue outer
29+
}
30+
}
31+
32+
fs.push(newFile)
33+
}
34+
35+
files.value = fs
36+
}
37+
})
38+
39+
function removeFile(file: File) {
40+
files.value = files.value.filter((f) => f !== file)
41+
}
42+
43+
return {
44+
onChange,
45+
onCancel,
46+
files,
47+
open,
48+
reset,
49+
removeFile,
50+
}
51+
}

frontend/kubecloud-v2/composables/stats.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,5 +109,5 @@ export const useStats = (options?: UseStatsOptions) => {
109109
return resources
110110
})
111111

112-
return { isLoading, stats }
112+
return { isLoading, stats, data: state }
113113
}

frontend/kubecloud-v2/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"izitoast": "^1.4.0",
2626
"nuxt": "^4.2.2",
2727
"nuxt-toast": "^1.4.0",
28+
"quill": "^2.0.3",
2829
"validator": "^13.15.26",
2930
"vue": "^3.5.25",
3031
"vue-router": "^4.6.3",

0 commit comments

Comments
 (0)