@@ -39,8 +39,9 @@ const social = [
|
||||
.copyright {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-top: 3rem;
|
||||
|
||||
&__text {
|
||||
padding: 0 0.25rem;
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="dropdown">
|
||||
<div v-if="modelValue" ref="floatingEl" class="dropdown-menu" :style="floatingStyle">
|
||||
<div
|
||||
v-if="modelValue"
|
||||
ref="floatingEl"
|
||||
class="dropdown-menu"
|
||||
:class="{ 'dropdown-menu--center': align === 'center' }"
|
||||
:style="floatingStyle"
|
||||
>
|
||||
<ul class="dropdown-menu__list">
|
||||
<li
|
||||
v-for="(item, i) in items"
|
||||
@@ -11,11 +17,21 @@
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-menu__item"
|
||||
:class="{ 'dropdown-menu__item--danger': item.danger }"
|
||||
:class="{
|
||||
'dropdown-menu__item--danger': item.danger,
|
||||
'dropdown-menu__item--colored': item.color,
|
||||
}"
|
||||
:style="{ color: item.color }"
|
||||
:disabled="item.disabled"
|
||||
@click="handleClick(item)"
|
||||
>
|
||||
<SvgIcon v-if="item.icon" :name="item.icon" :size="16" class="dropdown-menu__icon" />
|
||||
<SvgIcon
|
||||
v-if="item.icon"
|
||||
:name="item.icon"
|
||||
:size="16"
|
||||
:color="item.iconColor ?? item.color"
|
||||
class="dropdown-menu__icon"
|
||||
/>
|
||||
<span class="dropdown-menu__label">{{ item.label }}</span>
|
||||
</button>
|
||||
</li>
|
||||
@@ -43,6 +59,7 @@ const props = defineProps({
|
||||
placement: { type: String, default: 'bottom-end' },
|
||||
offset: { type: Number, default: 8 },
|
||||
minWidth: { type: String, default: '12rem' },
|
||||
align: { type: String, default: 'start' },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'select'])
|
||||
@@ -180,6 +197,21 @@ onBeforeUnmount(unmountFloating)
|
||||
&__icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__item--colored &__icon {
|
||||
:deep([stroke]) {
|
||||
stroke: currentcolor;
|
||||
}
|
||||
}
|
||||
|
||||
&--center &__item {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&--center &__label {
|
||||
flex: 0 1 auto;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-enter-active,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<ul>
|
||||
<li v-for="item in iconList" :key="item.icon">
|
||||
<button type="button" class="header-block__icon-btn" @click="item.action">
|
||||
<SvgIcon :name="item.icon" :size="24" color="#9a9a9a" />
|
||||
<SvgIcon :name="item.icon" :size="24" color="var(--header-icon-color)" />
|
||||
<span v-if="item.count" class="header-block__icon-badge">{{ item.count }}</span>
|
||||
</button>
|
||||
</li>
|
||||
@@ -30,14 +30,27 @@
|
||||
<div v-if="!hasSlotContent && pageTitle">{{ pageTitle }}</div>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<DropdownMenu
|
||||
v-model="userMenuOpen"
|
||||
:reference="userMenuTrigger"
|
||||
:items="userMenuItems"
|
||||
placement="bottom-end"
|
||||
align="center"
|
||||
min-width="9rem"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUIStore } from '@/store/ui'
|
||||
import { computed, useSlots } from 'vue'
|
||||
import { gallery } from '@/utils/gallery'
|
||||
import useAuth from '@/composables/useAuth'
|
||||
import { computed, ref, useSlots } from 'vue'
|
||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||
import DropdownMenu from '@/components/DropdownMenu.vue'
|
||||
import { useLogoutMutation } from '@/services/query/auth'
|
||||
|
||||
defineProps({
|
||||
pageTitle: { type: String, default: '' },
|
||||
@@ -47,13 +60,55 @@ const slots = useSlots()
|
||||
const hasSlotContent = computed(() => !!slots.default)
|
||||
|
||||
const ui = useUIStore()
|
||||
const router = useRouter()
|
||||
const { clearSession } = useAuth()
|
||||
const logoutMutation = useLogoutMutation()
|
||||
|
||||
const logo = gallery.authLogo
|
||||
|
||||
const userMenuOpen = ref(false)
|
||||
const userMenuTrigger = ref(null)
|
||||
|
||||
const openUserMenu = (event) => {
|
||||
const trigger = event.currentTarget
|
||||
if (userMenuOpen.value && userMenuTrigger.value === trigger) {
|
||||
userMenuOpen.value = false
|
||||
return
|
||||
}
|
||||
userMenuTrigger.value = trigger
|
||||
userMenuOpen.value = true
|
||||
}
|
||||
|
||||
const goEditProfile = () => {
|
||||
router.push({ name: 'student-profile' }).catch(() => {})
|
||||
}
|
||||
|
||||
const onLogout = async () => {
|
||||
try {
|
||||
await logoutMutation.mutateAsync()
|
||||
} catch {
|
||||
/* ignore */
|
||||
} finally {
|
||||
clearSession()
|
||||
router.push({ name: 'login' })
|
||||
}
|
||||
}
|
||||
|
||||
const userMenuItems = [
|
||||
{ key: 'edit-profile', label: 'ویرایش پروفایل', icon: 'pencil', onClick: goEditProfile },
|
||||
{
|
||||
key: 'logout',
|
||||
label: 'خروج',
|
||||
icon: 'heart',
|
||||
color: 'var(--color-primary)',
|
||||
onClick: onLogout,
|
||||
},
|
||||
]
|
||||
|
||||
const iconList = [
|
||||
{ icon: 'chat', action: () => {} },
|
||||
{ icon: 'bell', action: () => {}, count: 5 },
|
||||
{ icon: 'user', action: () => {} },
|
||||
// { icon: 'chat', action: () => {} },
|
||||
// { icon: 'bell', action: () => {}, count: 5 },
|
||||
{ icon: 'user', action: openUserMenu },
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -136,12 +191,18 @@ const iconList = [
|
||||
}
|
||||
|
||||
&__icon-btn {
|
||||
--header-icon-color: #9a9a9a;
|
||||
|
||||
position: relative;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 9999px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
--header-icon-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&__icon-badge {
|
||||
|
||||
@@ -25,8 +25,8 @@ defineProps({
|
||||
|
||||
&__bar {
|
||||
background-color: var(--color-primary);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 5%);
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 3px 8px rgba(243, 102, 117, 45%);
|
||||
height: 1.9rem;
|
||||
width: 0.375rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="no-items">
|
||||
<div class="no-items__inner">
|
||||
<SvgIcon name="warning" :size="120" color="#cbd5e1" />
|
||||
<SvgIcon name="smiley-sad" :size="171" />
|
||||
<p class="no-items__title">{{ title }}</p>
|
||||
<p class="no-items__desc">{{ desc }}</p>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
@click="onChange(tab.name)"
|
||||
>
|
||||
<span class="tabs-block__tab-card">
|
||||
<SvgIcon v-if="tab.icon" :name="tab.icon" :size="16" color="#bcbcbc" />
|
||||
<SvgIcon v-if="tab.icon" :name="tab.icon" :size="18" color="#bcbcbc" />
|
||||
<span class="tabs-block__tab-label">{{ tab.label }}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
@dragleave="isDragging = false"
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<SvgIcon name="upload" :size="32" class="file-uploader__upload-icon" />
|
||||
<span class="file-uploader__upload-text">{{ uploadText }}</span>
|
||||
<img :src="uploadSimple" class="file-uploader__upload-icon" />
|
||||
<span class="file-uploader__upload-text">
|
||||
<span class="file-uploader__upload-text--light">بارگذاری</span>
|
||||
<span class="file-uploader__upload-text--bold">فایل ضمیمــــه</span>
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
class="file-uploader__input"
|
||||
@@ -56,14 +59,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="file-uploader__empty">هیچ فایلی آپلود نشده است</div>
|
||||
<!-- <div v-else class="file-uploader__empty">هیچ فایلی آپلود نشده است</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||
import CircleButton from '@/components/CircleButton.vue'
|
||||
import uploadSimple from '@/assets/images/upload-simple.png'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Array, default: () => [] },
|
||||
@@ -77,10 +81,6 @@ const emit = defineEmits(['update:modelValue', 'select', 'remove', 'error'])
|
||||
|
||||
const isDragging = ref(false)
|
||||
|
||||
const uploadText = computed(() =>
|
||||
props.modelValue.length === 0 ? 'فایل جدید اضافه کنید' : 'فایل دیگری اضافه کنید'
|
||||
)
|
||||
|
||||
const handleFileSelect = (event) => {
|
||||
const files = [...event.target.files]
|
||||
emitFiles(files)
|
||||
@@ -140,14 +140,15 @@ const formatFileSize = (bytes) => {
|
||||
}
|
||||
|
||||
&__dropzone {
|
||||
border: 2px dashed #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #f4f4f4;
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
transition: border-color 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&:hover,
|
||||
@@ -161,10 +162,20 @@ const formatFileSize = (bytes) => {
|
||||
}
|
||||
|
||||
&__upload-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: var(--font-family-fa);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #4b5563;
|
||||
|
||||
&--light {
|
||||
text-align: right;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&--bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</div>
|
||||
|
||||
<div class="image-cropper__hint">
|
||||
<SvgIcon name="warning" class="image-cropper__hint-icon" />
|
||||
<SvgIcon :name="helperIcon" color="#B8B8B8" :size="20" />
|
||||
<p>{{ helperText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,6 +99,7 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: 'سایز تصویر رسمی 3x4 و حجم تصویر بیشتر از 250 کیلو بایت نباشد.',
|
||||
},
|
||||
helperIcon: { type: String, default: 'warning' },
|
||||
error: { type: String, default: '' },
|
||||
})
|
||||
|
||||
@@ -314,7 +315,7 @@ onBeforeUnmount(cleanup)
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
gap: 0.25rem;
|
||||
|
||||
p {
|
||||
font-family: var(--font-family-fa);
|
||||
@@ -325,7 +326,7 @@ onBeforeUnmount(cleanup)
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.75rem;
|
||||
line-height: 1.85rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<SvgIcon
|
||||
name="caret-down"
|
||||
:size="20"
|
||||
color="#DDDDDD"
|
||||
class="select-field__caret"
|
||||
:class="{ 'select-field__caret--open': isOpen }"
|
||||
/>
|
||||
@@ -242,10 +243,6 @@ onBeforeUnmount(() => unmountFloating())
|
||||
transition: all 0.2s;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1px var(--color-validate);
|
||||
}
|
||||
|
||||
&--error {
|
||||
border-color: var(--color-error);
|
||||
|
||||
@@ -308,10 +305,6 @@ onBeforeUnmount(() => unmountFloating())
|
||||
border: 1px solid var(--color-thd-gray);
|
||||
border-radius: 0.75rem;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1px var(--color-validate);
|
||||
}
|
||||
}
|
||||
|
||||
&__empty {
|
||||
|
||||
@@ -116,7 +116,6 @@ defineExpose({ focus: () => inputEl.value?.focus() })
|
||||
|
||||
&:focus {
|
||||
border-color: var(--color-thd-gray);
|
||||
box-shadow: 0 0 0 1px var(--color-validate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +134,7 @@ defineExpose({ focus: () => inputEl.value?.focus() })
|
||||
|
||||
&__icon {
|
||||
position: absolute;
|
||||
inset-inline-start: 0.75rem;
|
||||
inset-inline-end: 0.75rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
|
||||
@@ -143,7 +143,6 @@ onMounted(() => {
|
||||
|
||||
&:focus {
|
||||
border-color: var(--color-thd-gray);
|
||||
box-shadow: 0 0 0 1px var(--color-validate);
|
||||
}
|
||||
|
||||
&--no-resize {
|
||||
|
||||
@@ -26,6 +26,7 @@ const props = defineProps({
|
||||
const component = computed(() => {
|
||||
const c = iconRegistry[props.name]
|
||||
if (!c && import.meta.env.DEV) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[SvgIcon] unknown icon "${props.name}"`)
|
||||
}
|
||||
|
||||
@@ -46,6 +47,5 @@ const iconStyle = computed(() => ({
|
||||
.svg-icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
fill: currentcolor;
|
||||
}
|
||||
</style>
|
||||
|
||||
Vendored
+8
@@ -12,17 +12,23 @@ export type IconName =
|
||||
| 'caret-down'
|
||||
| 'caret-left'
|
||||
| 'caret-right'
|
||||
| 'certificate'
|
||||
| 'chat'
|
||||
| 'chat-centered-dots'
|
||||
| 'chat-teardrop-text'
|
||||
| 'check'
|
||||
| 'check-circle'
|
||||
| 'check-square'
|
||||
| 'clock'
|
||||
| 'close'
|
||||
| 'copy'
|
||||
| 'envelope-open'
|
||||
| 'eye'
|
||||
| 'eye-slash'
|
||||
| 'file'
|
||||
| 'folder-simple-star'
|
||||
| 'funnel'
|
||||
| 'graduation-cap'
|
||||
| 'heart'
|
||||
| 'instagram'
|
||||
| 'link'
|
||||
@@ -39,12 +45,14 @@ export type IconName =
|
||||
| 'plus'
|
||||
| 'scroll'
|
||||
| 'shopping-bag'
|
||||
| 'smiley-sad'
|
||||
| 'spinner'
|
||||
| 'square'
|
||||
| 'telegram'
|
||||
| 'trash'
|
||||
| 'upload'
|
||||
| 'user'
|
||||
| 'user-circle-check'
|
||||
| 'user-sound'
|
||||
| 'users'
|
||||
| 'users-three'
|
||||
|
||||
Reference in New Issue
Block a user