initial
This commit is contained in:
39
apps/client/composables/api.ts
Normal file
39
apps/client/composables/api.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { NitroFetchRequest } from 'nitropack'
|
||||
import { callWithNuxt } from '#app'
|
||||
|
||||
export function $api<
|
||||
T = unknown,
|
||||
R extends NitroFetchRequest = NitroFetchRequest,
|
||||
>(
|
||||
request: Parameters<typeof $fetch<T, R>>[0],
|
||||
options?: Partial<Parameters<typeof $fetch<T, R>>[1]>,
|
||||
) {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const cookies = useRequestHeaders(['cookie'])
|
||||
|
||||
return $fetch<T, R>(request, {
|
||||
...options,
|
||||
headers: {
|
||||
...options?.headers,
|
||||
...cookies,
|
||||
},
|
||||
retry: false,
|
||||
baseURL: runtimeConfig.public.apiHost as string,
|
||||
credentials: 'include',
|
||||
onResponseError: async ({ response }) => {
|
||||
if (response.status === 401) {
|
||||
nuxtApp.runWithContext(() => {
|
||||
useCookie('session').value = null
|
||||
})
|
||||
await callWithNuxt(nuxtApp, clearNuxtState, ['user'])
|
||||
await callWithNuxt(nuxtApp, navigateTo, ['/login', { redirectCode: 401 }])
|
||||
}
|
||||
|
||||
// setStaticError({
|
||||
// status: response.status,
|
||||
// message: nuxtApp.$i18n.t('something_went_wrong'),
|
||||
// });
|
||||
},
|
||||
})
|
||||
}
|
||||
16
apps/client/composables/static-error.ts
Normal file
16
apps/client/composables/static-error.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface StaticError {
|
||||
status: number
|
||||
message?: string
|
||||
}
|
||||
|
||||
export function useStaticError() {
|
||||
return useState<StaticError | undefined>('static-error')
|
||||
}
|
||||
|
||||
export function setStaticError(value?: StaticError) {
|
||||
useStaticError().value = value
|
||||
}
|
||||
|
||||
export function clearStaticError() {
|
||||
clearNuxtState(['static-error'])
|
||||
}
|
||||
69
apps/client/composables/use-auth.ts
Normal file
69
apps/client/composables/use-auth.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export interface User {
|
||||
id: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const user = useState<User | null>('user')
|
||||
|
||||
const authenticated = computed(() => !!user.value)
|
||||
|
||||
async function getUser() {
|
||||
user.value = await $api('/users/current', { method: 'GET' })
|
||||
}
|
||||
|
||||
async function login(email: string, password: string) {
|
||||
await $api('/sessions', { method: 'POST', body: { email, password } })
|
||||
await getUser()
|
||||
|
||||
navigateTo('/projects')
|
||||
}
|
||||
|
||||
async function register(email: string, password: string) {
|
||||
await $api('/users', { method: 'POST', body: { email, password } })
|
||||
|
||||
navigateTo('/login')
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await $api('/sessions', { method: 'delete', body: {} })
|
||||
}
|
||||
finally {
|
||||
clearNuxtState('user')
|
||||
navigateTo('/login')
|
||||
}
|
||||
}
|
||||
|
||||
async function requestResetPassword(email: string) {
|
||||
await $api('/users/password_reset', { method: 'post', body: { email } })
|
||||
}
|
||||
|
||||
async function resetPassword(newPassword: string, resetCode: string) {
|
||||
await $api('/users/password_reset', {
|
||||
method: 'put',
|
||||
body: {
|
||||
newPassword,
|
||||
resetCode,
|
||||
},
|
||||
})
|
||||
}
|
||||
async function resendVerificationCode(email: string) {
|
||||
await $api('/users/verification', {
|
||||
method: 'put',
|
||||
body: { email },
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
authenticated,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
resendVerificationCode,
|
||||
requestResetPassword,
|
||||
resetPassword,
|
||||
}
|
||||
}
|
||||
156
apps/client/composables/use-filters.ts
Normal file
156
apps/client/composables/use-filters.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { computed, provide, reactive, unref } from 'vue'
|
||||
import type { ComputedRef, InjectionKey, MaybeRef } from 'vue'
|
||||
import { omit } from 'lodash-es'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const DATE_FORMAT = 'DD-MM-YYYY'
|
||||
|
||||
export interface Filter {
|
||||
type?: 'select' | 'calendar'
|
||||
key: string
|
||||
label: string
|
||||
placeholder: string
|
||||
searchable?: boolean
|
||||
multiple?: boolean
|
||||
options?: { label?: string, value: unknown }[]
|
||||
transform?: (value: AppliedFilter) => AppliedFilters
|
||||
}
|
||||
export type Filters = Filter[]
|
||||
|
||||
export type AppliedFilter = null | string | string[]
|
||||
export type AppliedFilters = Record<string, AppliedFilter>
|
||||
|
||||
export interface FiltersContext {
|
||||
schema: MaybeRef<Filters>
|
||||
appliedFiltersRaw: AppliedFilters
|
||||
appliedFilters: ComputedRef<AppliedFilters>
|
||||
empty: boolean | ComputedRef<boolean>
|
||||
apply: (p1: AppliedFilters | string, p2?: AppliedFilter) => void
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export const filtersContextKey: InjectionKey<FiltersContext>
|
||||
= Symbol('FILTERS')
|
||||
|
||||
export default (filters: MaybeRef<Filters>) => {
|
||||
const url = useRequestURL()
|
||||
|
||||
const searchString = computed(() => url.search)
|
||||
|
||||
const parsedUrl: { other: Record<string, string>, filters: AppliedFilters }
|
||||
= reactive({
|
||||
other: {},
|
||||
filters: {},
|
||||
})
|
||||
|
||||
const allowedFilters = computed<string[]>(() => {
|
||||
return unref(filters).map(filter => filter.key)
|
||||
})
|
||||
|
||||
parseUrl(searchString.value)
|
||||
|
||||
const appliedFiltersRaw = reactive<AppliedFilters>({
|
||||
...Object.fromEntries(
|
||||
allowedFilters.value.map(key => [key, isMultiple(key) ? [] : null]),
|
||||
),
|
||||
...parsedUrl.filters,
|
||||
})
|
||||
|
||||
const appliedFilters = computed<AppliedFilters>(() => {
|
||||
return Object.entries(appliedFiltersRaw).reduce((result, [key, value]) => {
|
||||
const filter = getFilterByKey(key)!
|
||||
if (filter.transform) {
|
||||
const transformedValue = filter.transform(value)
|
||||
|
||||
if (transformedValue)
|
||||
result = { ...result, ...transformedValue }
|
||||
else
|
||||
result[key] = value
|
||||
}
|
||||
else {
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}, {} as AppliedFilters)
|
||||
})
|
||||
|
||||
const empty = computed(() =>
|
||||
Object.values(appliedFiltersRaw).every((value) => {
|
||||
return Array.isArray(value) ? value.length === 0 : value !== null
|
||||
}),
|
||||
)
|
||||
|
||||
function parseUrl(searchString: string) {
|
||||
const params = new URLSearchParams(searchString)
|
||||
|
||||
parsedUrl.other = {}
|
||||
parsedUrl.filters = {}
|
||||
|
||||
for (const [key, value] of Array.from(params.entries())) {
|
||||
if (allowedFilters.value.includes(key)) {
|
||||
let newValue = isMultiple(key) ? value.split(',') : value
|
||||
|
||||
if (isCalendar(key)) {
|
||||
newValue = [...newValue].map(date =>
|
||||
dayjs(date, DATE_FORMAT).valueOf().toString(),
|
||||
)
|
||||
}
|
||||
|
||||
parsedUrl.filters[key] = newValue
|
||||
}
|
||||
else {
|
||||
parsedUrl.other[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
parsedUrl.other = omit(parsedUrl.other, ['page'])
|
||||
}
|
||||
|
||||
function apply(p1: AppliedFilters | string, p2?: AppliedFilter) {
|
||||
if (p2 && typeof p1 === 'string') {
|
||||
appliedFiltersRaw[p1] = p2
|
||||
}
|
||||
else if (typeof p1 === 'object') {
|
||||
for (const [key, value] of Object.entries(p1))
|
||||
appliedFiltersRaw[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
for (const key of Object.keys(appliedFiltersRaw))
|
||||
appliedFiltersRaw[key] = isMultiple(key) ? [] : null
|
||||
}
|
||||
|
||||
function getFilterByKey(key: string) {
|
||||
return unref(filters).find(f => f.key === key)
|
||||
}
|
||||
|
||||
function isMultiple(key: string) {
|
||||
const filter = getFilterByKey(key)
|
||||
|
||||
return filter?.multiple ?? filter?.type === 'calendar' ?? false
|
||||
}
|
||||
|
||||
function isCalendar(key: string) {
|
||||
const filter = getFilterByKey(key)
|
||||
|
||||
return filter?.type === 'calendar' ?? false
|
||||
}
|
||||
|
||||
provide(filtersContextKey, {
|
||||
schema: filters,
|
||||
appliedFiltersRaw,
|
||||
appliedFilters,
|
||||
empty,
|
||||
apply,
|
||||
reset,
|
||||
})
|
||||
|
||||
return {
|
||||
appliedFiltersRaw,
|
||||
appliedFilters,
|
||||
empty,
|
||||
apply,
|
||||
reset,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user