diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..6f40582 --- /dev/null +++ b/.eslintrc.cjs @@ -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' + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a4cff5 --- /dev/null +++ b/.gitignore @@ -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 + diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..5c295a7 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "bracketSpacing": true, + "printWidth": 140, + "singleQuote": true, + "trailingComma": "none", + "tabWidth": 2, + "useTabs": false, + "semi": false +} diff --git a/README.md b/README.md index 886d9d8..04cdf6d 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file +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. +

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!** diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/index.html b/index.html new file mode 100644 index 0000000..a888544 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..061910e --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..df36fcf Binary files /dev/null and b/public/favicon.ico differ diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..59b121f --- /dev/null +++ b/src/App.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/src/assets/base.css b/src/assets/base.css new file mode 100644 index 0000000..ed558ce --- /dev/null +++ b/src/assets/base.css @@ -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; +} diff --git a/src/assets/calendar.svg b/src/assets/calendar.svg new file mode 100644 index 0000000..1e3db5c --- /dev/null +++ b/src/assets/calendar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/main.css b/src/assets/main.css new file mode 100644 index 0000000..8431e84 --- /dev/null +++ b/src/assets/main.css @@ -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%; +} diff --git a/src/components/AppointmentPage.vue b/src/components/AppointmentPage.vue new file mode 100644 index 0000000..6783384 --- /dev/null +++ b/src/components/AppointmentPage.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/src/components/AppointmentPageSlotItem.vue b/src/components/AppointmentPageSlotItem.vue new file mode 100644 index 0000000..e2d25e1 --- /dev/null +++ b/src/components/AppointmentPageSlotItem.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..0ac3a5f --- /dev/null +++ b/src/main.ts @@ -0,0 +1,6 @@ +import './assets/main.css' + +import { createApp } from 'vue' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/src/types/api-models.ts b/src/types/api-models.ts new file mode 100644 index 0000000..956f5ae --- /dev/null +++ b/src/types/api-models.ts @@ -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 +}> diff --git a/src/types/time-slot.ts b/src/types/time-slot.ts new file mode 100644 index 0000000..f3d6d0e --- /dev/null +++ b/src/types/time-slot.ts @@ -0,0 +1,5 @@ +export interface TimeSlot { + start: string + end: string + taken: boolean +} diff --git a/src/utils/api.ts b/src/utils/api.ts new file mode 100644 index 0000000..fdc5cca --- /dev/null +++ b/src/utils/api.ts @@ -0,0 +1,22 @@ +import axios from 'axios' +import type { BookSlotData, WeeklySlotsResponse } from '@/types/api-models' + +const BASE_URL = 'https://draliatest.azurewebsites.net/api/availability' + +export const getWeeklySlots = async (date: string): Promise => { + try { + const response = await axios.get(`${BASE_URL}/GetWeeklySlots/${date}`) + return response.data + } catch (error) { + console.error('Error fetching slots:', error) + return [] + } +} + +export const bookSlot = async (data: BookSlotData): Promise => { + try { + await axios.post(`${BASE_URL}/BookSlot`, data) + } catch (error) { + console.error('Error booking slot:', error) + } +} diff --git a/src/utils/date.ts b/src/utils/date.ts new file mode 100644 index 0000000..d4cb4ba --- /dev/null +++ b/src/utils/date.ts @@ -0,0 +1,23 @@ +import { addDays, addWeeks, format, isAfter, startOfDay, startOfWeek } from 'date-fns' + +export const getClosestLastMonday = (referenceDate: Date) => { + const closestPastMonday = startOfWeek(referenceDate, { weekStartsOn: 2 }) + + const formattedDate = format(closestPastMonday, 'yyyyMMdd') + + return formattedDate +} + +export const getClosestNextMonday = (referenceDate: Date) => { + const startOfNextWeek = startOfWeek(addWeeks(referenceDate, 1), { weekStartsOn: 6 }) + + return format(startOfNextWeek, 'yyyyMMdd') +} + +export const isDateAfterTomorrow = (dateString: string) => { + const today = new Date() + const referenceDate = new Date(dateString) + const tomorrow = addDays(startOfDay(today), 1) + const dayAfterTomorrow = addDays(tomorrow, 1) + return isAfter(referenceDate, dayAfterTomorrow) +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..e14c754 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..100cf6a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.vitest.json" + } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..f094063 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "nightwatch.conf.*", + "playwright.config.*" + ], + "compilerOptions": { + "composite": true, + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"] + } +} diff --git a/tsconfig.vitest.json b/tsconfig.vitest.json new file mode 100644 index 0000000..571995d --- /dev/null +++ b/tsconfig.vitest.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.app.json", + "exclude": [], + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo", + + "lib": [], + "types": ["node", "jsdom"] + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..26a21a9 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,18 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import vueDevTools from 'vite-plugin-vue-devtools' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + vueDevTools(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + } +}) diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..4b1c897 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,14 @@ +import { fileURLToPath } from 'node:url' +import { mergeConfig, defineConfig, configDefaults } from 'vitest/config' +import viteConfig from './vite.config' + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + environment: 'jsdom', + exclude: [...configDefaults.exclude, 'e2e/**'], + root: fileURLToPath(new URL('./', import.meta.url)) + } + }) +)