Merge branch '9-implement-select-2-and-details-buttons-in-companies-page' into 'master'
Resolve "Implement "Select 2" and "Details" buttons in companies page" Closes #8 and #9 See merge request usi-si-teaching/msde/2022-2023/visual-analytics-atelier/group-projects/group-1!8
This commit is contained in:
commit
8bf18c9c9e
5 changed files with 101 additions and 38 deletions
|
@ -11,6 +11,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/font": "^7.2.96",
|
"@mdi/font": "^7.2.96",
|
||||||
"core-js": "^3.29.0",
|
"core-js": "^3.29.0",
|
||||||
|
"debounce": "^1.2.1",
|
||||||
|
"fuse.js": "^6.6.2",
|
||||||
"roboto-fontface": "*",
|
"roboto-fontface": "*",
|
||||||
"vue": "^3.2.0",
|
"vue": "^3.2.0",
|
||||||
"vue-router": "^4.0.0",
|
"vue-router": "^4.0.0",
|
||||||
|
@ -19,6 +21,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/types": "^7.21.4",
|
"@babel/types": "^7.21.4",
|
||||||
|
"@types/debounce": "^1.2.1",
|
||||||
"@types/node": "^18.15.0",
|
"@types/node": "^18.15.0",
|
||||||
"@types/webfontloader": "^1.6.35",
|
"@types/webfontloader": "^1.6.35",
|
||||||
"@vitejs/plugin-vue": "^3.0.3",
|
"@vitejs/plugin-vue": "^3.0.3",
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-app-bar color="primary">
|
|
||||||
<v-app-bar-title>
|
|
||||||
Stockingly
|
|
||||||
</v-app-bar-title>
|
|
||||||
|
|
||||||
<v-tabs centered>
|
|
||||||
<v-tab v-for="link in links" :key="link">
|
|
||||||
{{ link }}
|
|
||||||
</v-tab>
|
|
||||||
</v-tabs>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</v-app-bar>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
const links = [
|
|
||||||
'Dashboard',
|
|
||||||
'Messages',
|
|
||||||
'Profile',
|
|
||||||
'Updates',
|
|
||||||
];
|
|
||||||
</script>
|
|
|
@ -1,12 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<v-app>
|
<v-app>
|
||||||
<default-bar />
|
|
||||||
|
|
||||||
<default-view />
|
<default-view />
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import DefaultBar from './AppBar.vue'
|
|
||||||
import DefaultView from './View.vue'
|
import DefaultView from './View.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,4 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
|
<v-app-bar color="primary">
|
||||||
|
<v-app-bar-title>
|
||||||
|
Stockingly
|
||||||
|
</v-app-bar-title>
|
||||||
|
|
||||||
|
<v-text-field hide-details prepend-icon="mdi-magnify" single-line v-model="searchText"
|
||||||
|
placeholder="Search company..." />
|
||||||
|
<v-btn icon @click="clearSearch">
|
||||||
|
<v-icon>mdi-backspace-outline</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-app-bar>
|
||||||
<v-container class="fill-height">
|
<v-container class="fill-height">
|
||||||
<v-row>
|
<v-row>
|
||||||
<template v-if="loading" v-for="i in Array.from({ length: 10 }, (_, i) => i)" :key="i">
|
<template v-if="loading" v-for="i in Array.from({ length: 10 }, (_, i) => i)" :key="i">
|
||||||
|
@ -6,11 +17,10 @@
|
||||||
<v-skeleton-loader class="mx-auto" type="card"></v-skeleton-loader>
|
<v-skeleton-loader class="mx-auto" type="card"></v-skeleton-loader>
|
||||||
</v-col>
|
</v-col>
|
||||||
</template>
|
</template>
|
||||||
<template v-else v-for="company in companies" :key="company.ticker">
|
<template v-else v-for="company in filteredCompanies" :key="company.ticker">
|
||||||
<v-col cols="12" md="6" lg="4">
|
<v-col cols="12" md="6" lg="4">
|
||||||
<v-card class="ma-1 fill-height">
|
<v-card class="ma-1 fill-height d-flex flex-column">
|
||||||
|
<div class="d-flex stretch align-center flex-0">
|
||||||
<div class="d-flex stretch align-center">
|
|
||||||
<div class="flex-0 pa-3">
|
<div class="flex-0 pa-3">
|
||||||
<img style="max-height: 36px; min-height: 24px;" :src="company.logoSrc" />
|
<img style="max-height: 36px; min-height: 24px;" :src="company.logoSrc" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,7 +30,7 @@
|
||||||
</v-card-item>
|
</v-card-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-card-text>
|
<v-card-text class="flex-0">
|
||||||
<p class="text--primary">
|
<p class="text--primary">
|
||||||
{{ company.description }}
|
{{ company.description }}
|
||||||
</p>
|
</p>
|
||||||
|
@ -33,7 +43,7 @@
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<div class="px-4">
|
<div class="px-4 flex-0">
|
||||||
<v-chip label color="pink" class="ma-1">
|
<v-chip label color="pink" class="ma-1">
|
||||||
<v-icon start icon="mdi-chart-timeline-variant"></v-icon>
|
<v-icon start icon="mdi-chart-timeline-variant"></v-icon>
|
||||||
{{ company.ticker }}
|
{{ company.ticker }}
|
||||||
|
@ -43,22 +53,82 @@
|
||||||
{{ formatCurrency(company['market cap']) }}
|
{{ formatCurrency(company['market cap']) }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
<template v-for="tag in company.tags" :key="tag">
|
<template v-for="tag in company.tags" :key="tag">
|
||||||
<v-chip label class="ma-1">{{ tag }}</v-chip>
|
<v-chip label class="ma-1" :color="searchText == tag ? 'teal' : void (0)" @click="setSearch(tag)">{{ tag
|
||||||
|
}}</v-chip>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<v-card-actions class="flex-1">
|
||||||
|
<v-btn :variant="isSelected(company.ticker) ? 'tonal' : 'text'"
|
||||||
|
@click="(isSelected(company.ticker) ? unselect : select)(company.ticker)"
|
||||||
|
:color="isSelected(company.ticker) ? 'orange-darken-4' : void (0)">
|
||||||
|
{{ isSelected(company.ticker) ? 'Unselect' : 'Select' }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</template>
|
</template>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
<v-sheet class="d-flex align-center justify-center" style="position: fixed; bottom: 0; width: 100%"
|
||||||
|
v-if="selected.length > 0" color="blue-grey-lighten-5" :elevation="6">
|
||||||
|
<small class="ma-2">{{ selected.length }} / {{ MAX_SELECT }} companies selected:</small>
|
||||||
|
<v-chip class="ma-2" color="secondary" closable style="flex: initial !important" v-for="s in selected" :key="s"
|
||||||
|
@click:close="unselect(s)">{{ s }}</v-chip>
|
||||||
|
<v-btn class="ma-2" variant="flat" size="small" color="primary" prepend-icon="mdi-chart-areaspline-variant">
|
||||||
|
Compare
|
||||||
|
</v-btn>
|
||||||
|
<v-btn class="ma-2" size="small" variant="flat" color="error" @click="selected.splice(0, selected.length)"
|
||||||
|
prepend-icon="mdi-delete">
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
</v-sheet>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { getCompanies, Company } from '@/api';
|
import { getCompanies, Company } from '@/api';
|
||||||
import { ref, reactive, computed, watch } from 'vue';
|
import { ref, reactive, computed, watch } from 'vue';
|
||||||
|
import debounce from 'debounce';
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
|
const MAX_SELECT = 3;
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const companies = reactive<Company[]>([]);
|
const companies = ref<Company[]>([]);
|
||||||
|
const filteredCompanies = ref<(Company & { score?: number })[]>([]);
|
||||||
|
const selected = reactive<string[]>([]);
|
||||||
|
const searchText = ref("");
|
||||||
|
let fuse: Fuse<Company> | undefined = undefined;
|
||||||
|
|
||||||
|
watch(searchText, debounce(() => {
|
||||||
|
if (!fuse) return;
|
||||||
|
if (!searchText.value) {
|
||||||
|
filteredCompanies.value = companies.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
loading.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
filteredCompanies.value = fuse!.search(searchText.value).map(e => ({ ...e.item, score: e.score }))
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}, 300))
|
||||||
|
|
||||||
|
const isSelected = (ticker: string) => selected.indexOf(ticker) !== -1;
|
||||||
|
|
||||||
|
const unselect = (ticker: string) => {
|
||||||
|
if (isSelected(ticker)) selected.splice(selected.indexOf(ticker), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const select = (ticker: string) => {
|
||||||
|
if (!isSelected(ticker) && selected.length < MAX_SELECT) selected.push(ticker);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearSearch = () => searchText.value = '';
|
||||||
|
|
||||||
|
const setSearch = (text: string) => searchText.value = text;
|
||||||
|
|
||||||
|
watch(filteredCompanies, () => loading.value = false);
|
||||||
|
|
||||||
interface Metric {
|
interface Metric {
|
||||||
title: string,
|
title: string,
|
||||||
|
@ -92,10 +162,11 @@ const metrics = computed<(Metric & { percentage: (c: Company) => number })[]>(()
|
||||||
percentage: (c: Company) => (e.value(c) - e.minValue) * 100 / (e.maxValue - e.minValue)
|
percentage: (c: Company) => (e.value(c) - e.minValue) * 100 / (e.maxValue - e.minValue)
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
|
||||||
getCompanies().then(cs => {
|
getCompanies().then(cs => {
|
||||||
loading.value = false;
|
companies.value = cs;
|
||||||
companies.push(...cs);
|
const myIndex = Fuse.createIndex(['ceo', 'company name', 'description', 'sector', 'tags', 'ticker', 'website'], cs);
|
||||||
|
fuse = new Fuse(cs, { threshold: 0.5, ignoreLocation: true }, myIndex);
|
||||||
|
filteredCompanies.value = cs;
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatCurrency = (d: number) => {
|
const formatCurrency = (d: number) => {
|
||||||
|
|
|
@ -218,6 +218,11 @@
|
||||||
"@nodelib/fs.scandir" "2.1.5"
|
"@nodelib/fs.scandir" "2.1.5"
|
||||||
fastq "^1.6.0"
|
fastq "^1.6.0"
|
||||||
|
|
||||||
|
"@types/debounce@^1.2.1":
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.2.1.tgz#79b65710bc8b6d44094d286aecf38e44f9627852"
|
||||||
|
integrity sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==
|
||||||
|
|
||||||
"@types/json-schema@^7.0.9":
|
"@types/json-schema@^7.0.9":
|
||||||
version "7.0.11"
|
version "7.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
||||||
|
@ -714,6 +719,11 @@ de-indent@^1.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
||||||
integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
|
integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
|
||||||
|
|
||||||
|
debounce@^1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
|
||||||
|
integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==
|
||||||
|
|
||||||
debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
|
debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
|
||||||
version "4.3.4"
|
version "4.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||||
|
@ -991,6 +1001,11 @@ fsevents@~2.3.2:
|
||||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||||
|
|
||||||
|
fuse.js@^6.6.2:
|
||||||
|
version "6.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.6.2.tgz#fe463fed4b98c0226ac3da2856a415576dc9a111"
|
||||||
|
integrity sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==
|
||||||
|
|
||||||
glob-parent@^5.1.2, glob-parent@~5.1.2:
|
glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||||
version "5.1.2"
|
version "5.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||||
|
|
Reference in a new issue