1 parent
7639a54c50
commit
2b94bcc20e
11 files changed
+172
-51
No files matched your search
@@ -0,0 +1,66 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { usePushNotifications } from '@/composables/usePushNotifications.ts'
|
||||||
|
import NotificationsListElement from '@/components/NotificationsListElement.vue'
|
||||||
|
|
||||||
|
const status = ref<NotificationPermission | 'unsupported'>(
|
||||||
|
'Notification' in window ? Notification.permission : 'unsupported',
|
||||||
|
)
|
||||||
|
const loading = ref(false)
|
||||||
|
const push = usePushNotifications()
|
||||||
|
|
||||||
|
async function onClick() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
status.value = await push.enable()
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!navigator.permissions) return
|
||||||
|
const perm = await navigator.permissions.query({ name: 'notifications' })
|
||||||
|
perm.onchange = () => {
|
||||||
|
status.value = Notification.permission
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="d-flex">
|
||||||
|
<v-menu>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn rounded="0" icon v-bind="props" class="">
|
||||||
|
<v-badge bordered location="top right" color="error" content="9+" :offset-x="1" :offset-y="-3">
|
||||||
|
<v-icon icon="mdi-bell" />
|
||||||
|
</v-badge>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list width="15rem" border nav density="compact" class="pa-1">
|
||||||
|
<v-list-item>
|
||||||
|
<v-sheet class="d-flex align-center ga-2">
|
||||||
|
<span v-if="status === 'granted' && push.installationId.value"> Уведомления включены </span>
|
||||||
|
<span v-else-if="status === 'default' || status === 'granted'" class="d-flex flex-column align-center ga-1">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
density="compact"
|
||||||
|
:disabled="loading"
|
||||||
|
@click="onClick"
|
||||||
|
text="Enable notifications"
|
||||||
|
/>
|
||||||
|
<span>And choose 'Allow'</span>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="status === 'denied'">Notifications blocked</span>
|
||||||
|
<span v-else>Браузер не поддерживает уведомления</span>
|
||||||
|
</v-sheet>
|
||||||
|
</v-list-item>
|
||||||
|
<v-divider />
|
||||||
|
<notifications-list-element v-for="(el, idx) in [1, 2, 3, 4, 5]" :key="idx" />
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-list-item>
|
||||||
|
<v-sheet class="d-flex flex-column">
|
||||||
|
<div>Title</div>
|
||||||
|
<div>Subtitle</div>
|
||||||
|
</v-sheet>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { usePushNotifications } from '@/composables/usePushNotifications.ts'
|
|
||||||
|
|
||||||
const status = ref<NotificationPermission | 'unsupported'>(
|
|
||||||
'Notification' in window ? Notification.permission : 'unsupported',
|
|
||||||
)
|
|
||||||
const loading = ref(false)
|
|
||||||
const push = usePushNotifications()
|
|
||||||
|
|
||||||
async function onClick() {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
status.value = await push.enable()
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (!navigator.permissions) return
|
|
||||||
const perm = await navigator.permissions.query({ name: 'notifications' })
|
|
||||||
perm.onchange = () => {
|
|
||||||
status.value = Notification.permission
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="mr-2">
|
|
||||||
<span v-if="status === 'granted' && push.installationId.value">Уведомления включены</span>
|
|
||||||
<button v-else-if="status === 'default' || status === 'granted'" :disabled="loading" @click="onClick">
|
|
||||||
Включить уведомления
|
|
||||||
</button>
|
|
||||||
<span v-else-if="status === 'denied'">Уведомления заблокированы.</span>
|
|
||||||
<span v-else>Браузер не поддерживает уведомления</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
||||||
@@ -7,9 +7,9 @@ import BackButton from '@/components/BackButton.vue'
|
|||||||
import { useThemeMode } from '@/composables/useThemeMode.ts'
|
import { useThemeMode } from '@/composables/useThemeMode.ts'
|
||||||
import { THEME_ICONS } from '@/lib/theme.ts'
|
import { THEME_ICONS } from '@/lib/theme.ts'
|
||||||
import NavigationMenu from '@/components/NavigationMenu.vue'
|
import NavigationMenu from '@/components/NavigationMenu.vue'
|
||||||
import PushEnable from '@/components/PushEnable.vue'
|
|
||||||
import { usePushNotifications } from '@/composables/usePushNotifications.ts'
|
import { usePushNotifications } from '@/composables/usePushNotifications.ts'
|
||||||
import ProjectMenu from '@/components/ProjectMenu.vue'
|
import ProjectMenu from '@/components/ProjectMenu.vue'
|
||||||
|
import NotificationsList from '@/components/NotificationsList.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -22,7 +22,7 @@ const title = 'Ulitka'
|
|||||||
const isAuth = computed(() => !!session.value.data)
|
const isAuth = computed(() => !!session.value.data)
|
||||||
|
|
||||||
function goToSignIn() {
|
function goToSignIn() {
|
||||||
router.push({ path: '/sign-in' })
|
router.push({ path: '/sign-in-otp' })
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToSignUp() {
|
function goToSignUp() {
|
||||||
@@ -66,13 +66,12 @@ const fluid = computed(() => {
|
|||||||
|
|
||||||
<div v-if="!session.data">
|
<div v-if="!session.data">
|
||||||
<v-btn @click="goToSignIn">Sign In</v-btn>
|
<v-btn @click="goToSignIn">Sign In</v-btn>
|
||||||
<v-btn @click="goToSignUp">Sign Up</v-btn>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="session.data" class="d-flex align-center">
|
<div v-if="session.data" class="d-flex align-center">
|
||||||
<project-menu />
|
<project-menu />
|
||||||
|
|
||||||
<push-enable />
|
<notifications-list />
|
||||||
|
|
||||||
<v-btn rounded="0" :icon="THEME_ICONS[themeMode]" :title="`Theme: ${themeMode}`" @click="cycleThemeMode" />
|
<v-btn rounded="0" :icon="THEME_ICONS[themeMode]" :title="`Theme: ${themeMode}`" @click="cycleThemeMode" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createAuthClient } from 'better-auth/vue'
|
import { createAuthClient } from 'better-auth/vue'
|
||||||
|
import { emailOTPClient } from 'better-auth/client/plugins'
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
baseURL: window.location.origin,
|
baseURL: window.location.origin,
|
||||||
plugins: [],
|
plugins: [emailOTPClient()],
|
||||||
})
|
})
|
||||||
+7
-1
@@ -4,11 +4,12 @@ import { issuesRoutes } from '@/router/issues.ts'
|
|||||||
import HomeView from '@/views/HomeView.vue'
|
import HomeView from '@/views/HomeView.vue'
|
||||||
import VerifyEmailView from '@/views/users/VerifyEmailView.vue'
|
import VerifyEmailView from '@/views/users/VerifyEmailView.vue'
|
||||||
import SignUpView from '@/views/users/SignUpView.vue'
|
import SignUpView from '@/views/users/SignUpView.vue'
|
||||||
import SignInView from '@/views/users/SignInView.vue'
|
|
||||||
import { authClient } from '@/lib/auth-client.ts'
|
import { authClient } from '@/lib/auth-client.ts'
|
||||||
import { gamesRoutes } from '@/router/games.ts'
|
import { gamesRoutes } from '@/router/games.ts'
|
||||||
import { settingsRoutes } from '@/router/settings.ts'
|
import { settingsRoutes } from '@/router/settings.ts'
|
||||||
import { calendarRoutes } from '@/router/calendar.ts'
|
import { calendarRoutes } from '@/router/calendar.ts'
|
||||||
|
import SignInViewOtp from '@/views/users/SignInViewOtp.vue'
|
||||||
|
import SignInView from '@/views/users/SignInView.vue'
|
||||||
|
|
||||||
declare module 'vue-router' {
|
declare module 'vue-router' {
|
||||||
interface RouteMeta {
|
interface RouteMeta {
|
||||||
@@ -29,6 +30,11 @@ const router = createRouter({
|
|||||||
component: HomeView,
|
component: HomeView,
|
||||||
meta: { layout: 'Default', fluid: true },
|
meta: { layout: 'Default', fluid: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'sign-in-otp',
|
||||||
|
path: '/sign-in-otp',
|
||||||
|
component: SignInViewOtp,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'sign-in',
|
name: 'sign-in',
|
||||||
path: '/sign-in',
|
path: '/sign-in',
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ const { ckey } = defineProps<Props>()
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="d-flex flex-column ga-2">
|
||||||
|
<div class="text-uppercase text-title-large">Issue: {{ ckey }}</div>
|
||||||
<v-sheet border class="h-100 d-flex flex-column">
|
<v-sheet border class="h-100 d-flex flex-column">
|
||||||
<issue-details :ckey="ckey" />
|
<issue-details :ckey="ckey" />
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
@@ -203,6 +203,7 @@ async function onDeleteBatch() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="d-flex flex-column ga-2 h-100">
|
<div class="d-flex flex-column ga-2 h-100">
|
||||||
|
<div class="text-uppercase text-title-large">Issues</div>
|
||||||
<issue-filter
|
<issue-filter
|
||||||
:text="text"
|
:text="text"
|
||||||
:assignee-id="assigneeId"
|
:assignee-id="assigneeId"
|
||||||
|
|||||||
@@ -26,15 +26,12 @@ const signIn = async () => {
|
|||||||
localStorage.removeItem(REMEMBERED_EMAIL_KEY)
|
localStorage.removeItem(REMEMBERED_EMAIL_KEY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(data)
|
|
||||||
console.log(error)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" md="6" sm="8" offset-sm="2" offset-md="3" offset="0">
|
<v-col cols="12" md="4" sm="8" offset-sm="2" offset-md="4" offset="0">
|
||||||
<div class="d-flex flex-column ga-2">
|
<div class="d-flex flex-column ga-2">
|
||||||
<div class="text-uppercase">sign in</div>
|
<div class="text-uppercase">sign in</div>
|
||||||
<v-text-field prepend-inner-icon="mdi-email" placeholder="E-mail" v-model="signInData.email" />
|
<v-text-field prepend-inner-icon="mdi-email" placeholder="E-mail" v-model="signInData.email" />
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, reactive } from 'vue'
|
||||||
|
import { authClient } from '@/lib/auth-client.js'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const REMEMBERED_EMAIL_KEY = 'rememberedEmail'
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const signInData = reactive({
|
||||||
|
email: localStorage.getItem(REMEMBERED_EMAIL_KEY) ?? '',
|
||||||
|
OTP: '',
|
||||||
|
rememberMe: true,
|
||||||
|
showOTP: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const sendOTP = async () => {
|
||||||
|
const { data, error } = await authClient.emailOtp.sendVerificationOtp({
|
||||||
|
email: signInData.email,
|
||||||
|
type: 'sign-in',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data?.success) {
|
||||||
|
if (signInData.rememberMe) localStorage.setItem(REMEMBERED_EMAIL_KEY, signInData.email)
|
||||||
|
signInData.showOTP = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
localStorage.removeItem(REMEMBERED_EMAIL_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkOTP = async () => {
|
||||||
|
const { data, error } = await authClient.signIn.emailOtp({
|
||||||
|
email: signInData.email,
|
||||||
|
otp: signInData.OTP,
|
||||||
|
})
|
||||||
|
console.log({ data, error })
|
||||||
|
if (data) {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-row justify="center">
|
||||||
|
<v-col cols="12" lg="3" md="4" sm="8">
|
||||||
|
<div class="d-flex flex-column ga-2">
|
||||||
|
<div class="text-uppercase">sign in</div>
|
||||||
|
<v-text-field
|
||||||
|
prepend-inner-icon="mdi-email"
|
||||||
|
density="comfortable"
|
||||||
|
placeholder="E-mail"
|
||||||
|
v-model="signInData.email"
|
||||||
|
/>
|
||||||
|
<v-otp-input
|
||||||
|
v-if="signInData.showOTP"
|
||||||
|
density="comfortable"
|
||||||
|
autofocus
|
||||||
|
pattern="numeric"
|
||||||
|
v-model="signInData.OTP"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-btn v-if="signInData.showOTP" prepend-icon="mdi-login" variant="flat" @click="checkOTP" color="primary">
|
||||||
|
Sign in
|
||||||
|
</v-btn>
|
||||||
|
<v-btn v-else prepend-icon="mdi-email-fast-outline" variant="flat" @click="sendOTP"> Send Code </v-btn>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@@ -24,7 +24,7 @@ const signUp = async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" md="6" sm="8" offset-sm="2" offset-md="3" offset="0">
|
<v-col cols="12" md="4" sm="8" offset-sm="2" offset-md="4" offset="0">
|
||||||
<div class="d-flex flex-column ga-2">
|
<div class="d-flex flex-column ga-2">
|
||||||
<div class="text-uppercase">sign up</div>
|
<div class="text-uppercase">sign up</div>
|
||||||
<v-text-field placeholder="Name" prepend-inner-icon="mdi-account" v-model="signUpData.name" />
|
<v-text-field placeholder="Name" prepend-inner-icon="mdi-account" v-model="signUpData.name" />
|
||||||
|
|||||||
Reference in new issue
Block a user