Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
}
}
33 changes: 33 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
.DS_Store
dist
dist-ssr
coverage
*.local

/cypress/videos/
/cypress/screenshots/

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

*.tsbuildinfo

package-lock.json

10 changes: 10 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"bracketSpacing": true,
"printWidth": 140,
"singleQuote": true,
"trailingComma": "none",
"tabWidth": 2,
"useTabs": false,
"semi": false
}
42 changes: 40 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
# vue-recruitment-refactor-assignment
# Full-stack technical assigment: front-end

Front-end part of recruitment assignment for full stack dev position
Hello! First of all - welcome and congrats that we can meet on this stage of the process!

## Guidelines:

- to create your copy, use the green "Use this template" button on the top right. It should be set as private repo. Do
not use fork feature.
- to start the app within the app root dir please run `npm i && npm run dev`
- complete the task as described below
- once completed - give access to the repo to the hiring manager and other people provided
- send us the link to the pull request in your repo, in order for us to review your task

## Task description

Code represents POC of Visit Management Page.
<br/><br/>Patient can see a visit date with the option to book a new appointment but provided solution does not meet all
business requirements and has a few bugs.

### Issues to resolve:

- fix fetching available slots
- apply correct date / time formatting to the slot element to make it more readable (only time should be displayed)

### The goal is to improve patient's experience by:

- grouping slots by day
- when user clicks on a slot, booking action is triggered. Set up a confirmation step before booking new slot
- adding loading state of your choice when slot is being booked
- update confirmed date and available slots to avoid double bookings (two bookings for the same hour)

## Hints

- it's up to you how much time you want to spend
- feel free to refactor and reorganise the code as you feel like
- add any libraries that you might need
- add tests
- if you could do something better, but it feels like too much work - please put a comment and describe what would you
do

### **Good luck!**
1 change: 1 addition & 0 deletions env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
13 changes: 13 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
43 changes: 43 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "vue-recruitment-refactor-assignment",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"test:unit": "vitest",
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"axios": "^1.7.7",
"date-fns": "^4.1.0",
"vue": "^3.4.29"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.8.0",
"@tsconfig/node20": "^20.1.4",
"@types/jsdom": "^21.1.7",
"@types/node": "^20.14.5",
"@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.23.0",
"jsdom": "^24.1.0",
"npm-run-all2": "^6.2.0",
"prettier": "^3.2.5",
"sass-embedded": "^1.79.4",
"typescript": "~5.4.0",
"vite": "^5.3.1",
"vite-plugin-vue-devtools": "^7.3.1",
"vitest": "^1.6.0",
"vue-tsc": "^2.0.21"
}
}
Binary file added public/favicon.ico
Binary file not shown.
24 changes: 24 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<template>
<div id="app" class="page">
<AppointmentPage />
</div>
</template>

<script setup lang="ts">
import AppointmentPage from '@/components/AppointmentPage.vue'
</script>

<style scoped>
.page {
width: 100%;
height: auto;
min-height: 100%;
flex: 1;
background-color: #edeff2;
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.5;
padding: 50px 0;
}
</style>
42 changes: 42 additions & 0 deletions src/assets/base.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}

body {
min-height: 100vh;
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

button,
p {
margin: 0;
padding: 0;
}

button {
cursor: pointer;
}
1 change: 1 addition & 0 deletions src/assets/calendar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions src/assets/main.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@import './base.css';

#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
font-family: Avenir, Helvetica, Arial, sans-serif;
height: 100%;
}
90 changes: 90 additions & 0 deletions src/components/AppointmentPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<template>
<div>
<p>Your visit with <b>Test Doctor</b> is confirmed:</p>
<div class="appointment-date">
<img src="../assets/calendar.svg" class="icon" alt="calendar-icon" />
<p>
On <b>{{ displayedBookedSlot }}</b>
</p>
</div>
<p class="appointment-date__sub-title">Select new appointment date to book a new visit:</p>
<AppointmentPageSlotItem v-for="slot in timeSlots" :key="slot.start" :time-slot="slot" @book-slot="onBookSlot" />
</div>
</template>

<script setup lang="ts">
import { format } from 'date-fns'
import { computed, onMounted, ref } from 'vue'
import { getClosestLastMonday, getClosestNextMonday, isDateAfterTomorrow } from '@/utils/date'
import { bookSlot, getWeeklySlots } from '@/utils/api'
import type { WeeklySlotsResponse } from '@/types/api-models'
import type { TimeSlot } from '@/types/time-slot'
import AppointmentPageSlotItem from '@/components/AppointmentPageSlotItem.vue'
const timeSlots = ref<TimeSlot[]>([])
const bookedAppointmentDate = ref('2024-08-20 09:00')
const getAvailableSlots = async () => {
let apiSlots: WeeklySlotsResponse = []
const lastMonday = getClosestLastMonday(new Date())
const nextMonday = getClosestNextMonday(new Date())
const currentWeekSlots = await getWeeklySlots(lastMonday)
const nextWeekSlots = await getWeeklySlots(nextMonday)
apiSlots = [...currentWeekSlots, ...nextWeekSlots]
timeSlots.value = apiSlots
.filter((slot) => isDateAfterTomorrow(slot.Start))
.map((item) => {
return {
start: item.Start,
end: item.End,
taken: item.Taken ?? false
}
})
}
const onBookSlot = async (selectedSlot: TimeSlot) => {
await bookSlot({
Start: selectedSlot.start,
End: selectedSlot.end,
Comments: '',
Patient: {
Name: 'Example',
SecondName: 'Patient',
Email: '[email protected]',
Phone: '123456789'
}
})
}
const displayedBookedSlot = computed(() => {
return format(bookedAppointmentDate.value, "EEEE, d MMMM yyyy 'at' HH:mm")
})
onMounted(async () => {
await getAvailableSlots()
})
</script>

<style scoped lang="scss">
.appointment-date {
background-color: white;
padding: 10px 20px;
margin: 12px 0;
display: flex;
align-items: center;
&__sub-title {
margin-bottom: 6px;
}
}
.icon {
width: 20px;
height: auto;
margin-right: 10px;
}
</style>
34 changes: 34 additions & 0 deletions src/components/AppointmentPageSlotItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>
<button class="time-slot" :disabled="timeSlot.taken" @click="emits('book-slot', timeSlot)">
{{ timeSlot.start }}
</button>
</template>

<script setup lang="ts">
import type { TimeSlot } from '@/types/time-slot'
defineProps<{ timeSlot: TimeSlot }>()
const emits = defineEmits<{
(e: 'book-slot', slot: TimeSlot): void
}>()
</script>

<style scoped>
.time-slot {
padding: 4px 0;
width: 50px;
margin: 2px 0;
border: none;
background-color: #eff4fd;
border-radius: 3%;
color: #3773c1;
font-weight: bold;
}
.time-slot:disabled {
background-color: white;
text-decoration: line-through;
color: #bfc3cb;
cursor: auto;
}
</style>
6 changes: 6 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
17 changes: 17 additions & 0 deletions src/types/api-models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export type BookSlotData = {
Start: string
End: string
Comments?: string
Patient: {
Name: string
SecondName: string
Email: string
Phone: string
}
}

export type WeeklySlotsResponse = Array<{
Start: string
End: string
Taken?: boolean
}>
Loading
Loading