update
BIN
i18n-2.xlsm
@@ -14,15 +14,12 @@
|
||||
<meta name="robots" content="noindex, nofollow"/>
|
||||
<meta name="msapplication-tap-highlight" content="no">
|
||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||
<script src="http://localhost:8098"></script>
|
||||
<!--
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
|
||||
-->
|
||||
<link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
|
||||
<link rel="icon" type="image/ico" href="favicon.ico">
|
||||
<link rel="icon" href="icons/favicon.svg" type="image/svg+xml">
|
||||
<link rel="icon" type="image/ico" href="icons/favicon.ico">
|
||||
<link rel="apple-touch-icon" href="icons/apple-touch-icon.png">
|
||||
</head>
|
||||
<body>
|
||||
<!-- quasar:entry-point -->
|
||||
|
||||
|
Before Width: | Height: | Size: 63 KiB |
BIN
public/icons/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/icons/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
public/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 859 B After Width: | Height: | Size: 705 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
BIN
public/icons/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
64
public/icons/favicon.svg
Normal file
@@ -0,0 +1,64 @@
|
||||
<svg
|
||||
viewBox="0 0 8.4666662 8.4666662"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs id="defs1" />
|
||||
<g id="layer1">
|
||||
<rect
|
||||
style="fill: #27A7E7 ;stroke-width:0.233149"
|
||||
id="rect5"
|
||||
width="6.9885192"
|
||||
height="0.35581663"
|
||||
x="3.114475"
|
||||
y="0.86827624"
|
||||
transform="matrix(0.77578367,0.63099897,-0.77578367,0.63099897,0,0)"
|
||||
/>
|
||||
<rect
|
||||
style="fill: #27A7E7 ;stroke-width:0.24961"
|
||||
id="rect5-7"
|
||||
width="7.4819207"
|
||||
height="0.3809379"
|
||||
x="-3.9267058"
|
||||
y="5.7988153"
|
||||
transform="matrix(-0.70756824,0.70664502,0.70756824,0.70664502,0,0)"
|
||||
/>
|
||||
<circle
|
||||
style="fill: #27A7E7 ;stroke-width:0.134869"
|
||||
id="path5-8"
|
||||
cx="1.5875"
|
||||
cy="6.8791666"
|
||||
r="1.0583333"
|
||||
/>
|
||||
<circle
|
||||
style="fill: #27A7E7 ;stroke-width:0.168586"
|
||||
id="path5-8-5"
|
||||
cx="7.1437502"
|
||||
cy="7.1437502"
|
||||
r="1.3229166"
|
||||
/>
|
||||
<circle
|
||||
style="fill: #27A7E7 ;stroke-width:0.118011"
|
||||
id="path5-8-5-1"
|
||||
cx="1.4552083"
|
||||
cy="2.5135417"
|
||||
r="0.92604166"
|
||||
/>
|
||||
<circle
|
||||
style="fill: #27A7E7 ;stroke-width:0.101152"
|
||||
id="path5-8-5-1-7"
|
||||
cx="7.1437502"
|
||||
cy="1.3229166"
|
||||
r="0.79374999"
|
||||
/>
|
||||
<circle
|
||||
style="fill: #F36D3A; stroke-width:0.23602"
|
||||
id="path5"
|
||||
cx="3.96875"
|
||||
cy="4.4979167"
|
||||
r="1.8520833"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -233,4 +233,4 @@ export default defineConfig((ctx) => {
|
||||
extraScripts: []
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
45
src/App.vue
@@ -3,14 +3,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, onMounted, ref } from 'vue'
|
||||
import { inject, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from 'stores/auth'
|
||||
import { useSettingsStore } from 'stores/settings'
|
||||
import { useQuasar } from 'quasar'
|
||||
import type { WebApp } from '@twa-dev/types'
|
||||
|
||||
|
||||
const router = useRouter()
|
||||
const tg = inject('tg') as WebApp
|
||||
|
||||
@@ -30,39 +29,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
function parseIdString (input: string): { id: number; taskId?: number; meetingId?: number } | null {
|
||||
const pattern = /^p(?<id>\d+)(?:t(?<taskId>\d+))?(?:m(?<meetingId>\d+))?$/
|
||||
const match = input.match(pattern)
|
||||
|
||||
if (!match?.groups?.id) return null
|
||||
|
||||
const id = parseInt(match.groups.id, 10)
|
||||
const taskId = match.groups.taskId ? parseInt(match.groups.taskId, 10) : undefined
|
||||
const meetingId = match.groups.meetingId ? parseInt(match.groups.meetingId, 10) : undefined
|
||||
|
||||
return {
|
||||
id,
|
||||
...(taskId !== undefined && { taskId }),
|
||||
...(meetingId !== undefined && { meetingId })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const startRouteInfo = ref<{ id: number; taskId?: number; meetingId?: number } | null>(null)
|
||||
if (tg.initDataUnsafe.start_param) {
|
||||
startRouteInfo.value = parseIdString(tg.initDataUnsafe.start_param)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
if (startRouteInfo.value) authStore.setStartRouteInfo(startRouteInfo.value)
|
||||
if (!authStore.isInit) await authStore.init(tg)
|
||||
if (!settingsStore.isInit) await settingsStore.init()
|
||||
} catch {
|
||||
// await router.push({ name: 'server_error'})
|
||||
const authStore = useAuthStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
if (tg) {
|
||||
if (!authStore.isInit) await authStore.init(tg)
|
||||
if (!settingsStore.isInit) await settingsStore.init()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('App initialization failed:', error)
|
||||
alert(error)
|
||||
await router.push({ name: '404' })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
124
src/components/BaseLogo.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex row items-center no-wrap logo-component"
|
||||
>
|
||||
<svg
|
||||
class="iconcolor"
|
||||
viewBox="0 0 8.4666662 8.4666662"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs id="defs1" />
|
||||
<g id="layer1">
|
||||
<rect
|
||||
class="fill-brand"
|
||||
style="stroke-width:0.233149"
|
||||
id="rect5"
|
||||
width="6.9885192"
|
||||
height="0.35581663"
|
||||
x="3.114475"
|
||||
y="0.86827624"
|
||||
transform="matrix(0.77578367,0.63099897,-0.77578367,0.63099897,0,0)"
|
||||
/>
|
||||
<rect
|
||||
class="fill-brand"
|
||||
style="stroke-width:0.24961"
|
||||
id="rect5-7"
|
||||
width="7.4819207"
|
||||
height="0.3809379"
|
||||
x="-3.9267058"
|
||||
y="5.7988153"
|
||||
transform="matrix(-0.70756824,0.70664502,0.70756824,0.70664502,0,0)"
|
||||
/>
|
||||
<circle
|
||||
class="fill-brand"
|
||||
style="stroke-width:0.134869"
|
||||
id="path5-8"
|
||||
cx="1.5875"
|
||||
cy="6.8791666"
|
||||
r="1.0583333"
|
||||
/>
|
||||
<circle
|
||||
class="fill-brand"
|
||||
style="stroke-width:0.168586"
|
||||
id="path5-8-5"
|
||||
cx="7.1437502"
|
||||
cy="7.1437502"
|
||||
r="1.3229166"
|
||||
/>
|
||||
<circle
|
||||
class="fill-brand"
|
||||
style="stroke-width:0.118011"
|
||||
id="path5-8-5-1"
|
||||
cx="1.4552083"
|
||||
cy="2.5135417"
|
||||
r="0.92604166"
|
||||
/>
|
||||
<circle
|
||||
class="fill-brand"
|
||||
style="stroke-width:0.101152"
|
||||
id="path5-8-5-1-7"
|
||||
cx="7.1437502"
|
||||
cy="1.3229166"
|
||||
r="0.79374999"
|
||||
/>
|
||||
<circle
|
||||
style="stroke-width:0.23602"
|
||||
id="path5"
|
||||
cx="3.96875"
|
||||
cy="4.4979167"
|
||||
r="1.8520833"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<span
|
||||
class="text-brand"
|
||||
style="margin-right: 0.075em;"
|
||||
>
|
||||
tg
|
||||
</span>
|
||||
|
||||
<span class="text-brand2 text-bold">
|
||||
Crew
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.logo-component {
|
||||
svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-right: 0.125em;
|
||||
}
|
||||
|
||||
span {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.fill-brand {
|
||||
fill: $brand;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
100%,
|
||||
0% {
|
||||
fill: $brand2;
|
||||
}
|
||||
60% {
|
||||
fill: $brand2;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
#path5 {
|
||||
animation: blink 3s infinite;
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@
|
||||
<template #footer>
|
||||
<q-btn
|
||||
rounded color="primary"
|
||||
class="w100 q-mt-md q-mb-xs"
|
||||
class="w100 q-mt-md q-mb-xs fix-disabled-btn"
|
||||
:disable="!(isFormValid && (isDirty(initialMeeting, modelValue) || newFiles.length !== 0))"
|
||||
@click = "emit('update', newFiles)"
|
||||
>
|
||||
|
||||
87
src/components/pnActionBar.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="flex row q-mb-sm q-mt-md q-mx-sm justify-between items-center no-wrap">
|
||||
<q-btn
|
||||
v-if="showCalendarBtn"
|
||||
flat round
|
||||
:color="calendarActive ? 'primary' : 'grey'"
|
||||
@click="emit('toggle-calendar')"
|
||||
>
|
||||
<div>
|
||||
<q-icon name="mdi-calendar-month-outline" size="sm"/>
|
||||
<q-badge
|
||||
color="red"
|
||||
rounded
|
||||
floating
|
||||
transparent
|
||||
style="position: relative; top: -6px; margin-left: -12px"
|
||||
:style="{ opacity: calendarBadge ? 0.8 : 0 }"
|
||||
/>
|
||||
</div>
|
||||
</q-btn>
|
||||
|
||||
<q-input
|
||||
v-model="search"
|
||||
clearable
|
||||
clear-icon="close"
|
||||
borderless
|
||||
filled
|
||||
:placeholder="$t(placeholder)"
|
||||
dense
|
||||
class="col-grow q-pt-xs q-mx-sm"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" color="grey"/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-btn
|
||||
v-if="showFilterBtn"
|
||||
@click="emit('open-filters')"
|
||||
flat round
|
||||
:color="filterActive ? 'primary' : 'grey'"
|
||||
>
|
||||
<div>
|
||||
<q-icon name="mdi-filter-outline" size="sm"/>
|
||||
<q-badge
|
||||
color="red"
|
||||
rounded
|
||||
floating
|
||||
transparent
|
||||
style="position: relative; top: -6px; margin-left: -12px"
|
||||
:style="{ opacity: filterBadge ? 0.8 : 0 }"
|
||||
/>
|
||||
</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const search = defineModel<string>({
|
||||
required: true,
|
||||
default: ''
|
||||
})
|
||||
|
||||
defineProps({
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Search...'
|
||||
},
|
||||
showCalendarBtn: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showFilterBtn: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
calendarActive: Boolean,
|
||||
filterActive: Boolean,
|
||||
calendarBadge: Boolean,
|
||||
filterBadge: Boolean
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'toggle-calendar',
|
||||
'open-filters'
|
||||
])
|
||||
</script>
|
||||
125
src/components/pnItemBtmDialog.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<q-item
|
||||
@click="showDialog=true"
|
||||
clickable
|
||||
v-ripple
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar
|
||||
rounded
|
||||
text-color="white"
|
||||
:icon
|
||||
:color="iconColor"
|
||||
size="lg"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
{{ $t(title) }}
|
||||
</q-item-label>
|
||||
<q-item-label caption>
|
||||
<slot name="value"/>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="mdi-chevron-right" color="grey"/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-dialog
|
||||
v-model="showDialog"
|
||||
maximized
|
||||
transition-show="slide-up"
|
||||
transition-hide="slide-down"
|
||||
position="bottom"
|
||||
>
|
||||
<q-card
|
||||
class="fix-card-width flex column no-scroll no-wrap q-px-none"
|
||||
style="
|
||||
border-top-left-radius: var(--top-raduis) !important;
|
||||
border-top-right-radius: var(--top-raduis) !important;
|
||||
"
|
||||
>
|
||||
<div
|
||||
ref="cardHeaderRef"
|
||||
class="flex items-center no-wrap justify-between w100 q-my-none q-pa-md"
|
||||
>
|
||||
<div>
|
||||
<div class="flex column q-mx-xs">
|
||||
<span class="text-h6 ellipsis">{{ $t(title) }}</span>
|
||||
<span v-if="caption" class="text-grey text-caption">{{ $t(caption) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between no-wrap">
|
||||
<q-btn
|
||||
icon="mdi-close"
|
||||
@click="showDialog=false"
|
||||
flat round
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="cardBodyRef"
|
||||
class="q-px-none q-ma-none"
|
||||
>
|
||||
<pn-shadow-scroll
|
||||
:hideShadows="false"
|
||||
:height="bodyHeight"
|
||||
>
|
||||
<div ref="cardBodyInnerRef" class="q-px-md q-ma-none">
|
||||
<q-resize-observer @resize="updateDimensions" />
|
||||
<slot/>
|
||||
</div>
|
||||
</pn-shadow-scroll>
|
||||
</div>
|
||||
<div
|
||||
ref="cardFooterRef"
|
||||
class="q-pa-md"
|
||||
>
|
||||
<slot name="footer"/>
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useSlots } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
caption?: string
|
||||
icon: string
|
||||
iconColor: string
|
||||
}>()
|
||||
|
||||
const showDialog=ref<boolean>(false)
|
||||
|
||||
const slots = useSlots()
|
||||
const cardHeaderRef = ref<HTMLElement | null>(null)
|
||||
const cardFooterRef = ref<HTMLElement | null>(null)
|
||||
const cardBodyRef = ref<HTMLElement | null>(null)
|
||||
const cardBodyInnerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const headerHeight = ref(0)
|
||||
const footerHeight = ref(0)
|
||||
const bodyInnerHeight = ref(0)
|
||||
const bodyHeight = ref(0)
|
||||
|
||||
const updateDimensions = () => {
|
||||
headerHeight.value = cardHeaderRef.value?.offsetHeight || 0
|
||||
footerHeight.value = cardFooterRef.value?.offsetHeight || 0
|
||||
bodyInnerHeight.value = cardBodyInnerRef.value?.offsetHeight || 0
|
||||
bodyHeight.value = window.innerHeight - headerHeight.value - footerHeight.value - 48
|
||||
}
|
||||
|
||||
watch(() => slots.body?.(), updateDimensions, { flush: 'post' })
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fix-card-width {
|
||||
width: var(--body-width) !important;
|
||||
}
|
||||
</style>
|
||||
53
src/components/pnListSelector.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<q-list class="q-gutter-y-sm">
|
||||
<q-btn
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
flat
|
||||
no-caps dense
|
||||
class="w100"
|
||||
align="left"
|
||||
@click="selectItem(option)"
|
||||
:class="isSelected(option.value) ? 'text-primary' : ''"
|
||||
>
|
||||
<q-icon
|
||||
:name="isSelected(option.value) ? 'mdi-check' : ''"
|
||||
color="primary"
|
||||
class="q-pr-sm"
|
||||
/>
|
||||
<span
|
||||
:class="!isSelected(option.value) ? 'text-weight-regular' : ''"
|
||||
>
|
||||
{{ $te(option.label) ? $t(option.label) : option.label }}
|
||||
</span>
|
||||
</q-btn>
|
||||
</q-list>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface ListOption {
|
||||
label: string
|
||||
value: string | number
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
options: ListOption[]
|
||||
}>()
|
||||
|
||||
const model = defineModel<string | number | null>({
|
||||
required: true
|
||||
})
|
||||
|
||||
const isSelected = computed(() => (value: string | number) => {
|
||||
return model.value === value
|
||||
})
|
||||
|
||||
const selectItem = (option: ListOption) => {
|
||||
model.value = option.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@
|
||||
<template #footer>
|
||||
<q-btn
|
||||
rounded color="primary"
|
||||
class="w100 q-mt-md q-mb-xs"
|
||||
class="w100 q-mt-md q-mb-xs fix-disabled-btn"
|
||||
@click = "emit('update', newFiles)"
|
||||
:disable="!(isFormValid && (isDirty(initialTask, modelValue) || newFiles.length !== 0))"
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
class="text-caption flex items-center w100"
|
||||
:class="'text-' + taskStatus.color"
|
||||
>
|
||||
<q-icon :name="taskStatus.icon"/>
|
||||
<q-icon :name="taskStatus.icon" class="q-pr-xs"/>
|
||||
<span>
|
||||
{{ $t(taskStatus.text) }}
|
||||
</span>
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
// app global css in SCSS form
|
||||
.text-brand {
|
||||
color: $green-14 !important;
|
||||
color: $brand !important;
|
||||
}
|
||||
|
||||
.bg-brand {
|
||||
background: $green-14 !important;
|
||||
background: $brand !important;
|
||||
}
|
||||
|
||||
.text-brand2 {
|
||||
color: $brand2 !important;
|
||||
}
|
||||
|
||||
.bg-brand2 {
|
||||
background: $brand2 !important;
|
||||
}
|
||||
|
||||
$base-width: 100;
|
||||
@@ -13,10 +21,14 @@ $base-width: 100;
|
||||
$base-width: $base-width - 10;
|
||||
}
|
||||
|
||||
$base-height: 100;
|
||||
@while $base-height > 0 {
|
||||
.h#{$base-height} { height: #{$base-height}+'%'; }
|
||||
$base-height: $base-height - 10;
|
||||
body, html, #q-app {
|
||||
font-family: $typography-font-family;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
:root {
|
||||
@@ -66,6 +78,20 @@ body {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'myFont';
|
||||
src: url(./fonts/Inter-Regular.woff2);
|
||||
}
|
||||
|
||||
.fix-disabled-btn.q-btn[disabled]:not(.q-btn--flat) {
|
||||
background-color: $grey-5 !important;
|
||||
}
|
||||
|
||||
.fix-disabled-btn.q-btn.q-btn--flat[disabled] {
|
||||
color: $grey-9 !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.pn-icon {
|
||||
font-family: 'pn-icon';
|
||||
font-style: normal;
|
||||
@@ -73,7 +99,7 @@ body {
|
||||
|
||||
@font-face {
|
||||
font-family: 'pn-icon';
|
||||
src: url("./fonts/pn.woff") format("woff")
|
||||
src: url(./fonts/pn.woff) format("woff")
|
||||
}
|
||||
|
||||
.icon-file-default:before {
|
||||
|
||||
BIN
src/css/fonts/Inter-Regular.woff2
Normal file
@@ -1,18 +1,4 @@
|
||||
// Quasar SCSS (& Sass) Variables
|
||||
// --------------------------------------------------
|
||||
// To customize the look and feel of this app, you can override
|
||||
// the Sass/SCSS variables found in Quasar's source Sass/SCSS files.
|
||||
|
||||
// Check documentation for full list of Quasar variables
|
||||
|
||||
// Your own variables (that are declared here) and Quasar's own
|
||||
// ones will be available out of the box in your .vue/.scss/.sass files
|
||||
|
||||
// It's highly recommended to change the default colors
|
||||
// to match your app's branding.
|
||||
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
||||
|
||||
$primary : #1976D2;
|
||||
$primary : #27A7E7;
|
||||
$secondary : #26A69A;
|
||||
$accent : #9C27B0;
|
||||
|
||||
@@ -26,4 +12,9 @@ $warning : #F2C037;
|
||||
|
||||
$lightgrey : #DCDCDC;
|
||||
|
||||
$body-font-size: var(--dynamic-font-size)
|
||||
$brand: #27A7E7;
|
||||
$brand2: #F36D3A;
|
||||
|
||||
$body-font-size: var(--dynamic-font-size);
|
||||
|
||||
$typography-font-family: 'myFont', Roboto !default;
|
||||
|
||||
@@ -92,7 +92,25 @@ function parseIntString (s: string | string[] | undefined) :number | null {
|
||||
return regex.test(s) ? Number(s) : null
|
||||
}
|
||||
|
||||
function parseStartParams (input: string): { id: number; taskId?: number; meetingId?: number } | null {
|
||||
const pattern = /^p(?<id>\d+)(?:t(?<taskId>\d+))?(?:m(?<meetingId>\d+))?$/
|
||||
const match = input.match(pattern)
|
||||
|
||||
if (!match?.groups?.id) return null
|
||||
|
||||
const id = parseInt(match.groups.id, 10)
|
||||
const taskId = match.groups.taskId ? parseInt(match.groups.taskId, 10) : undefined
|
||||
const meetingId = match.groups.meetingId ? parseInt(match.groups.meetingId, 10) : undefined
|
||||
|
||||
return {
|
||||
id,
|
||||
...(taskId !== undefined && { taskId }),
|
||||
...(meetingId !== undefined && { meetingId })
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
isDirty,
|
||||
parseIntString
|
||||
parseIntString,
|
||||
parseStartParams
|
||||
}
|
||||
149
src/pages/AcceptTermsPage.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div class="flex column w100 q-pa-none q-ma-none no-wrap" style="height: 100vh">
|
||||
<div class="flex justify-center items-center w100 q-pb-md">
|
||||
{{'👋 '+ $t('accept_terms__welcome_title')}}
|
||||
<base-logo class="q-pa-xs"/>
|
||||
!
|
||||
</div>
|
||||
|
||||
<div class="col-grow">
|
||||
<q-resize-observer @resize="onResize"/>
|
||||
<pn-shadow-scroll :height="sectionHeight" :hideShadows="false">
|
||||
<div class="q-px-md text-caption">
|
||||
<div>
|
||||
{{$t('accept_terms__welcome')}}
|
||||
</div>
|
||||
|
||||
<div class="text-bold q-pt-md">
|
||||
{{$t('accept_terms__section_privacy_data_title')}}
|
||||
</div>
|
||||
<div>
|
||||
{{$t('accept_terms__section_privacy_data')}}
|
||||
</div>
|
||||
<ul>
|
||||
<li> {{$t('accept_terms__section_privacy_data_option1')}}</li>
|
||||
<li> {{$t('accept_terms__section_privacy_data_option2')}}</li>
|
||||
<li> {{$t('accept_terms__section_privacy_data_option3')}}</li>
|
||||
<li> {{$t('accept_terms__section_privacy_data_option4')}}</li>
|
||||
</ul>
|
||||
|
||||
<div class="text-bold q-pt-md">
|
||||
{{$t('accept_terms__section_choise_title')}}
|
||||
</div>
|
||||
<div>
|
||||
{{$t('accept_terms__section_choise_agree')}}
|
||||
</div>
|
||||
<ul>
|
||||
<li> {{$t('accept_terms__section_choise_agree_option1')}}</li>
|
||||
<li> {{$t('accept_terms__section_choise_agree_option2')}}</li>
|
||||
</ul>
|
||||
<div>
|
||||
{{$t('accept_terms__section_choise_decline')}}
|
||||
</div>
|
||||
<ul>
|
||||
<li> {{$t('accept_terms__section_choise_decline_option1')}}</li>
|
||||
<li> {{$t('accept_terms__section_choise_decline_option2')}}</li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
{{$t('accept_terms__section_revoke')}}
|
||||
</div>
|
||||
</div>
|
||||
</pn-shadow-scroll>
|
||||
</div>
|
||||
|
||||
<div class="flex column w100 q-pt-md">
|
||||
<div class="text-caption q-pb-md">
|
||||
<div class="flex column q-gutter-y-md">
|
||||
<div class="flex items-center no-wrap">
|
||||
<q-checkbox v-model="agreement" val="1" dense class="q-px-sm"/>
|
||||
<span>
|
||||
{{$t('accept_terms__section_checkbox_agreement') + ' '}}
|
||||
<a href="">{{ $t('accept_terms__section_checkbox_agreement_doc') }}</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center no-wrap">
|
||||
<q-checkbox v-model="agreement" val="2" dense class="q-px-sm"/>
|
||||
<span>
|
||||
{{$t('accept_terms__section_checkbox_privacy') + ' '}}
|
||||
<a href="">{{ $t('accept_terms__section_checkbox_privacy_doc') }}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex no-wrap justify-center w100 q-gutter-x-md">
|
||||
<q-btn
|
||||
flat
|
||||
color="negative"
|
||||
no-caps
|
||||
style="opacity: 0.8"
|
||||
@click="onDecline"
|
||||
class="w40"
|
||||
>
|
||||
{{$t('accept_terms__btn_decline')}}
|
||||
</q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
color="primary"
|
||||
no-caps
|
||||
:disable="agreement.length !== 2"
|
||||
@click="onSubmit"
|
||||
class="w40 fix-disabled"
|
||||
>
|
||||
{{$t('accept_terms__btn_agree')}}
|
||||
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject } from 'vue'
|
||||
import { parseStartParams } from 'helpers/helpers'
|
||||
import BaseLogo from 'components/BaseLogo.vue'
|
||||
import { useAuthStore } from 'stores/auth'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { WebApp } from '@twa-dev/types'
|
||||
const tg = inject('tg') as WebApp
|
||||
|
||||
function onDecline () {
|
||||
tg.close()
|
||||
}
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
const startParams = typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.start_param
|
||||
? parseStartParams(window.Telegram.WebApp.initDataUnsafe.start_param)
|
||||
: null
|
||||
|
||||
async function onSubmit () {
|
||||
await authStore.termsAccepted()
|
||||
const route = startParams === null
|
||||
? { name: '404' }
|
||||
: startParams?.taskId
|
||||
? { name: 'task_info', params: { id: startParams.id, taskId: startParams.taskId }}
|
||||
: startParams?.meetingId
|
||||
? { name: 'meeting_info', params: { id: startParams.id, meetingId: startParams.meetingId }}
|
||||
: { name: 'files', params: { id: startParams.id }}
|
||||
await router.push(route)
|
||||
}
|
||||
|
||||
const agreement = ref([])
|
||||
|
||||
interface sizeParams {
|
||||
height: number,
|
||||
width: number
|
||||
}
|
||||
|
||||
const sectionHeight = ref(0)
|
||||
function onResize(size: sizeParams) {
|
||||
sectionHeight.value = size.height
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -3,78 +3,70 @@
|
||||
<template #title>
|
||||
{{ $t('settings__title') }}
|
||||
</template>
|
||||
|
||||
<pn-scroll-list>
|
||||
<q-list separator>
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-avatar color="primary" rounded text-color="white" icon="mdi-translate" size="md" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<span>{{ $t('settings__language') }}</span>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-select
|
||||
class="fix-input-right text-body1"
|
||||
v-model="locale"
|
||||
:options="localeOptions"
|
||||
dense
|
||||
borderless
|
||||
emit-value
|
||||
map-options
|
||||
hide-bottom-space
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-avatar color="primary" rounded text-color="white" icon="mdi-format-size" size="md" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<span>{{ $t('settings__font_size') }}</span>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<div class="flex justify-end">
|
||||
<q-btn
|
||||
@click="settingsStore.decreaseFontSize()"
|
||||
color="negative" flat
|
||||
icon="mdi-format-font-size-decrease"
|
||||
class="q-pa-sm q-mx-xs"
|
||||
:disable="!settingsStore.canDecrease"
|
||||
/>
|
||||
<q-btn
|
||||
@click="settingsStore.increaseFontSize()"
|
||||
color="positive" flat
|
||||
icon="mdi-format-font-size-increase"
|
||||
class="q-pa-sm q-mx-xs"
|
||||
:disable="!settingsStore.canIncrease"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<pn-item-btm-dialog
|
||||
title="settings__language"
|
||||
icon="mdi-translate"
|
||||
iconColor="primary"
|
||||
>
|
||||
<template #value>
|
||||
{{ localeOptions.find(el => el.value === locale)?.label }}
|
||||
</template>
|
||||
<pn-list-selector
|
||||
v-model="locale"
|
||||
:options="localeOptions"
|
||||
/>
|
||||
</pn-item-btm-dialog>
|
||||
|
||||
<pn-item-btm-dialog
|
||||
title="settings__font_size"
|
||||
icon="mdi-format-size"
|
||||
iconColor="primary"
|
||||
>
|
||||
<template #value>
|
||||
{{ $t(fontSizeLabel) }}
|
||||
</template>
|
||||
<pn-list-selector
|
||||
v-model="fontSize"
|
||||
:options="fontSizeOptions"
|
||||
/>
|
||||
</pn-item-btm-dialog>
|
||||
</q-list>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, watch, ref, onMounted } from 'vue'
|
||||
import { useSettingsStore } from 'stores/settings'
|
||||
import pnItemBtmDialog from 'components/pnItemBtmDialog.vue'
|
||||
import pnListSelector from 'components/pnListSelector.vue'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const locale = ref('')
|
||||
const localeOptions = settingsStore.supportLocale
|
||||
|
||||
const locale = computed({
|
||||
get: () => settingsStore.settings.locale,
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
set: (value: string) => settingsStore.updateLocale(value)
|
||||
watch(locale, async (newValue) => {
|
||||
await settingsStore.updateSettings({ locale: newValue })
|
||||
})
|
||||
|
||||
const fontSize = ref(16)
|
||||
const fontSizeOptions = settingsStore.supportFontSizes
|
||||
const fontSizeLabel = computed(() =>
|
||||
fontSizeOptions.find(el => el.value === fontSize.value)?.label ?? ''
|
||||
)
|
||||
|
||||
watch(fontSize, async (newValue) => {
|
||||
await settingsStore.updateSettings({ fontSize: newValue })
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
locale.value = settingsStore.settings.locale
|
||||
fontSize.value = settingsStore.settings.fontSize
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fix-input-right :deep(.q-field__native) {
|
||||
justify-content: end;
|
||||
}
|
||||
</style>
|
||||
@@ -45,11 +45,15 @@
|
||||
readonly
|
||||
filled
|
||||
class="w100"
|
||||
:label = "$t('user_card__' + key)"
|
||||
:label = "(key!=='email' && key!=='phone') ? $t('user_card__' + key) : undefined"
|
||||
>
|
||||
<template #control>
|
||||
{{displayUser[key]}}
|
||||
</template>
|
||||
<template #prepend v-if="(key==='email' || key==='phone')">
|
||||
<q-icon v-if="key==='email'" name="mdi-email-outline"/>
|
||||
<q-icon v-if="key==='phone'" name="mdi-phone-outline"/>
|
||||
</template>
|
||||
</q-field>
|
||||
</div>
|
||||
|
||||
@@ -73,15 +77,13 @@
|
||||
const userId = parseIntString(route.params.userId)
|
||||
const user = computed(() => userId && usersStore.userById(userId))
|
||||
|
||||
const tname = computed(() => {
|
||||
return (!user.value)
|
||||
? ''
|
||||
: user.value.firstname
|
||||
? user.value.lastname
|
||||
? user.value.firstname + ' ' + user.value.lastname
|
||||
: user.value.firstname
|
||||
: user.value.lastname ?? ''
|
||||
})
|
||||
const tname = computed(() =>
|
||||
user.value
|
||||
? [user.value?.firstname, user.value?.lastname]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
: ''
|
||||
)
|
||||
|
||||
const userPosition = computed(() => {
|
||||
return (!user.value)
|
||||
|
||||
@@ -2,21 +2,12 @@
|
||||
<div class="q-pa-none flex column col-grow no-scroll">
|
||||
<pn-scroll-list>
|
||||
<template #card-body-header>
|
||||
|
||||
<div class="flex row q-ma-md justify-between">
|
||||
<q-input
|
||||
v-model="search"
|
||||
clearable
|
||||
clear-icon="close"
|
||||
:placeholder="$t('chats__search')"
|
||||
dense
|
||||
class="col-grow"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
<pn-action-bar
|
||||
v-model="search"
|
||||
placeholder="chats__search"
|
||||
:show-filter-btn="false"
|
||||
:show-calendar-btn="false"
|
||||
/>
|
||||
</template>
|
||||
<q-list separator>
|
||||
<q-item
|
||||
@@ -59,6 +50,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject } from 'vue'
|
||||
import { useChatsStore } from 'stores/chats'
|
||||
import pnActionBar from 'components/pnActionBar.vue'
|
||||
import type { WebApp } from '@twa-dev/types'
|
||||
const tg = inject('tg') as WebApp
|
||||
|
||||
|
||||
@@ -2,60 +2,16 @@
|
||||
<div class="q-pa-none flex column col-grow no-scroll">
|
||||
<pn-scroll-list>
|
||||
<template #card-body-header>
|
||||
|
||||
<div class="flex row q-mb-xs q-mt-md q-mx-sm justify-between">
|
||||
<q-btn
|
||||
icon="mdi-calendar-month-outline"
|
||||
flat dense round
|
||||
class="q-mr-sm"
|
||||
size="lg"
|
||||
:color="showCalendar ? 'primary' : 'grey'"
|
||||
@click="showCalendar = !showCalendar"
|
||||
>
|
||||
<div>
|
||||
<q-badge
|
||||
color="red"
|
||||
rounded
|
||||
floating
|
||||
transparent
|
||||
style="position: relative; top: -16px; margin-left: -12px"
|
||||
:style="{ opacity: datesRange ? 0.8 : 0 }"
|
||||
/>
|
||||
</div>
|
||||
</q-btn>
|
||||
<q-input
|
||||
v-model="search"
|
||||
clearable
|
||||
clear-icon="close"
|
||||
:placeholder="$t('files__search')"
|
||||
dense
|
||||
class="col-grow"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-btn
|
||||
@click="showFiltersDialog = true"
|
||||
icon="mdi-filter-outline"
|
||||
dense round flat
|
||||
size="lg"
|
||||
:color="showFiltersDialog ? 'primary' : 'grey'"
|
||||
class="q-mr-xs"
|
||||
>
|
||||
<div>
|
||||
<q-badge
|
||||
color="red"
|
||||
rounded
|
||||
floating
|
||||
transparent
|
||||
style="position: relative; top: -16px; margin-left: -12px"
|
||||
:style="{ opacity: !checkFiltersSelect ? 0.8 : 0 }"
|
||||
/>
|
||||
</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
<pn-action-bar
|
||||
v-model="search"
|
||||
placeholder="files__search"
|
||||
:calendar-active="showCalendar"
|
||||
:filter-active="showFiltersDialog"
|
||||
:calendar-badge="!!datesRange"
|
||||
:filter-badge="!checkFiltersSelect"
|
||||
@toggle-calendar="showCalendar = !showCalendar"
|
||||
@open-filters="showFiltersDialog = true"
|
||||
/>
|
||||
<q-slide-transition>
|
||||
<div v-show="showCalendar">
|
||||
<q-date
|
||||
@@ -241,6 +197,7 @@
|
||||
import { useFilesStore } from 'stores/files'
|
||||
import { useUsersStore } from 'stores/users'
|
||||
import { useChatsStore } from 'stores/chats'
|
||||
import pnActionBar from 'components/pnActionBar.vue'
|
||||
import { date } from 'quasar'
|
||||
import { parseFileName, fileIcon, fileSize } from 'helpers/files-functions'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -2,41 +2,14 @@
|
||||
<div class="q-pa-none flex column col-grow no-scroll">
|
||||
<pn-scroll-list>
|
||||
<template #card-body-header>
|
||||
|
||||
<div class="flex row q-mb-xs q-mt-md q-mx-sm justify-between">
|
||||
<q-btn
|
||||
icon="mdi-calendar-month-outline"
|
||||
flat dense round
|
||||
class="q-mr-sm"
|
||||
size="lg"
|
||||
:color="showCalendar ? 'primary' : 'grey'"
|
||||
@click="showCalendar = !showCalendar"
|
||||
>
|
||||
<div>
|
||||
<q-badge
|
||||
color="red"
|
||||
rounded
|
||||
floating
|
||||
transparent
|
||||
style="position: relative; top: -16px; margin-left: -12px"
|
||||
:style="{ opacity: datesRange ? 0.8 : 0 }"
|
||||
/>
|
||||
</div>
|
||||
</q-btn>
|
||||
<q-input
|
||||
v-model="search"
|
||||
clearable
|
||||
clear-icon="close"
|
||||
:placeholder="$t('meetings__search')"
|
||||
dense
|
||||
class="col-grow"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<pn-action-bar
|
||||
v-model="search"
|
||||
placeholder="meetings__search"
|
||||
:calendar-active="showCalendar"
|
||||
:calendar-badge="!!datesRange"
|
||||
@toggle-calendar="showCalendar = !showCalendar"
|
||||
:show-filter-btn="false"
|
||||
/>
|
||||
<q-slide-transition>
|
||||
<div v-show="showCalendar">
|
||||
<q-date
|
||||
@@ -65,7 +38,7 @@
|
||||
<q-btn flat dense no-caps v-if ="showReset" @click="resetDisplayPreviousMeetings">
|
||||
<div class="flex items-center text-caption text-grey">
|
||||
{{$t('meetings__previous_hide')}}
|
||||
<q-icon name="close" size="xs"/>
|
||||
<q-icon name="mdi-close" size="xs"/>
|
||||
</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
@@ -227,6 +200,7 @@
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { Meeting } from 'types/Meeting'
|
||||
import { date } from 'quasar'
|
||||
import pnActionBar from 'components/pnActionBar.vue'
|
||||
|
||||
const search = ref('')
|
||||
const showCalendar = ref<boolean>(false)
|
||||
|
||||
@@ -2,60 +2,16 @@
|
||||
<div class="q-pa-none flex column col-grow no-scroll">
|
||||
<pn-scroll-list>
|
||||
<template #card-body-header>
|
||||
|
||||
<div class="flex row q-mb-xs q-mt-md q-mx-sm justify-between">
|
||||
<q-btn
|
||||
icon="mdi-calendar-month-outline"
|
||||
flat dense round
|
||||
class="q-mr-sm"
|
||||
size="lg"
|
||||
:color="showCalendar ? 'primary' : 'grey'"
|
||||
@click="showCalendar = !showCalendar"
|
||||
>
|
||||
<div>
|
||||
<q-badge
|
||||
color="red"
|
||||
rounded
|
||||
floating
|
||||
transparent
|
||||
style="position: relative; top: -16px; margin-left: -12px"
|
||||
:style="{ opacity: datesRange ? 0.8 : 0 }"
|
||||
/>
|
||||
</div>
|
||||
</q-btn>
|
||||
<q-input
|
||||
v-model="search"
|
||||
clearable
|
||||
clear-icon="close"
|
||||
:placeholder="$t('tasks__search')"
|
||||
dense
|
||||
class="col-grow"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-btn
|
||||
@click="showFiltersDialog = true"
|
||||
icon="mdi-filter-outline"
|
||||
dense round flat
|
||||
size="lg"
|
||||
:color="showFiltersDialog ? 'primary' : 'grey'"
|
||||
class="q-mr-xs"
|
||||
>
|
||||
<div>
|
||||
<q-badge
|
||||
color="red"
|
||||
rounded
|
||||
floating
|
||||
transparent
|
||||
style="position: relative; top: -16px; margin-left: -12px"
|
||||
:style="{ opacity: !checkFiltersSelect ? 0.8 : 0 }"
|
||||
/>
|
||||
</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
<pn-action-bar
|
||||
v-model="search"
|
||||
placeholder="tasks__search"
|
||||
:calendar-active="showCalendar"
|
||||
:filter-active="showFiltersDialog"
|
||||
:calendar-badge="!!datesRange"
|
||||
:filter-badge="!checkFiltersSelect"
|
||||
@toggle-calendar="showCalendar = !showCalendar"
|
||||
@open-filters="showFiltersDialog = true"
|
||||
/>
|
||||
<q-slide-transition>
|
||||
<div v-show="showCalendar">
|
||||
<q-date
|
||||
@@ -236,6 +192,7 @@
|
||||
import taskItem from 'components/taskItem.vue'
|
||||
import type { Task } from 'types/Task'
|
||||
import { date } from 'quasar'
|
||||
import pnActionBar from 'components/pnActionBar.vue'
|
||||
|
||||
const search = ref('')
|
||||
const showCalendar = ref<boolean>(false)
|
||||
|
||||
@@ -2,20 +2,12 @@
|
||||
<div class="q-pa-none flex column col-grow no-scroll">
|
||||
<pn-scroll-list>
|
||||
<template #card-body-header>
|
||||
<div class="flex row q-ma-md justify-between">
|
||||
<q-input
|
||||
v-model="search"
|
||||
clearable
|
||||
clear-icon="close"
|
||||
:placeholder="$t('users__search')"
|
||||
dense
|
||||
class="col-grow"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
<pn-action-bar
|
||||
v-model="search"
|
||||
placeholder="users__search"
|
||||
:show-filter-btn="false"
|
||||
:show-calendar-btn="false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<q-list separator>
|
||||
@@ -57,6 +49,7 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUsersStore } from 'stores/users'
|
||||
import pnActionBar from 'components/pnActionBar.vue'
|
||||
import type { User } from 'types/User'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -8,45 +8,51 @@ import {
|
||||
import routes from './routes'
|
||||
import { useProjectsStore } from 'stores/projects'
|
||||
import { useAuthStore } from 'stores/auth'
|
||||
import { parseStartParams } from 'helpers/helpers'
|
||||
|
||||
export default defineRouter(function (/* { store, ssrContext } */) {
|
||||
const createHistory = process.env.SERVER
|
||||
? createMemoryHistory
|
||||
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
|
||||
|
||||
const Router = createRouter({
|
||||
const startRouteInfo = typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.start_param
|
||||
? parseStartParams(window.Telegram.WebApp.initDataUnsafe.start_param)
|
||||
: null
|
||||
|
||||
const Router = createRouter({
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
routes,
|
||||
|
||||
// Leave this as is and make changes in quasar.conf.js instead!
|
||||
// quasar.conf.js -> build -> vueRouterMode
|
||||
// quasar.conf.js -> build -> publicPath
|
||||
history: createHistory(process.env.VUE_ROUTER_BASE),
|
||||
history: createHistory(process.env.VUE_ROUTER_BASE)
|
||||
})
|
||||
|
||||
Router.beforeEach(async (to) => {
|
||||
|
||||
if (to.name === 'settings') return
|
||||
if (to.name === '404') return
|
||||
|
||||
const authStore = useAuthStore()
|
||||
console.log(window.Telegram.WebApp.initDataUnsafe.start_param, startRouteInfo)
|
||||
console.log(112, to)
|
||||
if (to.name === 'settings' || to.name === '404' || to.name === 'accept-terms') return true
|
||||
const authStore = useAuthStore()
|
||||
const projectsStore = useProjectsStore()
|
||||
|
||||
|
||||
if (authStore.startRouteInfo && to.path === '/') {
|
||||
const { id, taskId, meetingId } = authStore.startRouteInfo
|
||||
authStore.setStartRouteInfo(null)
|
||||
|
||||
if (!projectsStore.isInit) await projectsStore.init()
|
||||
const project = projectsStore.projectById(id)
|
||||
if (!authStore.isInit) await authStore.init(window.Telegram.WebApp)
|
||||
// if (!authStore.isTermsAccepted) return { name: 'accept-terms' }
|
||||
|
||||
if (!project) return { name: '404' }
|
||||
if (to.path === '/' && startRouteInfo) {
|
||||
console.log(222, startRouteInfo)
|
||||
const { id, taskId, meetingId } = startRouteInfo
|
||||
|
||||
return taskId
|
||||
? { name: 'task_info', params: { id, taskId } }
|
||||
: meetingId
|
||||
? { name: 'meeting_info', params: { id, meetingId } }
|
||||
: { name: 'files', params: { id } }
|
||||
if (!projectsStore.isInit) await projectsStore.init()
|
||||
|
||||
const project = projectsStore.projectById(id)
|
||||
if (!project) return { name: '404' }
|
||||
|
||||
return taskId
|
||||
? { name: 'task_info', params: { id, taskId } }
|
||||
: meetingId
|
||||
? { name: 'meeting_info', params: { id, meetingId } }
|
||||
: { name: 'files', params: { id } }
|
||||
}
|
||||
|
||||
if (to.params.id) {
|
||||
|
||||
@@ -85,6 +85,12 @@ const routes: RouteRecordRaw[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'accept-terms',
|
||||
path: '/accept-terms-of-use',
|
||||
component: () => import('pages/AcceptTermsPage.vue'),
|
||||
meta: { hideBackButton: true }
|
||||
},
|
||||
{
|
||||
name: '404',
|
||||
path: '/:catchAll(.*)*',
|
||||
|
||||
@@ -5,25 +5,32 @@ import type { WebApp } from '@twa-dev/types'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const isInit = ref(false)
|
||||
const isTermsAccepted = ref(false)
|
||||
const telegramUserData = ref()
|
||||
|
||||
async function init (tg: WebApp) {
|
||||
await api.post('/auth?' + tg?.initData)
|
||||
const { data } = await api.post('/auth?' + tg?.initData)
|
||||
telegramUserData.value = tg?.initDataUnsafe.user
|
||||
isTermsAccepted.value = data.data?.is_terms_accepted
|
||||
isInit.value = true
|
||||
}
|
||||
|
||||
const startRouteInfo = ref<{ id: number; taskId?: number; meetingId?: number } | null>(null)
|
||||
async function termsAccepted () {
|
||||
const { data } = await api.post('/terms/accept')
|
||||
if (data.success) isTermsAccepted.value = true
|
||||
}
|
||||
|
||||
function setStartRouteInfo (info: { id: number; taskId?: number; meetingId?: number } | null) {
|
||||
startRouteInfo.value = info
|
||||
async function termsRevoked () {
|
||||
const { data } = await api.post('/terms/revoke')
|
||||
if (data.success) isTermsAccepted.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
isInit,
|
||||
isTermsAccepted,
|
||||
telegramUserData,
|
||||
startRouteInfo,
|
||||
setStartRouteInfo,
|
||||
init
|
||||
init,
|
||||
termsAccepted,
|
||||
termsRevoked
|
||||
}
|
||||
})
|
||||
|
||||
@@ -11,9 +11,6 @@ interface AppSettings {
|
||||
}
|
||||
|
||||
const defaultFontSize = 16
|
||||
const minFontSize = 10
|
||||
const maxFontSize = 22
|
||||
const fontSizeStep = 2
|
||||
|
||||
const defaultSettings: AppSettings = {
|
||||
fontSize: defaultFontSize,
|
||||
@@ -29,14 +26,18 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
const isInit = ref(false)
|
||||
|
||||
const currentFontSize = computed(() => settings.value?.fontSize ?? defaultFontSize)
|
||||
const canIncrease = computed(() => currentFontSize.value < maxFontSize)
|
||||
const canDecrease = computed(() => currentFontSize.value > minFontSize)
|
||||
|
||||
const supportLocale = [
|
||||
{ value: 'en-US', label: 'English' },
|
||||
{ value: 'ru-RU', label: 'Русский' }
|
||||
]
|
||||
|
||||
const supportFontSizes = [
|
||||
{ value: 12, label: 'settings__fontsize_small' },
|
||||
{ value: 16, label: 'settings__fontsize_medium' },
|
||||
{ value: 20, label: 'settings__fontsize_large' }
|
||||
]
|
||||
|
||||
const quasarLangMap: Record<string, string> = {
|
||||
'en-US': 'en-US',
|
||||
'ru-RU': 'ru'
|
||||
@@ -89,32 +90,12 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const updateLocale = async (newLocale: string) => {
|
||||
if (i18nLocale) {
|
||||
i18nLocale.value = newLocale
|
||||
await updateQuasarLang(newLocale)
|
||||
settings.value.locale = newLocale
|
||||
await saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
const saveSettings = async () => {
|
||||
await api.put('/settings', { settings: settings.value })
|
||||
}
|
||||
|
||||
const updateSettings = async (newSettings: Partial<AppSettings>) => {
|
||||
settings.value = { ...settings.value, ...newSettings }
|
||||
updateCssVariable()
|
||||
await applyLocale()
|
||||
await saveSettings()
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
const { data } = await api.get('/settings')
|
||||
settings.value = {
|
||||
fontSize: data.data.settings.fontSize || defaultSettings.fontSize,
|
||||
locale: data.data.settings.locale || detectLocale()
|
||||
fontSize: data.data.fontSize || defaultSettings.fontSize,
|
||||
locale: data.data.locale || detectLocale()
|
||||
}
|
||||
} catch {
|
||||
settings.value.locale = detectLocale()
|
||||
@@ -124,30 +105,25 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
isInit.value = true
|
||||
}
|
||||
|
||||
const clampFontSize = (size: number) =>
|
||||
Math.max(minFontSize, Math.min(size, maxFontSize))
|
||||
|
||||
const increaseFontSize = async () => {
|
||||
const newSize = clampFontSize(currentFontSize.value + fontSizeStep)
|
||||
await updateSettings({ fontSize: newSize })
|
||||
const saveSettings = async () => {
|
||||
await api.put('/settings', settings.value)
|
||||
}
|
||||
|
||||
const decreaseFontSize = async () => {
|
||||
const newSize = clampFontSize(currentFontSize.value - fontSizeStep)
|
||||
await updateSettings({ fontSize: newSize })
|
||||
const updateSettings = async (newSettings: Partial<AppSettings>) => {
|
||||
settings.value = { ...settings.value, ...newSettings }
|
||||
updateCssVariable()
|
||||
await applyLocale()
|
||||
await saveSettings()
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
settings,
|
||||
supportLocale,
|
||||
supportFontSizes,
|
||||
isInit,
|
||||
currentFontSize,
|
||||
canIncrease,
|
||||
canDecrease,
|
||||
init,
|
||||
increaseFontSize,
|
||||
decreaseFontSize,
|
||||
updateSettings,
|
||||
updateLocale
|
||||
updateSettings
|
||||
}
|
||||
})
|
||||
|
||||