Skip to content

Commit 42727f9

Browse files
committed
Added DevCard for dev list #39
* added DevCard.vue * added interfaces to GQL queries * added fields to dev details GQL query * very trippy * WIP
1 parent f601aaa commit 42727f9

File tree

3 files changed

+363
-9
lines changed

3 files changed

+363
-9
lines changed

stm_vue_ui/src/components/DevCard.vue

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
<script setup lang="ts">
2+
import type { DevListForStack, Tech } from "@/graphql/queries";
3+
import { useQueryStore } from "@/stores/QueryStore";
4+
import { computed } from "vue";
5+
6+
const store = useQueryStore();
7+
8+
const props = defineProps<{
9+
/** Expects _source property from the ElasticSearch GQL response for this dev */
10+
devDetails: DevListForStack;
11+
}>();
12+
13+
/** The name is optional and may be completely absent. Built it out of what we have. */
14+
const devName = computed(() => {
15+
if (!props.devDetails) return "devDetails not initialized yet";
16+
if (props.devDetails.name) return props.devDetails.name;
17+
if (props.devDetails.login) return props.devDetails.login;
18+
19+
return "Anonymous Software Engineer";
20+
});
21+
22+
/** DevID can either be a GH login or an internal STM owner ID. */
23+
const devId = computed(() => {
24+
if (!props.devDetails) return "devDetails not initialized yet";
25+
26+
if (props.devDetails.login) {
27+
return `/${props.devDetails.login}`;
28+
} else {
29+
return `/?dev=${props.devDetails.ownerId}`;
30+
}
31+
});
32+
33+
/** Returns TRUE if public contact details are available */
34+
const hasPublicContactDetails = computed(
35+
() =>
36+
props.devDetails &&
37+
props.devDetails.login &&
38+
(props.devDetails.blog || props.devDetails.email)
39+
);
40+
41+
/** Only the year component of the current date. */
42+
const yearNow = new Date().getFullYear();
43+
44+
/** Returns a phrase like `10 projects over 5 years` or an empty string if not enough data */
45+
const projectsOverYears = computed(() => {
46+
// try to get the number of projects - is there enough data?
47+
if (!props.devDetails?.report?.projectsIncluded) return "";
48+
const projectCount = props.devDetails.report.projectsIncluded.length;
49+
if (projectCount == 0) return "";
50+
51+
// get the year of the first commit
52+
// prefer first_contributor_commit_date_iso, then date_init, then now()
53+
// check if the data is present and is not in the future
54+
const firstContributorCommitDateIso = props.devDetails.report
55+
.firstContributorCommitDateIso
56+
? Number.parseInt(
57+
props.devDetails.report.firstContributorCommitDateIso.substring(0, 4)
58+
)
59+
: 0;
60+
61+
const dateInit = props.devDetails.report.dateInit
62+
? Number.parseInt(props.devDetails.report.dateInit.substring(0, 4))
63+
: 0;
64+
65+
const firstCommitYear =
66+
firstContributorCommitDateIso > 0 &&
67+
firstContributorCommitDateIso <= yearNow
68+
? firstContributorCommitDateIso
69+
: dateInit > 0 && dateInit <= yearNow
70+
? dateInit
71+
: yearNow;
72+
73+
// calculate the total number of years of experience
74+
const years = yearNow - firstCommitYear + 1;
75+
76+
// get plural or singular form
77+
const msgYearsPart = years > 1 ? "years" : "year";
78+
const msgProjectsPart = years > 1 ? "projects" : "project";
79+
80+
// build the final output
81+
return `${projectCount} ${msgProjectsPart} over ${years} ${msgYearsPart}`;
82+
});
83+
84+
/** Returns a list of languages that are in the dev's stack and the search filter */
85+
const matchingLanguages = computed(() => {
86+
if (!props.devDetails?.report?.tech) return [];
87+
88+
// get the list of languages from the search filter in an array form
89+
const listOfFilterLangs = Array.from(store.tech.keys()).map((key) =>
90+
key.toLowerCase()
91+
);
92+
93+
// create an array of techs present in the filter
94+
const matchingLangs = props.devDetails.report.tech
95+
.map((tech) =>
96+
listOfFilterLangs.includes(tech.language?.toLowerCase()) ? tech : null
97+
)
98+
.filter((n) => n) as Tech[];
99+
100+
// languages with most code lines come first
101+
matchingLangs.sort((a, b) => (b ? b.codeLines : 0) - (a ? a.codeLines : 0));
102+
103+
return matchingLangs;
104+
});
105+
106+
/** Returns a list of languages not in the list of search filter */
107+
const otherLanguages = computed(() => {
108+
if (!props.devDetails?.report?.tech) return [];
109+
110+
// get the list of languages from the search filter in an array form
111+
const listOfFilterLangs = Array.from(store.tech.keys()).map((key) =>
112+
key.toLowerCase()
113+
);
114+
115+
// create an array of techs absent from the filter
116+
const otherLangs = props.devDetails.report.tech
117+
.map((tech) =>
118+
listOfFilterLangs.includes(tech.language?.toLowerCase()) ? null : tech
119+
)
120+
.filter((n) => n) as Tech[];
121+
122+
// languages with most code lines come first
123+
otherLangs.sort((a, b) => (b ? b.codeLines : 0) - (a ? a.codeLines : 0));
124+
125+
return otherLangs;
126+
});
127+
128+
/** Returns a map of package names and their count. */
129+
const matchingPkgs = computed(() => {
130+
// an output collector
131+
const matchingPkgs = new Map<string, number>();
132+
133+
if (!props.devDetails?.report?.tech) return [];
134+
135+
// get the list of languages from the search filter in an array form
136+
const listOfFilterPkgs = Array.from(store.pkg).map((value) =>
137+
value.toLowerCase()
138+
);
139+
140+
// collect all dev packages present in the search filter and tot up their counts
141+
for (let tech of props.devDetails.report.tech) {
142+
if (tech.refs) {
143+
// pkgs per tech can be in 2 locations - refs and pkgs
144+
for (let pkg of tech.refs) {
145+
const k = pkg.k.toLowerCase();
146+
// match with the filter
147+
for (let kw of listOfFilterPkgs) {
148+
if (k.includes(kw)) {
149+
const c = matchingPkgs.get(kw);
150+
matchingPkgs.set(kw, c ? c + pkg.c : pkg.c);
151+
}
152+
}
153+
}
154+
}
155+
if (tech.pkgs) {
156+
for (let pkg of tech.pkgs) {
157+
const k = pkg.k.toLowerCase();
158+
// match with the filter
159+
for (let kw of listOfFilterPkgs) {
160+
if (k.includes(kw)) {
161+
const c = matchingPkgs.get(kw);
162+
matchingPkgs.set(kw, c ? c + pkg.c : pkg.c);
163+
}
164+
}
165+
}
166+
}
167+
}
168+
169+
return Array.from(matchingPkgs, ([k, c]) => ({ k, c })).sort(
170+
(a, b) => b.c - a.c
171+
);
172+
});
173+
174+
/** Formats number of months into years + months in 5 month increment, depending on how many years.
175+
* E.g. 0.5y, 2.5y, 3y or 5y.
176+
*/
177+
function months_to_years(months?: number) {
178+
const blankValue = "n/a";
179+
if (!months) return blankValue;
180+
181+
// calculate the remainder of months ahead of time
182+
const remainder = months % 12 >= 6 ? ".5" : "";
183+
184+
// add the remainder to years only if it's less than 3 years
185+
let years = "";
186+
if (months < 12) {
187+
years = "< 1";
188+
} else if (months < 36) {
189+
years = Math.floor(months / 12).toString() + remainder;
190+
} else {
191+
years = Math.floor(months / 12).toString();
192+
}
193+
194+
return years + "y";
195+
}
196+
197+
/** Returns a simplified number, e.g 1,327 -> 1.3K. */
198+
function shorten_num(v?: number) {
199+
if (!v) return "0";
200+
201+
let txt = "";
202+
if (v < 1000) {
203+
txt = "< 1K";
204+
} else if (v >= 1_000 && v < 10_000.0) {
205+
txt = `${Math.round(v / 1000).toPrecision(1)}K`;
206+
} else if (v >= 10_000 && v < 1_000_000) {
207+
txt = `${v / 1000}K`;
208+
} else {
209+
txt = `${Math.round(v / 1_000_000.0).toPrecision(1)}M`;
210+
}
211+
212+
return txt;
213+
}
214+
215+
/** Formats integer numbers with commas for readability, e.g. 100000 -> 10,000. */
216+
function pretty_num(v?: number) {
217+
if (!v) return "";
218+
219+
return Math.round(v)
220+
.toString()
221+
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
222+
}
223+
</script>
224+
225+
<template>
226+
<div class="card mb-4">
227+
<div class="card-body">
228+
<div
229+
class="d-flex justify-content-between align-items-center"
230+
width="100%"
231+
height="50"
232+
>
233+
<h5 class="card-title ma-1">
234+
<a :href="devId">
235+
{{ devName }}
236+
</a>
237+
</h5>
238+
</div>
239+
240+
<p class="card-subtitle mb-2">
241+
<span class="me-2 text-muted">
242+
{{ projectsOverYears }}
243+
</span>
244+
<a
245+
v-if="hasPublicContactDetails"
246+
href="https://github.com/{{dev._source.login}}"
247+
title="Contact details available on GitHub"
248+
><span class="badge bg-success">Contact</span></a
249+
>
250+
<span v-if="props.devDetails.location" class="ms-1">{{
251+
props.devDetails.location
252+
}}</span>
253+
</p>
254+
255+
<ul class="list-inline">
256+
<li
257+
v-for="tech in matchingLanguages"
258+
:key="tech.language"
259+
class="list-inline-item bg-light text-dark py-1 px-2 rounded mb-3 me-3 border border-success"
260+
>
261+
<h6 class="mb-1">{{ tech.language }}</h6>
262+
<span class="fw-light smaller-90">
263+
<span class="calendar-badge me-3">{{
264+
months_to_years(tech.history?.months)
265+
}}</span>
266+
<span class="loc-badge">{{ shorten_num(tech?.codeLines) }}</span>
267+
</span>
268+
</li>
269+
270+
<li
271+
v-for="pkg in matchingPkgs"
272+
:key="pkg.k"
273+
class="list-inline-item bg-light text-dark py-1 px-2 rounded mb-3 me-3 border border-success"
274+
>
275+
<h6 class="mb-1">{{ pkg.k }}</h6>
276+
<span class="fw-light smaller-90">
277+
<span class="libs-badge">
278+
{{ pretty_num(pkg.c) }} mention{{ pkg.c > 1 ? "s" : "" }}
279+
</span>
280+
</span>
281+
</li>
282+
283+
<li
284+
v-for="tech in otherLanguages"
285+
:key="tech.language"
286+
class="list-inline-item bg-light text-dark py-1 px-2 rounded mb-3 me-3 border"
287+
>
288+
<h6 class="mb-1 fw-light">{{ tech.language }}</h6>
289+
<span class="fw-light smaller-90">
290+
<span class="calendar-badge me-3">
291+
{{ months_to_years(tech.history?.months) }}
292+
</span>
293+
<span class="loc-badge">{{ shorten_num(tech.codeLines) }}</span>
294+
</span>
295+
</li>
296+
</ul>
297+
</div>
298+
</div>
299+
</template>

stm_vue_ui/src/components/MatchingDevsList.vue

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { devListForStack } from "@/graphql/queries";
33
import { useQuery } from "@vue/apollo-composable";
44
import { useQueryStore } from "@/stores/QueryStore";
5+
import DevCard from "./DevCard.vue";
56
67
const store = useQueryStore();
78
@@ -13,15 +14,17 @@ const { result, loading, error } = useQuery(devListForStack, store.stackVar);
1314
<span v-if="loading"> Loading ...</span>
1415
<span v-else>List of Devs</span>
1516
</h6>
16-
<ul class="text-muted list-inline">
17-
<li
18-
v-for="dev in result?.devListForStack"
19-
:key="dev.login"
20-
class="me-3 mb-3 bg-light text-dark rounded border text-wrap p-1 list-inline-item"
21-
>
22-
{{ dev.login }}
23-
</li>
24-
</ul>
17+
18+
<h2
19+
class="pe-md-5 text-muted"
20+
v-if="!result || result.devListForStack.length == 0"
21+
>
22+
Could not find anyone with these exact skills
23+
</h2>
24+
<div v-for="dev in result?.devListForStack" :key="dev.login">
25+
<DevCard :dev-details="dev" />
26+
</div>
27+
2528
<p v-if="error" class="text-danger">
2629
<small>{{ error }}</small>
2730
</p>

0 commit comments

Comments
 (0)