Skip to content

Commit

Permalink
fetch random integers and display chart bars
Browse files Browse the repository at this point in the history
  • Loading branch information
plecrx committed Jan 11, 2024
1 parent ad31b17 commit feee80a
Show file tree
Hide file tree
Showing 22 changed files with 365 additions and 96 deletions.
3 changes: 3 additions & 0 deletions env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
interface ImportMeta {
readonly env: ImportMetaEnv;
}
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
"vue-tsc": "^1.8.8"
},
"lint-staged": {
"*.{vue,js,jsx,cjs,mjs,ts,tsx,cts,mts}": "eslint --cache --fix --ignore-path .gitignore",
"*.src/": "prettier --write"
}
}
21 changes: 21 additions & 0 deletions src/components/button.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<template>
<button class="button" type="button">
<slot/>
</button>
</template>

<style scoped lang="css">
.button {
border-radius: 16px;
border: none;
padding: 20px 24px;
font-weight: bold;
background-color: indigo;
color: floralwhite;
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
</style>
46 changes: 46 additions & 0 deletions src/components/chart.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script setup lang="ts">
defineProps<{
recordValues: Record<string | number, number>
}>()
const getBarHeight = (value: number) => `${value * 10}px`
</script>

<template>
<div class="chart">
<div
v-for="(occurrences, label) in recordValues"
:key="`${label}-${occurrences}`"
class="bar"
:style="{ height: `${getBarHeight(occurrences)}` }"
data-testid="chart-bar"
>
<span class="label" data-testid="chart-bar-label">{{ label }}</span>
</div>
</div>
</template>

<style scoped lang="css">
.chart {
display: flex;
min-height: 200px;
gap: 16px;
}
.bar {
position: relative;
background-color: cornflowerblue;
border: indigo;
width: 40px;
text-align: center;
border-radius: 4px;
color: indigo;
margin-top: auto;
}
.label {
position: absolute;
bottom: -20px;
left: 50%;
transform: translateX(-50%);
}
</style>
30 changes: 0 additions & 30 deletions src/components/counter-card.vue

This file was deleted.

18 changes: 18 additions & 0 deletions src/features/integersList/searchRandomIntegers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { POST } from '@/utils/http'

export type SearchRandomIntegersParams = {
base: 2 | 8 | 10 | 16
col: number
format: 'plain' | 'html'
max: number
min: number
num: number
}
export const searchRandomIntegers = async (params: SearchRandomIntegersParams): Promise<string[]> => {
const { num, min, max, col, base, format } = params

return await POST(
`/integers/?num=${num}&min=${min}&max=${max}&col=${col}&base=${base}&format=${format}`,
null
)
}
17 changes: 17 additions & 0 deletions src/layouts/page-layout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<div class="page-layout">
<slot/>
</div>
</template>

<style scoped lang="css">
.page-layout {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 20px;
height: 100vh;
}
</style>
34 changes: 30 additions & 4 deletions src/pages/home-page.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,35 @@
<script setup lang="ts">
import CounterCard from '@/components/counter-card.vue'
import Button from '@/components/button.vue'
import Chart from '@/components/chart.vue'
import PageLayout from '@/layouts/page-layout.vue'
import { useRandomIntegersStore } from '@/stores/useRandomIntegers.store.ts'
import { storeToRefs } from 'pinia'
const { integersArray } = storeToRefs(useRandomIntegersStore())
const { searchIntegers } = useRandomIntegersStore()
const handleSearchButtonClick = () => {
searchIntegers({ num: 20, min: 1, max: 10, col: 1, base: 2, format: 'plain' })
}
</script>

<template>
<div class="h-screen flex flex-col justify-center items-center gap-5">
<CounterCard msg="Vite + Vue + TS" />
</div>
<PageLayout>
<h1>{{ 'Distribution de nombres' }}</h1>

<div class="content-wrapper">
<Chart :recordValues="integersArray" />
<Button @click="handleSearchButtonClick"> Refresh </Button>
</div>
</PageLayout>
</template>

<style lang="css" scoped>
.content-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 32px;
}
</style>
12 changes: 11 additions & 1 deletion src/router/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useRandomIntegersStore } from '@/stores/useRandomIntegers.store.ts'
import { createRouter, createWebHistory } from 'vue-router'
import HomePage from '@/pages/home-page.vue'

Expand All @@ -7,7 +8,16 @@ const router = createRouter({
{
path: '/',
name: 'home',
component: HomePage
component: HomePage,
beforeEnter: () =>
useRandomIntegersStore().searchIntegers({
num: 20,
min: 1,
max: 10,
col: 1,
base: 2,
format: 'plain'
})
}
]
})
Expand Down
12 changes: 0 additions & 12 deletions src/stores/counter.ts

This file was deleted.

25 changes: 25 additions & 0 deletions src/stores/useRandomIntegers.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
searchRandomIntegers,
SearchRandomIntegersParams
} from '@/features/integersList/searchRandomIntegers.ts'
import { ref } from 'vue'
import { defineStore } from 'pinia'
export const useRandomIntegersStore = defineStore('integersList', () => {
const integersArray = ref<Record<number, number>>([])

const searchIntegers = async (params: SearchRandomIntegersParams) => {
integersArray.value = groupIntegersByValue(await searchRandomIntegers(params))
}

const groupIntegersByValue = (integersArray: string[]) => {
const occurrences: Record<string, number> = {};

integersArray.forEach((integer) => {
occurrences[integer] = (occurrences[integer] || 0) + 1;
});

return occurrences;
}

return { integersArray, searchIntegers }
})
7 changes: 7 additions & 0 deletions src/utils/http/httpClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const httpClient = async (input: RequestInfo, init?: RequestInit): Promise<Response> => {
const response = await fetch(input, init)
if (!response.ok) {
throw new Error(`[${response.status}] ${response.statusText}`)
}
return response
}
1 change: 1 addition & 0 deletions src/utils/http/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { POST } from './post.ts'
58 changes: 58 additions & 0 deletions src/utils/http/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { httpClient } from '@/utils/http/httpClient.ts'
export const POST = async (endpoint: string, body?: unknown): Promise<string[]> => {
const url = `${import.meta.env.VITE_API}${endpoint}`
const response = await httpClient(url, {
method: 'Post',
body: JSON.stringify(body) || null,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
})
const contentType = response.headers.get('Content-Type')

if (contentType === 'text/plain;charset=UTF-8') {
if (!response.body) {
return []
}

return decodeReadableStream(response.body)
}

/* if (contentType === 'application/json') return response.json() */

throw new Error('Type de contenu non pris en charge')
}

const decodeReadableStream = async (readableStream: ReadableStream): Promise<string[]> => {
const reader = readableStream.getReader()
const chunks: Uint8Array[] = []

let isDone = false

while (!isDone) {
const { done, value } = await reader.read()

if (done) {
isDone = true
}

if (value) {
chunks.push(value)
}
}

reader.releaseLock()

return decodeChunksToStringArray(chunks)
}

const decodeChunksToStringArray = (chunks: Uint8Array[]): string[] => {
return (
chunks.reduce(
(acc: string[], chunk: Uint8Array) =>
acc.concat(new TextDecoder().decode(chunk.buffer).split('\n').slice(0, -1)),
[]
) || []
)
}
19 changes: 19 additions & 0 deletions tests/components/button.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Button from '@/components/button.vue'
import { describe, it, expect, beforeEach } from 'vitest'

import { shallowMount, VueWrapper } from '@vue/test-utils'

describe('Button', () => {
let wrapper: VueWrapper
beforeEach(() => {
wrapper = shallowMount(Button, {
props: {
recordValues: { 20: 2, 15: 1 }
}
})
})

it('should render properly', () => {
expect(wrapper.exists()).toBe(true)
})
})
52 changes: 52 additions & 0 deletions tests/components/chart.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Chart from '@/components/chart.vue'
import { describe, it, expect, beforeEach } from 'vitest'

import { shallowMount, VueWrapper } from '@vue/test-utils'

describe('Chart', () => {
let wrapper: VueWrapper
beforeEach(() => {
wrapper = shallowMount(Chart, {
props: {
recordValues: { 20: 2, 15: 1 }
}
})
})

it('should render properly', () => {
expect(wrapper.exists()).toBe(true)
})

it('should render 2 bars given occurences of numbers', () => {
expect(wrapper.findAll('[data-testid="chart-bar"]').length).toBe(2)
})

it('should render the right label given occurrences of numbers', () => {
const [first, second] = wrapper.findAll('[data-testid="chart-bar-label"]')

expect(first.text()).toBe('15')
expect(second.text()).toBe('20')
})

it('should render 10 bars given occurences of numbers', async () => {
await wrapper.setProps({
recordValues: { 20: 2, 15: 1, 56: 5, 18: 6, 63: 1, 57: 7, 68: 8, 96: 5, 656: 5, 66: 6 }
})

expect(wrapper.findAll('[data-testid="chart-bar"]').length).toBe(10)
})

it('should render 2 bars given occurences of text', async () => {
await wrapper.setProps({ recordValues: { '20': 2, '15': 1 } })

expect(wrapper.findAll('[data-testid="chart-bar"]').length).toBe(2)
})

it('should render the right label given occurrences of text', async () => {
await wrapper.setProps({ recordValues: { '20': 2, '15': 1 } })
const [first, second] = wrapper.findAll('[data-testid="chart-bar-label"]')

expect(first.text()).toBe('15')
expect(second.text()).toBe('20')
})
})
Loading

0 comments on commit feee80a

Please sign in to comment.