mirror of
https://git.hmsn.ink/kospo/svcm/oa.git
synced 2026-03-20 17:03:42 +09:00
first
This commit is contained in:
207
src/composables/credit-card.ts
Normal file
207
src/composables/credit-card.ts
Normal file
File diff suppressed because one or more lines are too long
106
src/composables/darkmode.ts
Normal file
106
src/composables/darkmode.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { Plugin } from 'vue'
|
||||
|
||||
interface DarkmodeContext {
|
||||
isDark: Ref<boolean>
|
||||
onChange: (event: MouseEvent) => Promise<void>
|
||||
}
|
||||
type DarkModeSchema = 'auto' | 'dark' | 'light'
|
||||
|
||||
const darkmodeClass = 'is-dark'
|
||||
const injectionKey = Symbol('darkmode') as InjectionKey<DarkmodeContext>
|
||||
|
||||
export const useDarkmode = () => {
|
||||
return inject(injectionKey, {
|
||||
// default values to prevent inject errors
|
||||
isDark: ref(false),
|
||||
onChange: async () => {},
|
||||
})
|
||||
}
|
||||
|
||||
export function createDarkmode(): Plugin {
|
||||
return {
|
||||
install(app) {
|
||||
const preferredDark = usePreferredDark()
|
||||
const colorSchema = useStorage<DarkModeSchema>('color-schema', 'auto')
|
||||
|
||||
const enableTransitions = () =>
|
||||
!import.meta.env.SSR
|
||||
&& 'startViewTransition' in document
|
||||
&& window.matchMedia('(prefers-reduced-motion: no-preference)').matches
|
||||
|
||||
const isDark = computed({
|
||||
get() {
|
||||
return colorSchema.value === 'auto'
|
||||
? preferredDark.value
|
||||
: colorSchema.value === 'dark'
|
||||
},
|
||||
set(v: boolean) {
|
||||
// disable transitions
|
||||
if (!import.meta.env.SSR && document.documentElement) {
|
||||
document.documentElement.classList.add('no-transition')
|
||||
}
|
||||
|
||||
if (v === preferredDark.value) colorSchema.value = 'auto'
|
||||
else colorSchema.value = v ? 'dark' : 'light'
|
||||
|
||||
if (!import.meta.env.SSR && document.documentElement) {
|
||||
setTimeout(() => {
|
||||
document.documentElement.classList.remove('no-transition')
|
||||
}, 0)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const onChange = async (event: MouseEvent) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
|
||||
if (!enableTransitions()) {
|
||||
isDark.value = !isDark.value
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
const clipPath = [
|
||||
`circle(0px at ${event.clientX}px ${event.clientY}px)`,
|
||||
`circle(${Math.hypot(
|
||||
Math.max(event.clientX, target.clientWidth - event.clientX),
|
||||
Math.max(event.clientY, target.clientHeight - event.clientY),
|
||||
)}px at ${event.clientX}px ${event.clientY}px)`,
|
||||
]
|
||||
|
||||
await (document as any).startViewTransition(async () => {
|
||||
isDark.value = !isDark.value
|
||||
await nextTick()
|
||||
}).ready
|
||||
|
||||
document.documentElement.animate(
|
||||
{ clipPath: isDark.value ? clipPath.reverse() : clipPath },
|
||||
{
|
||||
duration: 300,
|
||||
easing: 'ease-in',
|
||||
pseudoElement: `::view-transition-${isDark.value ? 'old' : 'new'}(root)`,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (!import.meta.env.SSR) {
|
||||
watch(isDark, (value) => {
|
||||
const body = document.documentElement
|
||||
|
||||
if (value) {
|
||||
body.classList.add(darkmodeClass)
|
||||
}
|
||||
else {
|
||||
body.classList.remove(darkmodeClass)
|
||||
}
|
||||
}, { immediate: true })
|
||||
}
|
||||
|
||||
app.provide(injectionKey, {
|
||||
isDark,
|
||||
onChange,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
40
src/composables/dropdown.ts
Normal file
40
src/composables/dropdown.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
export interface DropdownOptions {
|
||||
clickOutside?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate refs to handle a dropdown state
|
||||
*/
|
||||
export function useDropdownContext(
|
||||
target: Ref<HTMLElement | undefined>,
|
||||
options: DropdownOptions = { clickOutside: true },
|
||||
) {
|
||||
const isOpen = ref(false)
|
||||
|
||||
if (options.clickOutside) {
|
||||
onClickOutside(target, () => {
|
||||
isOpen.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const open = () => {
|
||||
isOpen.value = true
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
const toggle = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
|
||||
return reactive({
|
||||
isOpen,
|
||||
open,
|
||||
close,
|
||||
toggle,
|
||||
})
|
||||
}
|
||||
20
src/composables/fetch.ts
Normal file
20
src/composables/fetch.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { H3Event } from 'h3'
|
||||
import { ofetch } from 'ofetch'
|
||||
|
||||
export function useApiFetch(event?: H3Event) {
|
||||
const token = useUserToken(event)
|
||||
|
||||
return ofetch.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8080',
|
||||
// We set an interceptor for each request to
|
||||
// include Bearer token to the request if user is logged in
|
||||
onRequest: ({ options }) => {
|
||||
if (token.value) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
Authorization: `Bearer ${token.value}`,
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
62
src/composables/field-context.ts
Normal file
62
src/composables/field-context.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { MaybeRefOrGetter, InjectionKey } from 'vue'
|
||||
import { useField, type FieldContext } from 'vee-validate'
|
||||
import { defu } from 'defu'
|
||||
|
||||
export type VFieldContext = ReturnType<typeof createVFieldContext>
|
||||
export const useVFieldSymbolContext = Symbol() as InjectionKey<VFieldContext>
|
||||
|
||||
function createVFieldContext<TValue = unknown>(id?: MaybeRefOrGetter<string>) {
|
||||
const internal = ref(toValue(id))
|
||||
const field = ref<FieldContext<TValue>>()
|
||||
|
||||
watch(
|
||||
() => toValue(id),
|
||||
(value) => {
|
||||
internal.value = value || `v-field-${crypto.randomUUID()}`
|
||||
},
|
||||
)
|
||||
|
||||
if (id) {
|
||||
field.value = useField(id)
|
||||
}
|
||||
|
||||
const vFieldContext = {
|
||||
id: internal,
|
||||
field,
|
||||
}
|
||||
|
||||
provide(useVFieldSymbolContext, vFieldContext)
|
||||
|
||||
return vFieldContext
|
||||
}
|
||||
|
||||
interface VFieldContextOption {
|
||||
id?: MaybeRefOrGetter<string>
|
||||
create?: MaybeRefOrGetter<boolean>
|
||||
inherit?: MaybeRefOrGetter<boolean>
|
||||
help?: MaybeRefOrGetter<string>
|
||||
}
|
||||
|
||||
export function useVFieldContext(options = {} as VFieldContextOption) {
|
||||
const _options = defu(options, {
|
||||
create: true,
|
||||
inherit: true,
|
||||
})
|
||||
|
||||
if (unref(_options.inherit)) {
|
||||
const vFieldContext = inject(useVFieldSymbolContext, undefined)
|
||||
if (vFieldContext) {
|
||||
return vFieldContext
|
||||
}
|
||||
}
|
||||
|
||||
const _help = unref(_options.help) ? unref(_options.help) + ': ' : ''
|
||||
|
||||
if (!unref(_options.create)) {
|
||||
throw new Error(
|
||||
`${_help}useVFieldContext (create = false) must be used inside a VField component`,
|
||||
)
|
||||
}
|
||||
|
||||
return createVFieldContext(_options.id)
|
||||
}
|
||||
59
src/composables/image-error.ts
Normal file
59
src/composables/image-error.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
type UseImageErrorOptions = {
|
||||
fallback?: string
|
||||
} | {
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
export function useImageError(target?: Ref<HTMLImageElement | null>, options: UseImageErrorOptions = {}) {
|
||||
const handler = (event: Event) => {
|
||||
if ('fallback' in options) {
|
||||
onceError(event, options.fallback)
|
||||
}
|
||||
else if ('width' in options) {
|
||||
onceError(event, options.width, options.height)
|
||||
}
|
||||
}
|
||||
const cleanup = () => {
|
||||
if (!target?.value) return
|
||||
target.value?.removeEventListener('error', handler)
|
||||
}
|
||||
|
||||
const stopWatch = watchEffect(() => {
|
||||
if (!target?.value) return
|
||||
cleanup()
|
||||
|
||||
target.value?.addEventListener('error', handler, {
|
||||
once: true,
|
||||
passive: true,
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopWatch()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
return {
|
||||
onceError,
|
||||
}
|
||||
}
|
||||
|
||||
function onceError(event: Event, fallback?: string): void
|
||||
function onceError(event: Event, width?: number, height?: number): void
|
||||
function onceError(event: Event, width?: number | string, height?: number): void {
|
||||
const target = event.target as HTMLImageElement
|
||||
if (!target || !width) return
|
||||
|
||||
let src: string
|
||||
if (typeof width === 'string') {
|
||||
src = width
|
||||
}
|
||||
else {
|
||||
src = `https://via.placeholder.com/${width}x${height ?? width}`
|
||||
}
|
||||
|
||||
target.src = src
|
||||
}
|
||||
38
src/composables/markdown-toc.ts
Normal file
38
src/composables/markdown-toc.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
const HEADER_SELECTORS = [
|
||||
'h1[id]',
|
||||
'h2[id]',
|
||||
'h3[id]',
|
||||
'h4[id]',
|
||||
'h5[id]',
|
||||
'h6[id]',
|
||||
'a[name]',
|
||||
]
|
||||
|
||||
export type TocItem = {
|
||||
id: string
|
||||
title: string
|
||||
level: number
|
||||
}
|
||||
|
||||
export function useMarkdownToc(target: Ref<HTMLElement | undefined>) {
|
||||
const toc = ref<TocItem[]>([])
|
||||
|
||||
watchEffect(() => {
|
||||
if (target.value) {
|
||||
const anchors = target.value.querySelectorAll(HEADER_SELECTORS.join(', '))
|
||||
anchors.forEach((anchor) => {
|
||||
if (anchor.classList.contains('toc-ignore')) return
|
||||
|
||||
toc.value.push({
|
||||
id: anchor.id,
|
||||
level: parseInt(anchor.tagName.replace(/[a-z]+/i, '') || '1'),
|
||||
title: anchor.textContent || '',
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return toc
|
||||
}
|
||||
238
src/composables/notyf.ts
Normal file
238
src/composables/notyf.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import type { INotyfNotificationOptions, Notyf, NotyfNotification } from 'notyf'
|
||||
import type { Plugin } from 'vue'
|
||||
|
||||
interface NotyfContext {
|
||||
dismiss: (notification: NotyfNotification) => void
|
||||
dismissAll: () => void
|
||||
success: (payload: string | Partial<INotyfNotificationOptions>) => void
|
||||
error: (payload: string | Partial<INotyfNotificationOptions>) => void
|
||||
info: (payload: string | Partial<INotyfNotificationOptions>) => void
|
||||
warning: (payload: string | Partial<INotyfNotificationOptions>) => void
|
||||
primary: (payload: string | Partial<INotyfNotificationOptions>) => void
|
||||
purple: (payload: string | Partial<INotyfNotificationOptions>) => void
|
||||
blue: (payload: string | Partial<INotyfNotificationOptions>) => void
|
||||
green: (payload: string | Partial<INotyfNotificationOptions>) => void
|
||||
orange: (payload: string | Partial<INotyfNotificationOptions>) => void
|
||||
}
|
||||
|
||||
export const useNotyf = () => {
|
||||
return inject(notyfSymbol)!
|
||||
}
|
||||
|
||||
const notyfSymbol: InjectionKey<NotyfContext>
|
||||
= Symbol('notyf')
|
||||
|
||||
export function createNotyf(): Plugin {
|
||||
return {
|
||||
async install(app) {
|
||||
const themeColors = useThemeColors()
|
||||
let notyf: Notyf
|
||||
|
||||
if (!import.meta.env.SSR) {
|
||||
const { Notyf } = await import('notyf')
|
||||
notyf = new Notyf({
|
||||
duration: 2000,
|
||||
position: {
|
||||
x: 'right',
|
||||
y: 'bottom',
|
||||
},
|
||||
types: [
|
||||
{
|
||||
type: 'warning',
|
||||
background: themeColors.warning,
|
||||
icon: {
|
||||
className: 'fas fa-hand-paper',
|
||||
tagName: 'i',
|
||||
text: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'info',
|
||||
background: themeColors.info,
|
||||
icon: {
|
||||
className: 'fas fa-info-circle',
|
||||
tagName: 'i',
|
||||
text: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'primary',
|
||||
background: themeColors.primary,
|
||||
icon: {
|
||||
className: 'fas fa-car-crash',
|
||||
tagName: 'i',
|
||||
text: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'accent',
|
||||
background: themeColors.purple,
|
||||
icon: {
|
||||
className: 'fas fa-car-crash',
|
||||
tagName: 'i',
|
||||
text: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'purple',
|
||||
background: themeColors.purple,
|
||||
icon: {
|
||||
className: 'fas fa-check',
|
||||
tagName: 'i',
|
||||
text: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'blue',
|
||||
background: themeColors.blue,
|
||||
icon: {
|
||||
className: 'fas fa-check',
|
||||
tagName: 'i',
|
||||
text: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'green',
|
||||
background: themeColors.lime,
|
||||
icon: {
|
||||
className: 'fas fa-check',
|
||||
tagName: 'i',
|
||||
text: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'orange',
|
||||
background: themeColors.orange,
|
||||
icon: {
|
||||
className: 'fas fa-check',
|
||||
tagName: 'i',
|
||||
text: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const context = {
|
||||
dismiss: (notification: NotyfNotification) => {
|
||||
notyf?.dismiss(notification)
|
||||
},
|
||||
dismissAll: () => {
|
||||
notyf?.dismissAll()
|
||||
},
|
||||
success: (payload: string | Partial<INotyfNotificationOptions>) => {
|
||||
return notyf?.success(payload)
|
||||
},
|
||||
error: (payload: string | Partial<INotyfNotificationOptions>) => {
|
||||
return notyf?.error(payload)
|
||||
},
|
||||
info: (payload: string | Partial<INotyfNotificationOptions>) => {
|
||||
const options: Partial<INotyfNotificationOptions> = {
|
||||
type: 'info',
|
||||
}
|
||||
|
||||
if (typeof payload === 'string') {
|
||||
options.message = payload
|
||||
}
|
||||
else {
|
||||
Object.assign(options, payload)
|
||||
}
|
||||
|
||||
return notyf?.open(options)
|
||||
},
|
||||
warning: (payload: string | Partial<INotyfNotificationOptions>) => {
|
||||
const options: Partial<INotyfNotificationOptions> = {
|
||||
type: 'warning',
|
||||
}
|
||||
|
||||
if (typeof payload === 'string') {
|
||||
options.message = payload
|
||||
}
|
||||
else {
|
||||
Object.assign(options, payload)
|
||||
}
|
||||
|
||||
return notyf?.open(options)
|
||||
},
|
||||
primary: (payload: string | Partial<INotyfNotificationOptions>) => {
|
||||
const options: Partial<INotyfNotificationOptions> = {
|
||||
type: 'primary',
|
||||
icon: {
|
||||
className: 'lnir lnir-checkmark-circle',
|
||||
tagName: 'i',
|
||||
color: '#fff',
|
||||
text: '',
|
||||
},
|
||||
}
|
||||
|
||||
if (typeof payload === 'string') {
|
||||
options.message = payload
|
||||
}
|
||||
else {
|
||||
Object.assign(options, payload)
|
||||
}
|
||||
|
||||
return notyf?.open(options)
|
||||
},
|
||||
purple: (payload: string | Partial<INotyfNotificationOptions>) => {
|
||||
const options: Partial<INotyfNotificationOptions> = {
|
||||
type: 'purple',
|
||||
}
|
||||
|
||||
if (typeof payload === 'string') {
|
||||
options.message = payload
|
||||
}
|
||||
else {
|
||||
Object.assign(options, payload)
|
||||
}
|
||||
|
||||
return notyf?.open(options)
|
||||
},
|
||||
blue: (payload: string | Partial<INotyfNotificationOptions>) => {
|
||||
const options: Partial<INotyfNotificationOptions> = {
|
||||
type: 'blue',
|
||||
}
|
||||
|
||||
if (typeof payload === 'string') {
|
||||
options.message = payload
|
||||
}
|
||||
else {
|
||||
Object.assign(options, payload)
|
||||
}
|
||||
|
||||
return notyf?.open(options)
|
||||
},
|
||||
green: (payload: string | Partial<INotyfNotificationOptions>) => {
|
||||
const options: Partial<INotyfNotificationOptions> = {
|
||||
type: 'green',
|
||||
}
|
||||
|
||||
if (typeof payload === 'string') {
|
||||
options.message = payload
|
||||
}
|
||||
else {
|
||||
Object.assign(options, payload)
|
||||
}
|
||||
|
||||
return notyf?.open(options)
|
||||
},
|
||||
orange: (payload: string | Partial<INotyfNotificationOptions>) => {
|
||||
const options: Partial<INotyfNotificationOptions> = {
|
||||
type: 'orange',
|
||||
}
|
||||
|
||||
if (typeof payload === 'string') {
|
||||
options.message = payload
|
||||
}
|
||||
else {
|
||||
Object.assign(options, payload)
|
||||
}
|
||||
|
||||
return notyf?.open(options)
|
||||
},
|
||||
} satisfies NotyfContext
|
||||
|
||||
app.provide(notyfSymbol, context)
|
||||
},
|
||||
}
|
||||
}
|
||||
24
src/composables/screen-size.ts
Normal file
24
src/composables/screen-size.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* This is a store that hold responsive state
|
||||
*
|
||||
* Using useMediaQuery from @vueuse/core allow to bind
|
||||
* css media queries results to ref
|
||||
*
|
||||
* We can import and use isLargeScreen, isMediumScreen anywhere in our project
|
||||
* @see /src/components/navigation/LandingNavigation.vue
|
||||
* @see /src/state/activeNavbarState.ts
|
||||
*/
|
||||
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
|
||||
export function useScreenSize() {
|
||||
const isLargeScreen = useMediaQuery('(width >= 1024px)')
|
||||
const isMediumScreen = useMediaQuery('(width >= 768px)')
|
||||
const isMobileScreen = useMediaQuery('(width <= 767px)')
|
||||
|
||||
return {
|
||||
isLargeScreen,
|
||||
isMediumScreen,
|
||||
isMobileScreen,
|
||||
}
|
||||
}
|
||||
35
src/composables/theme-colors.ts
Normal file
35
src/composables/theme-colors.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useCssVar } from '@vueuse/core'
|
||||
|
||||
export const useThemeColors = () => {
|
||||
const primary = useCssVar('--primary')
|
||||
const secondary = useCssVar('--secondary')
|
||||
const success = useCssVar('--success')
|
||||
const info = useCssVar('--info')
|
||||
const warning = useCssVar('--warning')
|
||||
const danger = useCssVar('--danger')
|
||||
const purple = useCssVar('--purple')
|
||||
const blue = useCssVar('--blue')
|
||||
const green = useCssVar('--green')
|
||||
const yellow = useCssVar('--yellow')
|
||||
const orange = useCssVar('--orange')
|
||||
const lime = useCssVar('--lime')
|
||||
const pink = useCssVar('--pink')
|
||||
const grey = useCssVar('--muted-grey')
|
||||
|
||||
return reactive({
|
||||
primary,
|
||||
secondary,
|
||||
success,
|
||||
info,
|
||||
warning,
|
||||
danger,
|
||||
purple,
|
||||
blue,
|
||||
green,
|
||||
yellow,
|
||||
orange,
|
||||
lime,
|
||||
pink,
|
||||
grey,
|
||||
})
|
||||
}
|
||||
30
src/composables/tiny-slider.ts
Normal file
30
src/composables/tiny-slider.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { TinySliderInstance, TinySliderSettings } from 'tiny-slider/src/tiny-slider'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
|
||||
export function useTinySlider(
|
||||
target: Ref<Element | undefined>,
|
||||
settings: MaybeRefOrGetter<Omit<TinySliderSettings, 'container'>> = {},
|
||||
) {
|
||||
const slider = shallowRef<TinySliderInstance | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
if (target.value) {
|
||||
const { tns } = await import('tiny-slider/src/tiny-slider')
|
||||
|
||||
slider.value = tns({
|
||||
container: target.value,
|
||||
...toValue(settings),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (slider.value) {
|
||||
slider.value.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
slider,
|
||||
}
|
||||
}
|
||||
96
src/composables/user-token.ts
Normal file
96
src/composables/user-token.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { H3Event } from 'h3'
|
||||
import { deleteCookie, getCookie, setCookie } from 'h3'
|
||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||
import { useSSRContext } from 'vue'
|
||||
import { type VueroSSRContext } from '/@server/types'
|
||||
|
||||
interface CookieOption {
|
||||
path?: string
|
||||
expires?: Date
|
||||
maxAge?: number
|
||||
domain?: string
|
||||
secure?: boolean
|
||||
httpOnly?: boolean
|
||||
sameSite?: boolean | 'none' | 'lax' | 'strict'
|
||||
partitioned?: boolean
|
||||
}
|
||||
|
||||
const tokenKey = 'token'
|
||||
const options = {
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
secure: true,
|
||||
} satisfies CookieOption
|
||||
|
||||
export function useUserToken(event?: H3Event) {
|
||||
let token: Ref<string | undefined>
|
||||
|
||||
// when client only, use localStorage
|
||||
if (!__VUERO_SSR_BUILD__) {
|
||||
token = useSessionStorage(tokenKey, '')
|
||||
}
|
||||
|
||||
// otherwise, we need to use cookies to share the token between client and server
|
||||
else {
|
||||
// server side: cookies are managed by http headers.
|
||||
// use h3 helpers and ssr context to get h3 event
|
||||
if (import.meta.env.SSR) {
|
||||
const _event = event || useSSRContext<VueroSSRContext>()?.event
|
||||
|
||||
token = computed<string | undefined>({
|
||||
get() {
|
||||
if (_event) {
|
||||
return getCookie(_event, tokenKey)
|
||||
}
|
||||
},
|
||||
set(value?: string) {
|
||||
if (_event) {
|
||||
if (value) {
|
||||
setCookie(_event, tokenKey, value, options)
|
||||
}
|
||||
else {
|
||||
deleteCookie(_event, tokenKey, options)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// client side, use cookies via document.cookie
|
||||
else {
|
||||
const cookies = useCookies(['locale'])
|
||||
|
||||
token = ref(cookies.get(tokenKey))
|
||||
|
||||
// we need to listen to changes in cookies in case it's changed from another tab
|
||||
// or another part of the app
|
||||
const listener = (event: any) => {
|
||||
if (event.name === tokenKey) {
|
||||
token.value = event.value
|
||||
}
|
||||
}
|
||||
|
||||
cookies.addChangeListener(listener)
|
||||
|
||||
// in case this composable is used inside a component that is unmounted
|
||||
// we need to remove the listener to avoid memory leaks
|
||||
if (getCurrentScope()) {
|
||||
onScopeDispose(() => cookies?.removeChangeListener(listener))
|
||||
}
|
||||
|
||||
// watch the token value and update the cookie if needed
|
||||
watch(token, (value) => {
|
||||
if (value && cookies.get(tokenKey) !== value) {
|
||||
cookies.set(tokenKey, value, options)
|
||||
}
|
||||
|
||||
else if (!value && cookies.get(tokenKey)) {
|
||||
cookies.remove(tokenKey, options)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
35
src/composables/vuero-context.ts
Normal file
35
src/composables/vuero-context.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { Plugin } from 'vue'
|
||||
|
||||
export interface VueroContext extends Record<string, any> {}
|
||||
|
||||
const injectionKey = Symbol('vuero-context') as InjectionKey<VueroContext>
|
||||
|
||||
export function useVueroContext<T>(
|
||||
key: string,
|
||||
defaultValue?: () => T,
|
||||
): Ref<T | null> {
|
||||
const context = inject(injectionKey)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useVueroContext() was called without having vuero-context plugin installed.')
|
||||
}
|
||||
|
||||
const state = toRef(context, key)
|
||||
|
||||
if (state.value === undefined) {
|
||||
const val = defaultValue?.()
|
||||
state.value = val
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
export function createVueroContext(
|
||||
context = {} as VueroContext,
|
||||
): Plugin {
|
||||
return {
|
||||
install(app) {
|
||||
app.provide(injectionKey, reactive(context))
|
||||
},
|
||||
}
|
||||
}
|
||||
120
src/composables/wizard.ts
Normal file
120
src/composables/wizard.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import type { InjectionKey } from 'vue'
|
||||
|
||||
/**
|
||||
* Using typescript types allow better developer experience
|
||||
* with autocompletion and compiler error prechecking
|
||||
*/
|
||||
import type { WizardData } from '/@src/types/wizard'
|
||||
|
||||
interface WizardStepOptions {
|
||||
number: number
|
||||
canNavigate?: boolean
|
||||
previousStepFn?: () => Promise<void>
|
||||
validateStepFn?: () => Promise<void>
|
||||
}
|
||||
|
||||
export type WizardContext = ReturnType<typeof createWizardContext>
|
||||
export const useWizardSymbolContext = Symbol('wizard') as InjectionKey<WizardContext>
|
||||
|
||||
function createWizardContext() {
|
||||
const step = ref(1)
|
||||
const loading = ref(false)
|
||||
const canNavigate = ref(false)
|
||||
const previousStepFn = shallowRef<WizardStepOptions['previousStepFn'] | null>()
|
||||
const validateStepFn = shallowRef<WizardStepOptions['validateStepFn'] | null>()
|
||||
const data = reactive<WizardData>({
|
||||
name: '',
|
||||
description: '',
|
||||
relatedTo: 'UI/UX Design',
|
||||
logo: null,
|
||||
timeFrame: ref({
|
||||
start: new Date(),
|
||||
end: new Date(),
|
||||
}),
|
||||
budget: '< 5K',
|
||||
attachments: [],
|
||||
teammates: [],
|
||||
tools: [],
|
||||
customer: null,
|
||||
})
|
||||
|
||||
const stepTitle = computed(() => {
|
||||
switch (step.value) {
|
||||
case 2:
|
||||
return 'Project Info'
|
||||
case 3:
|
||||
return 'Project Details'
|
||||
case 4:
|
||||
return 'Project Files'
|
||||
case 5:
|
||||
return 'Team Members'
|
||||
case 6:
|
||||
return 'Project Tools'
|
||||
case 7:
|
||||
return 'Preview'
|
||||
case 8:
|
||||
return 'Finish'
|
||||
case 1:
|
||||
default:
|
||||
return 'Project Type'
|
||||
}
|
||||
})
|
||||
|
||||
function setLoading(value: boolean) {
|
||||
loading.value = value
|
||||
}
|
||||
function setStep(options?: WizardStepOptions) {
|
||||
step.value = options?.number || 1
|
||||
canNavigate.value = options?.canNavigate ?? false
|
||||
previousStepFn.value = options?.previousStepFn ?? null
|
||||
validateStepFn.value = options?.validateStepFn ?? null
|
||||
}
|
||||
|
||||
async function save() {
|
||||
loading.value = true
|
||||
|
||||
// simulate saving data
|
||||
await sleep(2000)
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function reset() {
|
||||
data.name = ''
|
||||
data.description = ''
|
||||
data.relatedTo = 'UI/UX Design'
|
||||
data.logo = null
|
||||
data.timeFrame = {
|
||||
start: new Date(),
|
||||
end: new Date(),
|
||||
}
|
||||
data.budget = '< 5K'
|
||||
data.attachments = []
|
||||
data.teammates = []
|
||||
data.tools = []
|
||||
data.customer = null
|
||||
}
|
||||
|
||||
return reactive({
|
||||
canNavigate,
|
||||
previousStepFn,
|
||||
validateStepFn,
|
||||
step,
|
||||
loading,
|
||||
stepTitle,
|
||||
data,
|
||||
setLoading,
|
||||
setStep,
|
||||
save,
|
||||
reset,
|
||||
})
|
||||
}
|
||||
|
||||
export function useWizard() {
|
||||
let context = inject(useWizardSymbolContext, undefined)
|
||||
if (!context) {
|
||||
context = createWizardContext()
|
||||
provide(useWizardSymbolContext, context)
|
||||
}
|
||||
return context
|
||||
}
|
||||
Reference in New Issue
Block a user