Skip to content

Commit

Permalink
feat: connect Users to the backend
Browse files Browse the repository at this point in the history
  • Loading branch information
raichev-dima committed Nov 12, 2024
1 parent 75cbfc1 commit 45956f1
Show file tree
Hide file tree
Showing 11 changed files with 75 additions and 79 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ VITE_APP_INCLUDE_DEMOS=
VITE_APP_ROUTER_MODE_HISTORY=

VITE_APP_BUILD_VERSION=

VITE_API_BASE_URL=
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ node_modules
dist
dist-ssr
*.local
.env

# Editor directories and files
.vscode/*
Expand All @@ -25,3 +26,4 @@ dist-ssr

# Local Netlify folder
.netlify

51 changes: 14 additions & 37 deletions src/data/pages/users.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
import { sleep } from '../../services/utils'
import { User } from './../../pages/users/types'
import usersDb from './users-db.json'
import projectsDb from './projects-db.json'
import { Project } from '../../pages/projects/types'

export const users = usersDb as User[]

const getUserProjects = (userId: number | string) => {
return projectsDb
.filter((project) => project.team.includes(Number(userId)))
.map((project) => ({
...project,
project_owner: users.find((user) => user.id === project.project_owner)!,
team: project.team.map((userId) => users.find((user) => user.id === userId)!),
status: project.status as Project['status'],
}))
}

// Simulate API calls
import { User } from '../../pages/users/types'
import api from '../../services/api'

export type Pagination = {
page: number
Expand All @@ -37,25 +19,22 @@ export type Filters = {

const getSortItem = (obj: any, sortBy: string) => {
if (sortBy === 'projects') {
return obj.projects.map((project: any) => project.project_name).join(', ')
return obj.projects.map((project: any) => project).join(', ')
}

return obj[sortBy]
}

export const getUsers = async (filters: Partial<Filters & Pagination & Sorting>) => {
await sleep(1000)
const { isActive, search, sortBy, sortingOrder } = filters
let filteredUsers = users
let filteredUsers: User[] = await fetch(api.getAllUsers()).then((r) => r.json())

filteredUsers = filteredUsers.filter((user) => user.active === isActive)
filteredUsers = filteredUsers.filter((user) => user.isActive === isActive)

if (search) {
filteredUsers = filteredUsers.filter((user) => user.fullname.toLowerCase().includes(search.toLowerCase()))
filteredUsers = filteredUsers.filter((user) => user.fullName.toLowerCase().includes(search.toLowerCase()))
}

filteredUsers = filteredUsers.map((user) => ({ ...user, projects: getUserProjects(user.id) }))

if (sortBy && sortingOrder) {
filteredUsers = filteredUsers.sort((a, b) => {
const first = getSortItem(a, sortBy)
Expand All @@ -82,20 +61,18 @@ export const getUsers = async (filters: Partial<Filters & Pagination & Sorting>)
}

export const addUser = async (user: User) => {
await sleep(1000)
users.unshift(user)
const headers = new Headers()
headers.append('Content-Type', 'application/json')

return fetch(api.getAllUsers(), { method: 'POST', body: JSON.stringify(user), headers })
}

export const updateUser = async (user: User) => {
await sleep(1000)
const index = users.findIndex((u) => u.id === user.id)
users[index] = user
const headers = new Headers()
headers.append('Content-Type', 'application/json')
return fetch(api.getUser(user.id), { method: 'PUT', body: JSON.stringify(user), headers })
}

export const removeUser = async (user: User) => {
await sleep(1000)
users.splice(
users.findIndex((u) => u.id === user.id),
1,
)
return fetch(api.getUser(user.id), { method: 'DELETE' })
}
8 changes: 4 additions & 4 deletions src/pages/users/UsersPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ const onUserSaved = async (user: User) => {
if (userToEdit.value) {
await usersApi.update(user)
notify({
message: `${user.fullname} has been updated`,
message: `${user.fullName} has been updated`,
color: 'success',
})
} else {
usersApi.add(user)
await usersApi.add(user)
notify({
message: `${user.fullname} has been created`,
message: `${user.fullName} has been created`,
color: 'success',
})
}
Expand All @@ -43,7 +43,7 @@ const onUserSaved = async (user: User) => {
const onUserDelete = async (user: User) => {
await usersApi.remove(user)
notify({
message: `${user.fullname} has been deleted`,
message: `${user.fullName} has been deleted`,
color: 'success',
})
}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/users/composables/useUsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { User } from '../types'
import { watchIgnorable } from '@vueuse/core'

const makePaginationRef = () => ref<Pagination>({ page: 1, perPage: 10, total: 0 })
const makeSortingRef = () => ref<Sorting>({ sortBy: 'fullname', sortingOrder: null })
const makeSortingRef = () => ref<Sorting>({ sortBy: 'fullName', sortingOrder: null })
const makeFiltersRef = () => ref<Partial<Filters>>({ isActive: true, search: '' })

export const useUsers = (options?: {
Expand Down
15 changes: 7 additions & 8 deletions src/pages/users/types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { Project } from '../projects/types'
export type UserRole = 'ADMIN' | 'USER' | 'OWNER'

export type UserRole = 'admin' | 'user' | 'owner'
export type UUID = `${string}-${string}-${string}-${string}-${string}`

export type BaseUser = {
id: number
fullname: string
export type User = {
id: UUID
fullName: string
email: string
username: string
role: UserRole
avatar: string
projects: string[]
notes: string
active: boolean
isActive: boolean
}

export type User = BaseUser & { projects: Project[] }
33 changes: 16 additions & 17 deletions src/pages/users/widgets/EditUserForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,28 @@ const props = defineProps({
},
})
const defaultNewUser: User = {
id: -1,
const defaultNewUser: Omit<User, 'id'> = {
avatar: '',
fullname: '',
role: 'user',
fullName: '',
role: 'USER',
username: '',
notes: '',
email: '',
active: true,
isActive: true,
projects: [],
}
const newUser = ref<User>({ ...defaultNewUser })
const newUser = ref<User>({ ...defaultNewUser } as User)
const isFormHasUnsavedChanges = computed(() => {
return Object.keys(newUser.value).some((key) => {
if (key === 'avatar' || key === 'projects') {
return false
}
return newUser.value[key as keyof User] !== (props.user ?? defaultNewUser)?.[key as keyof User]
return (
newUser.value[key as keyof Omit<User, 'id'>] !== (props.user ?? defaultNewUser)?.[key as keyof Omit<User, 'id'>]
)
})
})
Expand Down Expand Up @@ -80,10 +81,10 @@ const onSave = () => {
}
}
const roleSelectOptions: { text: Capitalize<UserRole>; value: UserRole }[] = [
{ text: 'Admin', value: 'admin' },
{ text: 'User', value: 'user' },
{ text: 'Owner', value: 'owner' },
const roleSelectOptions: { text: Capitalize<Lowercase<UserRole>>; value: UserRole }[] = [
{ text: 'Admin', value: 'ADMIN' },
{ text: 'User', value: 'USER' },
{ text: 'Owner', value: 'OWNER' },
]
const { projects } = useProjects({ pagination: ref({ page: 1, perPage: 9999, total: 10 }) })
Expand Down Expand Up @@ -112,11 +113,11 @@ const { projects } = useProjects({ pagination: ref({ page: 1, perPage: 9999, tot
<div class="self-stretch flex-col justify-start items-start gap-4 flex">
<div class="flex gap-4 flex-col sm:flex-row w-full">
<VaInput
v-model="newUser.fullname"
v-model="newUser.fullName"
label="Full name"
class="w-full sm:w-1/2"
:rules="[validators.required]"
name="fullname"
name="fullName"
/>
<VaInput
v-model="newUser.username"
Expand All @@ -138,11 +139,9 @@ const { projects } = useProjects({ pagination: ref({ page: 1, perPage: 9999, tot
v-model="newUser.projects"
label="Projects"
class="w-full sm:w-1/2"
:options="projects"
:options="projects.map((p) => p.project_name)"
:rules="[validators.required]"
name="projects"
text-by="project_name"
track-by="id"
multiple
:max-visible-options="2"
/>
Expand All @@ -162,7 +161,7 @@ const { projects } = useProjects({ pagination: ref({ page: 1, perPage: 9999, tot
</div>

<div class="flex items-center w-1/2 mt-4">
<VaCheckbox v-model="newUser.active" label="Active" class="w-full" name="active" />
<VaCheckbox v-model="newUser.isActive" label="Active" class="w-full" name="active" />
</div>
</div>

Expand Down
4 changes: 2 additions & 2 deletions src/pages/users/widgets/UserAvatar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const isUrl = (avatar: string) => {
<VaAvatar
:size="size"
:src="isUrl(user.avatar) ? user.avatar : ''"
:fallback-text="user.avatar || user.fullname[0]"
:color="avatarColor(user.fullname)"
:fallback-text="user.avatar || user.fullName[0]"
:color="avatarColor(user.fullName)"
/>
</template>
20 changes: 10 additions & 10 deletions src/pages/users/widgets/UsersTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useVModel } from '@vueuse/core'
import { Project } from '../../projects/types'
const columns = defineVaDataTableColumns([
{ label: 'Full Name', key: 'fullname', sortable: true },
{ label: 'Full Name', key: 'fullName', sortable: true },
{ label: 'Email', key: 'email', sortable: true },
{ label: 'Username', key: 'username', sortable: true },
{ label: 'Role', key: 'role', sortable: true },
Expand All @@ -24,7 +24,7 @@ const props = defineProps({
loading: { type: Boolean, default: false },
pagination: { type: Object as PropType<Pagination>, required: true },
sortBy: { type: String as PropType<Sorting['sortBy']>, required: true },
sortingOrder: { type: String as PropType<Sorting['sortingOrder']>, required: true },
sortingOrder: { type: String as PropType<Sorting['sortingOrder']>, default: null },
})
const emit = defineEmits<{
Expand All @@ -39,9 +39,9 @@ const sortByVModel = useVModel(props, 'sortBy', emit)
const sortingOrderVModel = useVModel(props, 'sortingOrder', emit)
const roleColors: Record<UserRole, string> = {
admin: 'danger',
user: 'background-element',
owner: 'warning',
ADMIN: 'danger',
USER: 'background-element',
OWNER: 'warning',
}
const totalPages = computed(() => Math.ceil(props.pagination.total / props.pagination.perPage))
Expand All @@ -51,7 +51,7 @@ const { confirm } = useModal()
const onUserDelete = async (user: User) => {
const agreed = await confirm({
title: 'Delete user',
message: `Are you sure you want to delete ${user.fullname}?`,
message: `Are you sure you want to delete ${user.fullName}?`,
okText: 'Delete',
cancelText: 'Cancel',
size: 'small',
Expand All @@ -66,13 +66,13 @@ const onUserDelete = async (user: User) => {
const formatProjectNames = (projects: Project[]) => {
if (projects.length === 0) return 'No projects'
if (projects.length <= 2) {
return projects.map((project) => project.project_name).join(', ')
return projects.map((project) => project).join(', ')
}
return (
projects
.slice(0, 2)
.map((project) => project.project_name)
.map((project) => project)
.join(', ') +
' + ' +
(projects.length - 2) +
Expand All @@ -89,10 +89,10 @@ const formatProjectNames = (projects: Project[]) => {
:items="users"
:loading="$props.loading"
>
<template #cell(fullname)="{ rowData }">
<template #cell(fullName)="{ rowData }">
<div class="flex items-center gap-2 max-w-[230px] ellipsis">
<UserAvatar :user="rowData as User" size="small" />
{{ rowData.fullname }}
{{ rowData.fullName }}
</div>
</template>

Expand Down
8 changes: 8 additions & 0 deletions src/services/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL

export default {
getAllUsers: () => `${apiBaseUrl}/users`,
getUser: (id: string) => `${apiBaseUrl}/users/${id}`,
getUsers: ({ page, pageSize }: { page: number; pageSize: number }) =>
`${apiBaseUrl}/users/?page=${page}&pageSize=${pageSize}`,
}
9 changes: 9 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,13 @@ export default defineConfig({
include: resolve(dirname(fileURLToPath(import.meta.url)), './src/i18n/locales/**'),
}),
],
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
})

0 comments on commit 45956f1

Please sign in to comment.