init
This commit is contained in:
101
app/components/MaterialsItem.vue
Normal file
101
app/components/MaterialsItem.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<script lang="ts" setup="">
|
||||
import dayjs from "dayjs";
|
||||
import ru from 'dayjs/locale/ru'
|
||||
import type {Material} from "~/stores/materials/types.materials";
|
||||
import IconCalendar from "~/components/icons/iconCalendar.vue";
|
||||
|
||||
dayjs.locale(ru);
|
||||
interface Props {
|
||||
item: Material
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const date = computed(() => dayjs(props.item.datetime).format('D MMMM'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="item-container">
|
||||
<div class="date-container">
|
||||
<IconCalendar class="icon" />
|
||||
<span class="date">{{date}}</span>
|
||||
</div>
|
||||
|
||||
<div class="item-content" >
|
||||
<h2 class="title">{{item.title}}</h2>
|
||||
<p class="description">{{item.short_description}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.item-container {
|
||||
border-radius: 24px;
|
||||
padding: 32px 24px;
|
||||
transition: all .3s ease;
|
||||
box-shadow: unset;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
@include mixins.square(276px);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
box-shadow: -2px 5px 13px 0px #0000000A, -9px 21px 23px 0px #0000000A, -21px 48px 31px 0px #00000005, -37px 85px 37px 0px #00000003, -57px 133px 40px 0px #00000000;
|
||||
}
|
||||
}
|
||||
|
||||
.date-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: var(--text-light-gray);
|
||||
}
|
||||
.icon {
|
||||
@include mixins.square(20px);
|
||||
}
|
||||
|
||||
.date {
|
||||
font-family: FuturaPT, monospace;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
.title, .description {
|
||||
padding-top: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: SourceSans3, monospace;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
line-height: 120%;
|
||||
color: var(--text-black)
|
||||
}
|
||||
|
||||
.description {
|
||||
font-family: FuturaPT, monospace;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 120%;
|
||||
color: var(--text-gray);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.item-container {
|
||||
max-width: unset;
|
||||
min-width: unset;
|
||||
width: unset;
|
||||
height: unset;
|
||||
min-height: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
364
app/components/TiptapEditor.vue
Normal file
364
app/components/TiptapEditor.vue
Normal file
@@ -0,0 +1,364 @@
|
||||
<template>
|
||||
<div v-if="editor" class="container tiptap">
|
||||
<div class="control-group">
|
||||
<div class="button-group">
|
||||
<button
|
||||
:class="{ 'is-active': editor.isActive('paragraph') }"
|
||||
@click="editor.chain().focus().setParagraph().run()"
|
||||
>
|
||||
Paragraph
|
||||
</button>
|
||||
<button
|
||||
:disabled="hasH1"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
|
||||
@click="toggleH1"
|
||||
>
|
||||
H1
|
||||
</button>
|
||||
<button
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
|
||||
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
|
||||
>
|
||||
H2
|
||||
</button>
|
||||
<button
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
|
||||
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
|
||||
>
|
||||
H3
|
||||
</button>
|
||||
<button
|
||||
:class="{ 'is-active': editor.isActive('blockquote') }"
|
||||
@click="editor.chain().focus().toggleBlockquote().run()"
|
||||
>
|
||||
Blockquote
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<EditorContent :editor="editor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, computed, inject, ref, Ref } from 'vue'
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { ListItem } from '@tiptap/extension-list'
|
||||
import { Placeholder } from '@tiptap/extensions'
|
||||
import { Color, TextStyle } from '@tiptap/extension-text-style'
|
||||
import { EDITOR_KEY } from '~/shared/constants'
|
||||
|
||||
const TITLE_MAX = 80
|
||||
const DESCRIPTION_MAX = 255
|
||||
|
||||
const editor = inject<Ref<Editor | null>>(EDITOR_KEY)
|
||||
|
||||
const titleLength = ref(0)
|
||||
const descriptionLength = ref(0)
|
||||
|
||||
const hasH1 = computed(() => {
|
||||
if (!editor?.value) return false
|
||||
let found = false
|
||||
editor.value.state.doc.descendants((node) => {
|
||||
if (node.type.name === 'heading' && node.attrs.level === 1) {
|
||||
found = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return found
|
||||
})
|
||||
|
||||
// Получить текст H1
|
||||
function getH1Text(): string {
|
||||
if (!editor?.value) return ''
|
||||
let h1Text = ''
|
||||
editor.value.state.doc.descendants((node) => {
|
||||
if (node.type.name === 'heading' && node.attrs.level === 1) {
|
||||
h1Text = node.textContent
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return h1Text
|
||||
}
|
||||
|
||||
// Получить текст первого P
|
||||
function getFirstPText(): string {
|
||||
if (!editor?.value) return ''
|
||||
let firstPText = ''
|
||||
let foundP = false
|
||||
editor.value.state.doc.descendants((node) => {
|
||||
if (node.type.name === 'paragraph' && !foundP) {
|
||||
firstPText = node.textContent
|
||||
foundP = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return firstPText
|
||||
}
|
||||
|
||||
// Обновить счетчики
|
||||
function updateCharCount() {
|
||||
titleLength.value = getH1Text().length
|
||||
descriptionLength.value = getFirstPText().length
|
||||
}
|
||||
|
||||
// Функция для переключения H1
|
||||
function toggleH1() {
|
||||
if (!editor?.value) return
|
||||
|
||||
if (!hasH1.value) {
|
||||
editor.value.chain().focus().toggleHeading({ level: 1 }).run()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Получить валидные данные
|
||||
function getValidatedContent() {
|
||||
const titleText = getH1Text()
|
||||
const descriptionText = getFirstPText()
|
||||
|
||||
return {
|
||||
title: titleText,
|
||||
short_description: descriptionText,
|
||||
content: editor?.value?.getJSON()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getValidatedContent
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!editor) return
|
||||
|
||||
editor.value = new Editor({
|
||||
extensions: [
|
||||
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
||||
TextStyle.configure({ types: [ListItem.name] }),
|
||||
StarterKit,
|
||||
Placeholder.configure({
|
||||
emptyNodeClass: 'my-custom-is-empty-class',
|
||||
showOnlyWhenEditable: false,
|
||||
showOnlyCurrent: false,
|
||||
placeholder: ({ node, editor, pos }) => {
|
||||
if (node.type.name === 'heading') {
|
||||
return 'Введите заголовок'
|
||||
}
|
||||
if (node.type.name === 'paragraph') {
|
||||
let paragraphIndex = 0
|
||||
let foundPos = null
|
||||
|
||||
editor.state.doc.descendants((child, childPos) => {
|
||||
if (child.type.name === 'paragraph') {
|
||||
if (childPos === pos) {
|
||||
foundPos = paragraphIndex
|
||||
return false
|
||||
}
|
||||
paragraphIndex++
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if (foundPos === 0) {
|
||||
return 'Введите краткое описание...'
|
||||
}
|
||||
return 'Введите текст...'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
<h1></h1>
|
||||
<p></p>
|
||||
<p></p>
|
||||
`,
|
||||
onUpdate: ({ editor }) => {
|
||||
updateCharCount()
|
||||
|
||||
let paragraphIndex = 0
|
||||
let h1Found = false
|
||||
|
||||
editor.state.doc.descendants((node, pos) => {
|
||||
// Ограничиваем H1
|
||||
if (node.type.name === 'heading' && node.attrs.level === 1) {
|
||||
const text = node.textContent
|
||||
if (text.length > TITLE_MAX) {
|
||||
editor.commands.setTextSelection({
|
||||
from: pos + TITLE_MAX + 1,
|
||||
to: pos + text.length + 1
|
||||
})
|
||||
editor.commands.deleteSelection()
|
||||
}
|
||||
h1Found = true
|
||||
return
|
||||
}
|
||||
|
||||
// Ограничиваем только первый P
|
||||
if (node.type.name === 'paragraph') {
|
||||
if (paragraphIndex === 0) {
|
||||
const text = node.textContent
|
||||
if (text.length > DESCRIPTION_MAX) {
|
||||
editor.commands.setTextSelection({
|
||||
from: pos + DESCRIPTION_MAX + 1,
|
||||
to: pos + text.length + 1
|
||||
})
|
||||
editor.commands.deleteSelection()
|
||||
}
|
||||
}
|
||||
paragraphIndex++
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
updateCharCount()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (!editor?.value) return
|
||||
editor.value?.destroy()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
border-radius: 32px;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||
|
||||
li p {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
margin-top: 2.5rem;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin-top: 3.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: SourceSans3, monospace;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
|
||||
code {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
color: var(--black);
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25em 0.3em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--black);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--white);
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid var(--gray-3);
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--gray-2);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.tiptap p.is-empty::before {
|
||||
color: #adb5bd;
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--gray-3, #ddd);
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--gray-1, #f5f5f5);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: var(--blue-light, #e3f2fd);
|
||||
border-color: var(--blue, #1976d2);
|
||||
color: var(--blue, #1976d2);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
117
app/components/baseHeader.vue
Normal file
117
app/components/baseHeader.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<script lang="ts" setup="">
|
||||
|
||||
import IconLogo from "~/components/icons/iconLogo.vue";
|
||||
import {Button} from "~/components/ui/Buttons";
|
||||
import {ButtonKind} from "~/components/ui/Buttons/constants.buttons";
|
||||
import {EDITOR_KEY} from "~/shared/constants";
|
||||
import type {Editor} from "@tiptap/vue-3";
|
||||
import type {MaterialSendDto} from "~/stores/materials/types.materials";
|
||||
import dayjs from "dayjs";
|
||||
import {useMaterialsStore} from "~/stores/materials/useMaterialsStore";
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const editor = inject<Ref<Editor | null>>(EDITOR_KEY)
|
||||
|
||||
const isLoadingSave = ref(false);
|
||||
const materialsStore = useMaterialsStore()
|
||||
|
||||
const isntMaterialCreatePage = computed(() => route.name !== 'materials-create')
|
||||
const getButtonKind = computed(() => {
|
||||
if (route.name === 'materials-create') return ButtonKind.Send
|
||||
|
||||
return ButtonKind.CreateMaterial
|
||||
})
|
||||
|
||||
const onClick = () => {
|
||||
if (isntMaterialCreatePage.value) {
|
||||
router.push({name: 'materials-create'})
|
||||
return;
|
||||
}
|
||||
prepareEditorData()
|
||||
}
|
||||
|
||||
const prepareEditorData = async() => {
|
||||
const json = editor?.value?.getJSON()
|
||||
const html = editor?.value?.getHTML()
|
||||
if (!json || !html) return;
|
||||
const title = json.content?.find(el => el.type === 'heading' && el.attrs?.level === 1)?.content?.[0]?.text
|
||||
const shortDescription = json.content?.find(el => el.type === 'paragraph')?.content?.[0]?.text
|
||||
const sendData: MaterialSendDto = {
|
||||
title,
|
||||
short_description: shortDescription,
|
||||
datetime: dayjs().format('YYYY-MM-DDTHH:mm:ss.sssZ'),
|
||||
description_json: json,
|
||||
description_html: html
|
||||
|
||||
}
|
||||
try {
|
||||
isLoadingSave.value = true
|
||||
const {data,error} = await useFetch('/api/save', {
|
||||
method: 'POST',
|
||||
body: sendData
|
||||
})
|
||||
if (data.value) {
|
||||
router.push({name: 'materials-id', params: {id: data.value.id}})
|
||||
}
|
||||
} catch {
|
||||
|
||||
} finally {
|
||||
isLoadingSave.value = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-header">
|
||||
<div class="header">
|
||||
<NuxtLink to="/" class="link">
|
||||
<IconLogo class="icon" />
|
||||
</NuxtLink>
|
||||
<div>
|
||||
<Button.ButtonDefault :kind="getButtonKind" @click="onClick" :loading="isLoadingSave" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.header {
|
||||
max-width: 1224px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px 36px;
|
||||
background: #fff;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 44px;
|
||||
width: 115px;
|
||||
color: var(--text-blue);
|
||||
}
|
||||
|
||||
.bg-header {
|
||||
background: #fff;
|
||||
width: 100dvw;
|
||||
}
|
||||
|
||||
.link {
|
||||
line-height: 80%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.header {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.icon {
|
||||
height: 36px;
|
||||
width: 93px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
8
app/components/icons/iconAdd.vue
Normal file
8
app/components/icons/iconAdd.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 12.75H6C5.59 12.75 5.25 12.41 5.25 12C5.25 11.59 5.59 11.25 6 11.25H18C18.41 11.25 18.75 11.59 18.75 12C18.75 12.41 18.41 12.75 18 12.75Z" fill="currentColor"/>
|
||||
<path d="M12 18.75C11.59 18.75 11.25 18.41 11.25 18V6C11.25 5.59 11.59 5.25 12 5.25C12.41 5.25 12.75 5.59 12.75 6V18C12.75 18.41 12.41 18.75 12 18.75Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
</template>
|
||||
8
app/components/icons/iconArrowLeft.vue
Normal file
8
app/components/icons/iconArrowLeft.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.57 18.82C9.38 18.82 9.19 18.75 9.04 18.6L2.97 12.53C2.68 12.24 2.68 11.76 2.97 11.47L9.04 5.4C9.33 5.11 9.81 5.11 10.1 5.4C10.39 5.69 10.39 6.17 10.1 6.46L4.56 12L10.1 17.54C10.39 17.83 10.39 18.31 10.1 18.6C9.96 18.75 9.76 18.82 9.57 18.82Z" fill="currentColor"/>
|
||||
<path d="M20.5 12.75H3.67C3.26 12.75 2.92 12.41 2.92 12C2.92 11.59 3.26 11.25 3.67 11.25H20.5C20.91 11.25 21.25 11.59 21.25 12C21.25 12.41 20.91 12.75 20.5 12.75Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
</template>
|
||||
8
app/components/icons/iconArrowRight.vue
Normal file
8
app/components/icons/iconArrowRight.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.43 18.82C14.24 18.82 14.05 18.75 13.9 18.6C13.61 18.31 13.61 17.83 13.9 17.54L19.44 12L13.9 6.46C13.61 6.17 13.61 5.69 13.9 5.4C14.19 5.11 14.67 5.11 14.96 5.4L21.03 11.47C21.32 11.76 21.32 12.24 21.03 12.53L14.96 18.6C14.81 18.75 14.62 18.82 14.43 18.82Z" fill="currentColor"/>
|
||||
<path d="M20.33 12.75H3.5C3.09 12.75 2.75 12.41 2.75 12C2.75 11.59 3.09 11.25 3.5 11.25H20.33C20.74 11.25 21.08 11.59 21.08 12C21.08 12.41 20.74 12.75 20.33 12.75Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
</template>
|
||||
7
app/components/icons/iconArrowSquareLeft.vue
Normal file
7
app/components/icons/iconArrowSquareLeft.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 22.75H9C3.57 22.75 1.25 20.43 1.25 15V9C1.25 3.57 3.57 1.25 9 1.25H15C20.43 1.25 22.75 3.57 22.75 9V15C22.75 20.43 20.43 22.75 15 22.75ZM9 2.75C4.39 2.75 2.75 4.39 2.75 9V15C2.75 19.61 4.39 21.25 9 21.25H15C19.61 21.25 21.25 19.61 21.25 15V9C21.25 4.39 19.61 2.75 15 2.75H9Z" fill="currentColor"/>
|
||||
<path d="M13.26 16.28C13.07 16.28 12.88 16.21 12.73 16.06L9.2 12.53C8.91 12.24 8.91 11.76 9.2 11.47L12.73 7.94C13.02 7.65 13.5 7.65 13.79 7.94C14.08 8.23 14.08 8.71 13.79 9L10.79 12L13.79 15C14.08 15.29 14.08 15.77 13.79 16.06C13.65 16.21 13.46 16.28 13.26 16.28Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
</template>
|
||||
8
app/components/icons/iconArrowSquareRight.vue
Normal file
8
app/components/icons/iconArrowSquareRight.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 22.75H9C3.57 22.75 1.25 20.43 1.25 15V9C1.25 3.57 3.57 1.25 9 1.25H15C20.43 1.25 22.75 3.57 22.75 9V15C22.75 20.43 20.43 22.75 15 22.75ZM9 2.75C4.39 2.75 2.75 4.39 2.75 9V15C2.75 19.61 4.39 21.25 9 21.25H15C19.61 21.25 21.25 19.61 21.25 15V9C21.25 4.39 19.61 2.75 15 2.75H9Z" fill="currentColor"/>
|
||||
<path d="M10.74 16.28C10.55 16.28 10.36 16.21 10.21 16.06C9.92 15.77 9.92 15.29 10.21 15L13.21 12L10.21 9C9.92 8.71 9.92 8.23 10.21 7.94C10.5 7.65 10.98 7.65 11.27 7.94L14.8 11.47C15.09 11.76 15.09 12.24 14.8 12.53L11.27 16.06C11.12 16.21 10.93 16.28 10.74 16.28Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
</template>
|
||||
15
app/components/icons/iconCalendar.vue
Normal file
15
app/components/icons/iconCalendar.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 5.75C7.59 5.75 7.25 5.41 7.25 5V2C7.25 1.59 7.59 1.25 8 1.25C8.41 1.25 8.75 1.59 8.75 2V5C8.75 5.41 8.41 5.75 8 5.75Z" fill="currentColor"/>
|
||||
<path d="M16 5.75C15.59 5.75 15.25 5.41 15.25 5V2C15.25 1.59 15.59 1.25 16 1.25C16.41 1.25 16.75 1.59 16.75 2V5C16.75 5.41 16.41 5.75 16 5.75Z" fill="currentColor"/>
|
||||
<path d="M8.5 14.5C8.37 14.5 8.24 14.47 8.12 14.42C7.99 14.37 7.89 14.3 7.79 14.21C7.61 14.02 7.5 13.77 7.5 13.5C7.5 13.37 7.53 13.24 7.58 13.12C7.63 13 7.7 12.89 7.79 12.79C7.89 12.7 7.99 12.63 8.12 12.58C8.48 12.43 8.93 12.51 9.21 12.79C9.39 12.98 9.5 13.24 9.5 13.5C9.5 13.56 9.49 13.63 9.48 13.7C9.47 13.76 9.45 13.82 9.42 13.88C9.4 13.94 9.37 14 9.33 14.06C9.3 14.11 9.25 14.16 9.21 14.21C9.02 14.39 8.76 14.5 8.5 14.5Z" fill="currentColor"/>
|
||||
<path d="M12 14.5C11.87 14.5 11.74 14.47 11.62 14.42C11.49 14.37 11.39 14.3 11.29 14.21C11.11 14.02 11 13.77 11 13.5C11 13.37 11.03 13.24 11.08 13.12C11.13 13 11.2 12.89 11.29 12.79C11.39 12.7 11.49 12.63 11.62 12.58C11.98 12.42 12.43 12.51 12.71 12.79C12.89 12.98 13 13.24 13 13.5C13 13.56 12.99 13.63 12.98 13.7C12.97 13.76 12.95 13.82 12.92 13.88C12.9 13.94 12.87 14 12.83 14.06C12.8 14.11 12.75 14.16 12.71 14.21C12.52 14.39 12.26 14.5 12 14.5Z" fill="currentColor"/>
|
||||
<path d="M15.5 14.5C15.37 14.5 15.24 14.47 15.12 14.42C14.99 14.37 14.89 14.3 14.79 14.21C14.75 14.16 14.71 14.11 14.67 14.06C14.63 14 14.6 13.94 14.58 13.88C14.55 13.82 14.53 13.76 14.52 13.7C14.51 13.63 14.5 13.56 14.5 13.5C14.5 13.24 14.61 12.98 14.79 12.79C14.89 12.7 14.99 12.63 15.12 12.58C15.49 12.42 15.93 12.51 16.21 12.79C16.39 12.98 16.5 13.24 16.5 13.5C16.5 13.56 16.49 13.63 16.48 13.7C16.47 13.76 16.45 13.82 16.42 13.88C16.4 13.94 16.37 14 16.33 14.06C16.3 14.11 16.25 14.16 16.21 14.21C16.02 14.39 15.76 14.5 15.5 14.5Z" fill="currentColor"/>
|
||||
<path d="M8.5 18C8.37 18 8.24 17.97 8.12 17.92C8 17.87 7.89 17.8 7.79 17.71C7.61 17.52 7.5 17.26 7.5 17C7.5 16.87 7.53 16.74 7.58 16.62C7.63 16.49 7.7 16.38 7.79 16.29C8.16 15.92 8.84 15.92 9.21 16.29C9.39 16.48 9.5 16.74 9.5 17C9.5 17.26 9.39 17.52 9.21 17.71C9.02 17.89 8.76 18 8.5 18Z" fill="currentColor"/>
|
||||
<path d="M12 18C11.74 18 11.48 17.89 11.29 17.71C11.11 17.52 11 17.26 11 17C11 16.87 11.03 16.74 11.08 16.62C11.13 16.49 11.2 16.38 11.29 16.29C11.66 15.92 12.34 15.92 12.71 16.29C12.8 16.38 12.87 16.49 12.92 16.62C12.97 16.74 13 16.87 13 17C13 17.26 12.89 17.52 12.71 17.71C12.52 17.89 12.26 18 12 18Z" fill="currentColor"/>
|
||||
<path d="M15.5 18C15.24 18 14.98 17.89 14.79 17.71C14.7 17.62 14.63 17.51 14.58 17.38C14.53 17.26 14.5 17.13 14.5 17C14.5 16.87 14.53 16.74 14.58 16.62C14.63 16.49 14.7 16.38 14.79 16.29C15.02 16.06 15.37 15.95 15.69 16.02C15.76 16.03 15.82 16.05 15.88 16.08C15.94 16.1 16 16.13 16.06 16.17C16.11 16.2 16.16 16.25 16.21 16.29C16.39 16.48 16.5 16.74 16.5 17C16.5 17.26 16.39 17.52 16.21 17.71C16.02 17.89 15.76 18 15.5 18Z" fill="currentColor"/>
|
||||
<path d="M20.5 9.83997H3.5C3.09 9.83997 2.75 9.49997 2.75 9.08997C2.75 8.67997 3.09 8.33997 3.5 8.33997H20.5C20.91 8.33997 21.25 8.67997 21.25 9.08997C21.25 9.49997 20.91 9.83997 20.5 9.83997Z" fill="currentColor"/>
|
||||
<path d="M16 22.75H8C4.35 22.75 2.25 20.65 2.25 17V8.5C2.25 4.85 4.35 2.75 8 2.75H16C19.65 2.75 21.75 4.85 21.75 8.5V17C21.75 20.65 19.65 22.75 16 22.75ZM8 4.25C5.14 4.25 3.75 5.64 3.75 8.5V17C3.75 19.86 5.14 21.25 8 21.25H16C18.86 21.25 20.25 19.86 20.25 17V8.5C20.25 5.64 18.86 4.25 16 4.25H8Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
</template>
|
||||
7
app/components/icons/iconLoader.vue
Normal file
7
app/components/icons/iconLoader.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.6696 7.58373C18.9869 8.72224 19.0768 9.91209 18.9343 11.0854C18.7918 12.2586 18.4196 13.3923 17.8389 14.4218C17.2582 15.4512 16.4805 16.3561 15.55 17.085C14.6196 17.8138 13.5548 18.3523 12.4163 18.6696C11.2778 18.9869 10.0879 19.0768 8.91463 18.9343C7.74136 18.7918 6.60766 18.4196 5.57824 17.8389C4.54882 17.2582 3.64386 16.4805 2.91502 15.55C2.18617 14.6196 1.64773 13.5548 1.33042 12.4163" stroke="#00B2FF" stroke-width="2"/>
|
||||
</svg>
|
||||
|
||||
</template>
|
||||
20
app/components/icons/iconLogo.vue
Normal file
20
app/components/icons/iconLogo.vue
Normal file
File diff suppressed because one or more lines are too long
8
app/components/icons/iconSend.vue
Normal file
8
app/components/icons/iconSend.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.22 21.63C13.04 21.63 11.37 20.8 10.05 16.83L9.33 14.67L7.17 13.95C3.21 12.63 2.38 10.96 2.38 9.78001C2.38 8.61001 3.21 6.93001 7.17 5.60001L15.66 2.77001C17.78 2.06001 19.55 2.27001 20.64 3.35001C21.73 4.43001 21.94 6.21001 21.23 8.33001L18.4 16.82C17.07 20.8 15.4 21.63 14.22 21.63ZM7.64 7.03001C4.86 7.96001 3.87 9.06001 3.87 9.78001C3.87 10.5 4.86 11.6 7.64 12.52L10.16 13.36C10.38 13.43 10.56 13.61 10.63 13.83L11.47 16.35C12.39 19.13 13.5 20.12 14.22 20.12C14.94 20.12 16.04 19.13 16.97 16.35L19.8 7.86001C20.31 6.32001 20.22 5.06001 19.57 4.41001C18.92 3.76001 17.66 3.68001 16.13 4.19001L7.64 7.03001Z" fill="currentColor"/>
|
||||
<path d="M10.11 14.4C9.92 14.4 9.73001 14.33 9.58001 14.18C9.29001 13.89 9.29001 13.41 9.58001 13.12L13.16 9.53001C13.45 9.24001 13.93 9.24001 14.22 9.53001C14.51 9.82001 14.51 10.3 14.22 10.59L10.64 14.18C10.5 14.33 10.3 14.4 10.11 14.4Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
</template>
|
||||
8
app/components/icons/iconTickCircle.vue
Normal file
8
app/components/icons/iconTickCircle.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 22.75C6.07 22.75 1.25 17.93 1.25 12C1.25 6.07 6.07 1.25 12 1.25C17.93 1.25 22.75 6.07 22.75 12C22.75 17.93 17.93 22.75 12 22.75ZM12 2.75C6.9 2.75 2.75 6.9 2.75 12C2.75 17.1 6.9 21.25 12 21.25C17.1 21.25 21.25 17.1 21.25 12C21.25 6.9 17.1 2.75 12 2.75Z" fill="currentColor"/>
|
||||
<path d="M10.58 15.58C10.38 15.58 10.19 15.5 10.05 15.36L7.22 12.53C6.93 12.24 6.93 11.76 7.22 11.47C7.51 11.18 7.99 11.18 8.28 11.47L10.58 13.77L15.72 8.63C16.01 8.34 16.49 8.34 16.78 8.63C17.07 8.92 17.07 9.4 16.78 9.69L11.11 15.36C10.97 15.5 10.78 15.58 10.58 15.58Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
</template>
|
||||
8
app/components/icons/iconTickSquare.vue
Normal file
8
app/components/icons/iconTickSquare.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 22.75H9C3.57 22.75 1.25 20.43 1.25 15V9C1.25 3.57 3.57 1.25 9 1.25H15C20.43 1.25 22.75 3.57 22.75 9V15C22.75 20.43 20.43 22.75 15 22.75ZM9 2.75C4.39 2.75 2.75 4.39 2.75 9V15C2.75 19.61 4.39 21.25 9 21.25H15C19.61 21.25 21.25 19.61 21.25 15V9C21.25 4.39 19.61 2.75 15 2.75H9Z" fill="currentColor"/>
|
||||
<path d="M10.58 15.58C10.38 15.58 10.19 15.5 10.05 15.36L7.22 12.53C6.93 12.24 6.93 11.76 7.22 11.47C7.51 11.18 7.99 11.18 8.28 11.47L10.58 13.77L15.72 8.63C16.01 8.34 16.49 8.34 16.78 8.63C17.07 8.92 17.07 9.4 16.78 9.69L11.11 15.36C10.97 15.5 10.78 15.58 10.58 15.58Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
</template>
|
||||
47
app/components/ui/Buttons/ButtonBack.vue
Normal file
47
app/components/ui/Buttons/ButtonBack.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script lang="ts" setup="">
|
||||
import {useGoBackOrHome} from "~/composables/useGoBackOrHome";
|
||||
|
||||
const router = useRouter()
|
||||
const {goBack, canGoBack} = useGoBackOrHome()
|
||||
const onClickBack = () => {
|
||||
if (canGoBack.value) {
|
||||
goBack()
|
||||
return
|
||||
}
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button type="button" class="button" @click="onClickBack">
|
||||
<IconsIconArrowLeft class="icon"/>
|
||||
<span class="text" >Назад</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.button {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: var(--text-black);
|
||||
transition: all .3s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-blue);
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
@include mixins.square(24px);
|
||||
}
|
||||
|
||||
.text {
|
||||
font-family: FuturaPT, sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
110
app/components/ui/Buttons/ButtonDefault.vue
Normal file
110
app/components/ui/Buttons/ButtonDefault.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<script lang="ts" setup>
|
||||
import IconLoader from "~/components/icons/iconLoader.vue";
|
||||
import type {ButtonProps} from "~/components/ui/Buttons/types.buttons";
|
||||
import {ButtonKind} from "~/components/ui/Buttons/constants.buttons";
|
||||
import IconAdd from "~/components/icons/iconAdd.vue";
|
||||
import IconSend from "~/components/icons/iconSend.vue";
|
||||
import {useWindowSize} from "@vueuse/core";
|
||||
|
||||
const {width} = useWindowSize()
|
||||
|
||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
loading: false,
|
||||
kind: ButtonKind.CreateMaterial
|
||||
|
||||
})
|
||||
|
||||
const getButtonTextAndIcon = computed(() => {
|
||||
switch (props.kind) {
|
||||
case ButtonKind.CreateStory:
|
||||
return {
|
||||
text: 'Создать историю',
|
||||
icon: IconAdd,
|
||||
};
|
||||
case ButtonKind.CreateMaterial:
|
||||
return {
|
||||
text: 'Создать материал',
|
||||
icon: IconAdd,
|
||||
};
|
||||
case ButtonKind.Send:
|
||||
default:
|
||||
return {
|
||||
text: 'Сохранить',
|
||||
icon: IconSend
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button type="button" class="button">
|
||||
<IconLoader class="icon loader" :class="{loading}" v-if="loading" />
|
||||
|
||||
<template v-else>
|
||||
<span v-if="width > 768">{{getButtonTextAndIcon.text}}</span>
|
||||
<component v-else :is="getButtonTextAndIcon.icon" class="icon"/>
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.button {
|
||||
border-radius: 12px;
|
||||
padding: 0 24px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #E7F8FE;
|
||||
color: var(--text-blue);
|
||||
|
||||
font-family: FuturaPT, sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
line-height: 100%;
|
||||
|
||||
cursor: pointer;
|
||||
transition: all .3s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #D2F3FF;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #E7F8FE;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.loading {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
@include mixins.square(20px);
|
||||
}
|
||||
|
||||
.loader {
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
rotate: 0deg;
|
||||
}
|
||||
|
||||
100% {
|
||||
rotate: 360deg;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.button {
|
||||
padding: 10px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@include mixins.square(28px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
5
app/components/ui/Buttons/constants.buttons.ts
Normal file
5
app/components/ui/Buttons/constants.buttons.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum ButtonKind {
|
||||
Send = 'send',
|
||||
CreateStory = 'createStory',
|
||||
CreateMaterial = 'createMaterial'
|
||||
}
|
||||
7
app/components/ui/Buttons/index.ts
Normal file
7
app/components/ui/Buttons/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import ButtonBack from "~/components/ui/Buttons/ButtonBack.vue";
|
||||
import ButtonDefault from "~/components/ui/Buttons/ButtonDefault.vue";
|
||||
|
||||
export const Button = {
|
||||
ButtonBack: ButtonBack,
|
||||
ButtonDefault: ButtonDefault
|
||||
}
|
||||
7
app/components/ui/Buttons/types.buttons.ts
Normal file
7
app/components/ui/Buttons/types.buttons.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {ButtonBackTheme, ButtonKind} from "~/components/ui/Buttons/constants.buttons";
|
||||
|
||||
export interface ButtonProps {
|
||||
loading?: boolean
|
||||
kind?: ButtonKind,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user