mirror of
https://github.com/Aviortheking/codestats-readme.git
synced 2025-07-29 07:19:51 +00:00
Misc Update
Signed-off-by: Florian Bouillon <florian.bouillon@delta-wings.net>
This commit is contained in:
@ -1,13 +1,16 @@
|
||||
import { formatDateNumber, getColorOfLanguage } from "../common/utils"
|
||||
import Card, { CardOptions } from '../common/Card'
|
||||
import React from 'react'
|
||||
import FlexLayout from "../components/FlexLayout"
|
||||
import { formatDateNumber, getColor, getColorOfLanguage } from '../common/utils'
|
||||
import Card, { CardOptions } from '../common/Card'
|
||||
import FlexLayout from '../components/FlexLayout'
|
||||
|
||||
interface HistoryOptions extends CardOptions {
|
||||
width?: number
|
||||
height?: number
|
||||
layout?: 'horizontal'
|
||||
hide_legend?: boolean
|
||||
text_color?: string
|
||||
title?: string
|
||||
reverse_order?: boolean
|
||||
|
||||
// Put language in Other
|
||||
hide?: Array<string>
|
||||
@ -18,23 +21,19 @@ export default class HistoryCard extends Card {
|
||||
|
||||
private topLanguages: Array<string>
|
||||
|
||||
private legendMinWidth = 180
|
||||
|
||||
public constructor(
|
||||
private username: string,
|
||||
private days: Array<{day: string, total: number, data: Array<{xp: number, language: string}>}>,
|
||||
private options: HistoryOptions
|
||||
) {
|
||||
super(options)
|
||||
|
||||
this.height = 45 + (this.days.length + 1) * 40
|
||||
this.width = options.width ?? 500
|
||||
if (options.layout === 'horizontal') {
|
||||
this.width = 45 + (this.days.length + 1) * 40 + 100
|
||||
this.height = options.height ?? 300
|
||||
}
|
||||
this.processOptions()
|
||||
|
||||
const languagesToHide = options.hide || []
|
||||
|
||||
let languageCount: Array<{language: string, xp: number}> = []
|
||||
const languageCount: Array<{language: string, xp: number}> = []
|
||||
for (const day of this.days) {
|
||||
for (const data of day.data) {
|
||||
let index = languageCount.findIndex((item) => item.language === data.language)
|
||||
@ -56,30 +55,57 @@ export default class HistoryCard extends Card {
|
||||
this.topLanguages.push('Other')
|
||||
}
|
||||
|
||||
this.hideLanguages(languagesToHide)
|
||||
}
|
||||
|
||||
private hideLanguages(languagesToHide: Array<string>) {
|
||||
for (const day of this.days) {
|
||||
// Prepare array of indexes to remove
|
||||
const toRemove: Array<number> = []
|
||||
|
||||
// Loop through data
|
||||
for (let i = 0; i < day.data.length; i++) {
|
||||
const element = day.data[i];
|
||||
if (languagesToHide.includes(element.language)) {
|
||||
const otherIndex = day.data.findIndex((el) => el.language === 'Other')
|
||||
if (otherIndex === -1) {
|
||||
day.data.push({
|
||||
language: 'Other',
|
||||
xp: element.xp
|
||||
})
|
||||
} else {
|
||||
day.data[otherIndex].xp += element.xp
|
||||
}
|
||||
toRemove.push(i)
|
||||
const element = day.data[i]
|
||||
|
||||
// If Language should not be hidden: goto next
|
||||
if (!languagesToHide.includes(element.language)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Search indexOf 'Others'
|
||||
const otherIndex = day.data.findIndex((el) => el.language === 'Others')
|
||||
if (otherIndex === -1) {
|
||||
day.data.push({
|
||||
language: 'Others',
|
||||
xp: element.xp
|
||||
})
|
||||
} else {
|
||||
day.data[otherIndex].xp += element.xp
|
||||
}
|
||||
toRemove.push(i)
|
||||
}
|
||||
|
||||
// Reverse array and remove each indexes
|
||||
for (const index of toRemove.reverse()) {
|
||||
day.data.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.title = 'Code History Language breakdown'
|
||||
this.css = ProgressNode.getCSS('#000')
|
||||
private processOptions() {
|
||||
this.height = 45 + (this.days.length + 1) * 40
|
||||
this.width = this.options.width ?? 500
|
||||
if (this.options.layout === 'horizontal') {
|
||||
this.width = 45 + (this.days.length + 1) * 40 + (this.options.hide_legend ? 0 : this.legendMinWidth)
|
||||
this.height = this.options.height ?? 300
|
||||
}
|
||||
|
||||
this.title = this.options.title ?? `Last ${this.days.length} days XP history`
|
||||
this.css = ProgressNode.getCSS(getColor('text_color', this.options.text_color, this.options.theme))
|
||||
|
||||
if (this.options.reverse_order) {
|
||||
this.days = this.days.reverse()
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
@ -90,57 +116,71 @@ export default class HistoryCard extends Card {
|
||||
return prvs
|
||||
}, 0)
|
||||
|
||||
const legendWidth = Math.max(this.width * 20 / 100 + 60, 150)
|
||||
const legendWidth = this.options.hide_legend ? 0 : Math.max(this.width * 20 / 100 + 60, this.legendMinWidth)
|
||||
const historyWidth = this.width - legendWidth
|
||||
return super.render(
|
||||
<svg x="25">
|
||||
<FlexLayout items={[(() => {
|
||||
if (this.options.layout === 'horizontal') {
|
||||
return (
|
||||
<FlexLayout items={
|
||||
this.days.reverse().map((el, index) => (
|
||||
<VerticalProgressNode
|
||||
{...el}
|
||||
totalTotal={totalTotal} height={this.height - 120} />
|
||||
))
|
||||
}
|
||||
gap={40}
|
||||
// direction="column"
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<FlexLayout items={
|
||||
this.days.map((el, index) => (
|
||||
<ProgressNode
|
||||
{...el}
|
||||
totalTotal={totalTotal} width={historyWidth - 30} />
|
||||
))
|
||||
}
|
||||
gap={40}
|
||||
direction="column"
|
||||
/>
|
||||
)
|
||||
}
|
||||
})(), (
|
||||
<FlexLayout items={
|
||||
|
||||
const items: Array<JSX.Element> = []
|
||||
|
||||
// Format History bars
|
||||
const history = this.options.layout === 'horizontal' ? (
|
||||
<FlexLayout
|
||||
key={0}
|
||||
items={
|
||||
this.days.reverse().map((el, index) => (
|
||||
<VerticalProgressNode
|
||||
key={index}
|
||||
{...el}
|
||||
totalTotal={totalTotal}
|
||||
height={this.height - 120}
|
||||
/>
|
||||
))
|
||||
}
|
||||
gap={40}
|
||||
/>
|
||||
) : (
|
||||
<FlexLayout
|
||||
key={0}
|
||||
items={
|
||||
this.days.map((el, index) => (
|
||||
<ProgressNode
|
||||
key={index}
|
||||
{...el}
|
||||
totalTotal={totalTotal} width={historyWidth - 60} />
|
||||
))
|
||||
}
|
||||
gap={40}
|
||||
direction="column"
|
||||
/>
|
||||
)
|
||||
items.push(history)
|
||||
|
||||
if (!this.options.hide_legend) {
|
||||
items.push(
|
||||
<FlexLayout
|
||||
key={1}
|
||||
items={
|
||||
this.topLanguages.map((el, index) => (
|
||||
<>
|
||||
<React.Fragment key={index}>
|
||||
<rect rx="5" x="2" y="7" height="12" width="12" fill={getColorOfLanguage(el)} />
|
||||
<text x="18" y="18" className="lang-name" key={index}>{el}</text>
|
||||
</>
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
gap={20}
|
||||
direction="column"
|
||||
/>
|
||||
)]}
|
||||
gap={this.options.layout === 'horizontal' ? this.width - 180 : historyWidth - 20}
|
||||
gap={20}
|
||||
direction="column"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return super.render(
|
||||
<svg x="25">
|
||||
<FlexLayout items={items}
|
||||
gap={historyWidth}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -159,7 +199,7 @@ class ProgressNode extends React.Component<{
|
||||
}
|
||||
.xp-txt {
|
||||
font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif;
|
||||
fill: ${textColor};
|
||||
fill: black;
|
||||
}
|
||||
.xp-txt-invert {
|
||||
font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif;
|
||||
@ -197,27 +237,30 @@ class ProgressNode extends React.Component<{
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{(() => {
|
||||
let size = this.calcSize(offset) + 6
|
||||
const txtSize = (this.props.total.toString().length + 3) * 8
|
||||
let classes = 'xp-txt'
|
||||
if (size + txtSize >= this.calcSize(this.props.totalTotal)) {
|
||||
size -= txtSize
|
||||
classes += ' xp-txt-invert'
|
||||
}
|
||||
return (
|
||||
<text x={size} y="33" className={classes}>{this.props.total} XP</text>
|
||||
)
|
||||
})()}
|
||||
{this.getXPText(offset)}
|
||||
|
||||
</svg>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
protected calcSize(number: number) {
|
||||
private calcSize(number: number) {
|
||||
return number * this.props.width / this.props.totalTotal
|
||||
}
|
||||
|
||||
private getXPText(offset: number) {
|
||||
let size = this.calcSize(offset) + 6
|
||||
const txtSize = (this.props.total.toString().length + 3) * 8
|
||||
let classes = 'xp-txt'
|
||||
if (size + txtSize >= this.calcSize(this.props.totalTotal)) {
|
||||
size -= txtSize
|
||||
classes += ' xp-txt-invert'
|
||||
}
|
||||
return (
|
||||
<text x={size} y="33" className={classes}>{this.props.total} XP</text>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -228,6 +271,7 @@ class VerticalProgressNode extends React.Component<{
|
||||
data: Array<{xp: number, language: string}>
|
||||
height: number
|
||||
}> {
|
||||
|
||||
public render() {
|
||||
let offset = this.props.totalTotal
|
||||
const maskId = `mask-${this.props.day}`
|
||||
@ -244,26 +288,26 @@ class VerticalProgressNode extends React.Component<{
|
||||
offset -= el.xp
|
||||
return (
|
||||
<rect
|
||||
key={index}
|
||||
mask={`url(#${maskId})`}
|
||||
width="16"
|
||||
fill={color}
|
||||
y={25 + this.calcSize(offset)}
|
||||
x="0"
|
||||
height={`${this.calcSize(el.xp)}px`}
|
||||
key={index}
|
||||
mask={`url(#${maskId})`}
|
||||
width="16"
|
||||
fill={color}
|
||||
y={25 + this.calcSize(offset)}
|
||||
x="0"
|
||||
height={`${this.calcSize(el.xp)}px`}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
)
|
||||
})}
|
||||
{this.getXPTxt()}
|
||||
|
||||
</svg>
|
||||
<text x="2" y={this.props.height + 18} className="subtitle">{new Date(this.props.day).toDateString().substr(4, 3)}</text>
|
||||
<text x="5" y={this.props.height + 34} className="subtitle">{formatDateNumber(new Date(this.props.day).getDate())}</text>
|
||||
<text x="6" y={this.props.height + 34} className="subtitle">{formatDateNumber(new Date(this.props.day).getDate())}</text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
protected calcSize(number: number) {
|
||||
private calcSize(number: number) {
|
||||
return number * this.props.height / this.props.totalTotal
|
||||
}
|
||||
|
||||
@ -279,4 +323,5 @@ class VerticalProgressNode extends React.Component<{
|
||||
<text transform={`rotate(90, 4, ${position})`} letterSpacing="5" y={position} x="4" rotate="-90" className={classes}>{this.props.total} XP</text>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,14 @@
|
||||
import { kFormatter, encodeHTML, getProgress, getLevel, parseBoolean, calculateCircleProgress, getColor } from '../common/utils'
|
||||
import React from 'react'
|
||||
import {
|
||||
kFormatter,
|
||||
encodeHTML,
|
||||
getProgress,
|
||||
getLevel,
|
||||
calculateCircleProgress,
|
||||
getColor
|
||||
} from '../common/utils'
|
||||
import icons from '../common/icons'
|
||||
import Card, { CardOptions } from '../common/Card'
|
||||
import React from 'react'
|
||||
import FlexLayout from '../components/FlexLayout'
|
||||
|
||||
interface ProfileCardOptions extends CardOptions {
|
||||
@ -14,6 +21,7 @@ interface ProfileCardOptions extends CardOptions {
|
||||
}
|
||||
|
||||
export default class ProfileCard extends Card {
|
||||
|
||||
private stats: Record<string, {
|
||||
icon: JSX.Element
|
||||
label: string
|
||||
@ -39,14 +47,14 @@ export default class ProfileCard extends Card {
|
||||
this.stats = {
|
||||
xp: {
|
||||
icon: icons.star,
|
||||
label: "XP",
|
||||
value: xp,
|
||||
label: 'XP',
|
||||
value: xp
|
||||
},
|
||||
recent_xp: {
|
||||
icon: icons.commits,
|
||||
label: 'Recent xp',
|
||||
value: this.recentXp,
|
||||
},
|
||||
value: this.recentXp
|
||||
}
|
||||
}
|
||||
|
||||
// Card Settings
|
||||
@ -57,7 +65,7 @@ export default class ProfileCard extends Card {
|
||||
)
|
||||
|
||||
this.title = `${encodeHTML(this.username)}${
|
||||
["x", "s"].includes(this.username.slice(-1)) ? "" : "s"
|
||||
['x', 's'].includes(this.username.slice(-1)) ? '' : 's'
|
||||
} Code::Stats Profile`
|
||||
|
||||
const textColor = getColor('text_color', options.text_color, options.theme)
|
||||
@ -67,7 +75,7 @@ export default class ProfileCard extends Card {
|
||||
this.css += RankCircle.getCSS(textColor, iconColor, getProgress(xp))
|
||||
}
|
||||
if ((this.options.hide || []) < Object.keys(this.stats)) {
|
||||
this.css += TextNode.getCSS(textColor, !!this.options.show_icons ? iconColor : undefined)
|
||||
this.css += TextNode.getCSS(textColor, this.options.show_icons ? iconColor : undefined)
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,18 +85,23 @@ export default class ProfileCard extends Card {
|
||||
<RankCircle xp={this.options.hide_rank ? undefined : this.xp} />
|
||||
<svg x="0" y="0">
|
||||
<FlexLayout
|
||||
items={Object.keys(this.stats).filter((item) => !(this.options.hide || []).includes(item)).map((el, index) => {
|
||||
const item = this.stats[el]
|
||||
return (
|
||||
<TextNode
|
||||
{...item}
|
||||
icon={!!this.options.show_icons ? item.icon : undefined}
|
||||
key={index}
|
||||
index={index}
|
||||
items={
|
||||
Object
|
||||
.keys(this.stats)
|
||||
.filter((item) => !(this.options.hide || []).includes(item))
|
||||
.map((el, index) => {
|
||||
const item = this.stats[el]
|
||||
return (
|
||||
<TextNode
|
||||
{...item}
|
||||
icon={this.options.show_icons ? item.icon : undefined}
|
||||
key={index}
|
||||
index={index}
|
||||
|
||||
/>
|
||||
)
|
||||
})}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
gap={this.options.line_height || this.defaults.line_height}
|
||||
direction="column"
|
||||
/>
|
||||
@ -96,6 +109,7 @@ export default class ProfileCard extends Card {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class RankCircle extends React.Component<{
|
||||
@ -136,7 +150,7 @@ class RankCircle extends React.Component<{
|
||||
}
|
||||
`
|
||||
|
||||
public render () {
|
||||
public render() {
|
||||
if (!this.props.xp) {
|
||||
return undefined
|
||||
}
|
||||
@ -160,6 +174,7 @@ class RankCircle extends React.Component<{
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TextNode extends React.Component<{
|
||||
@ -200,7 +215,11 @@ class TextNode extends React.Component<{
|
||||
) : undefined
|
||||
|
||||
return (
|
||||
<g className="stagger" data-index={this.props.index} style={{animationDelay: `${delay}ms`}} transform="translate(25, 0)">
|
||||
<g
|
||||
className="stagger"
|
||||
style={{ animationDelay: `${delay}ms` }}
|
||||
transform="translate(25, 0)"
|
||||
>
|
||||
{icon}
|
||||
<text className="stat bold" x={this.props.icon ? 25 : undefined} y="12.5">{this.props.label}:</text>
|
||||
<text
|
||||
@ -211,4 +230,5 @@ class TextNode extends React.Component<{
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { getColor, getProgress, trunc } from "../common/utils"
|
||||
import Card, { CardOptions } from '../common/Card'
|
||||
import React from 'react'
|
||||
import FlexLayout from "../components/FlexLayout";
|
||||
import { TopLanguage } from "../interfaces";
|
||||
import { getColor, getPercent, getProgress, trunc } from '../common/utils'
|
||||
import Card, { CardOptions } from '../common/Card'
|
||||
import FlexLayout from '../components/FlexLayout'
|
||||
import { TopLanguage } from '../interfaces'
|
||||
|
||||
interface TopLanguagesOptions extends CardOptions {
|
||||
hide?: Array<string>
|
||||
@ -34,22 +34,23 @@ export default class TopLanguagesCard extends Card {
|
||||
}
|
||||
|
||||
const textColor = getColor('text_color', options.text_color, options.theme)
|
||||
this.title = "Most Used Languages"
|
||||
this.title = 'Most Used Languages'
|
||||
this.css = CompactTextNode.getCSS(textColor as string)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const total = this.langs.reduce((acc, curr) => acc + curr.xp, 0)
|
||||
if (this.options.layout === 'compact') {
|
||||
this.width = this.width + 50
|
||||
this.height = 90 + Math.round(this.langs.length / 2) * 25
|
||||
return super.render(
|
||||
<svg x="25">
|
||||
<mask id="rect-mask">
|
||||
<rect x="0" y="0" width={this.width - 50} height="8" fill="white" rx="5" />
|
||||
<rect x="0" y="0" width={this.width - 50} height="8" fill="white" rx="5" />
|
||||
</mask>
|
||||
<CompactProgressBar langs={this.langs} parentWidth={this.width} />
|
||||
<CompactProgressBar langs={this.langs} total={total} parentWidth={this.width} />
|
||||
{this.langs.map((el, index) => (
|
||||
<CompactTextNode key={index} index={index} lang={el} />
|
||||
<CompactTextNode key={index} index={index} total={total} lang={el} />
|
||||
))}
|
||||
</svg>
|
||||
)
|
||||
@ -58,29 +59,31 @@ export default class TopLanguagesCard extends Card {
|
||||
<svg x="25">
|
||||
<FlexLayout items={
|
||||
this.langs.map((el, index) => (
|
||||
<ProgressNode lang={el} parentWidth={this.width} />
|
||||
<ProgressNode key={index} lang={el} total={total} parentWidth={this.width} />
|
||||
))
|
||||
}
|
||||
gap={40}
|
||||
direction="column"
|
||||
gap={40}
|
||||
direction="column"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CompactProgressBar extends React.Component<{
|
||||
langs: Array<TopLanguage>
|
||||
parentWidth: number
|
||||
total: number
|
||||
}> {
|
||||
|
||||
public render() {
|
||||
|
||||
let offset = 0
|
||||
const totalSize = this.props.langs.reduce((acc, curr) => acc + curr.xp, 0)
|
||||
|
||||
return this.props.langs.map((lang, index) => {
|
||||
const percent = trunc((lang.xp / totalSize) * (this.props.parentWidth - 50), 2)
|
||||
const percent = trunc((lang.xp / this.props.total) * (this.props.parentWidth - 50), 2)
|
||||
const progress = percent < 10 ? percent + 10 : percent
|
||||
|
||||
const output = (
|
||||
@ -92,7 +95,7 @@ class CompactProgressBar extends React.Component<{
|
||||
y="0"
|
||||
width={progress}
|
||||
height="8"
|
||||
fill={lang.color || "#858585"}
|
||||
fill={lang.color || '#858585'}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -100,11 +103,13 @@ class CompactProgressBar extends React.Component<{
|
||||
return output
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CompactTextNode extends React.Component<{
|
||||
index: number
|
||||
lang: TopLanguage
|
||||
total: number
|
||||
}> {
|
||||
|
||||
public static getCSS = (textColor: string) => `
|
||||
@ -128,17 +133,19 @@ class CompactTextNode extends React.Component<{
|
||||
<g transform={`translate(${x}, ${y})`}>
|
||||
<circle cx="5" cy="6" r="5" fill={this.props.lang.color} />
|
||||
<text data-testid="lang-name" x="15" y="10" className='lang-name'>
|
||||
{this.props.lang.name} {getProgress(this.props.lang.xp)}%
|
||||
{this.props.lang.name} {getPercent(this.props.lang.xp, this.props.total)}%
|
||||
</text>
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class ProgressNode extends React.Component<{
|
||||
lang: TopLanguage
|
||||
parentWidth: number
|
||||
total: number
|
||||
}> {
|
||||
|
||||
private paddingRight = 60
|
||||
@ -146,12 +153,18 @@ class ProgressNode extends React.Component<{
|
||||
|
||||
public render() {
|
||||
const width = this.props.parentWidth - this.paddingRight
|
||||
const progress1 = getProgress(this.props.lang.xp)
|
||||
const progress2 = getProgress(this.props.lang.xp - this.props.lang.recentXp)
|
||||
const progress1 = getPercent(this.props.lang.xp, this.props.total)
|
||||
const progress2 = getPercent(this.props.lang.xp - this.props.lang.recentXp, this.props.total)
|
||||
|
||||
return (
|
||||
<>
|
||||
<text data-testid="lang-name" x="2" y="15" className="lang-name">{this.props.lang.name} {progress1}% {this.props.lang.recentXp >= 1 ? ' + ' + (trunc(progress1 - progress2, 2)) + '%' : ''}</text>
|
||||
<text
|
||||
x="2"
|
||||
y="15"
|
||||
className="lang-name"
|
||||
>
|
||||
{this.props.lang.name} {progress1}% {this.props.lang.recentXp >= 1 ? ` + ${trunc(progress1 - progress2, 2)}%` : ''}
|
||||
</text>
|
||||
<svg width={width}>
|
||||
<rect rx="5" ry="5" x="0" y="25" width={width} height="8" fill="#ddd" />
|
||||
{progress1 !== progress2 && (
|
||||
@ -176,4 +189,5 @@ class ProgressNode extends React.Component<{
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ export interface CardOptions {
|
||||
}
|
||||
|
||||
export default class Card {
|
||||
|
||||
public hideBorder = false
|
||||
public hideTitle = false
|
||||
public css = ''
|
||||
@ -34,7 +35,7 @@ export default class Card {
|
||||
this.hideTitle = parseBoolean(options.hide_title)
|
||||
this.colors = {
|
||||
titleColor: getColor('title_color', options.title_color, options.theme),
|
||||
bgColor: getColor('bg_color', options.bg_color, options.theme),
|
||||
bgColor: getColor('bg_color', options.bg_color, options.theme)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -75,24 +76,24 @@ export default class Card {
|
||||
}
|
||||
|
||||
renderGradient() {
|
||||
if (typeof this.colors.bgColor !== "object") return;
|
||||
if (typeof this.colors.bgColor !== 'object') return
|
||||
|
||||
const gradients = this.colors.bgColor.slice(1);
|
||||
return typeof this.colors.bgColor === "object"
|
||||
? (
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="gradient"
|
||||
gradientTransform={`rotate(${this.colors.bgColor[0]})`}
|
||||
>
|
||||
{gradients.map((grad, index) => {
|
||||
let offset = (index * 100) / (gradients.length - 1);
|
||||
return `<stop offset="${offset}%" stop-color="#${grad}" />`;
|
||||
})}
|
||||
</linearGradient>
|
||||
</defs>
|
||||
)
|
||||
: "";
|
||||
const gradients = this.colors.bgColor.slice(1)
|
||||
return typeof this.colors.bgColor === 'object' ?
|
||||
(
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="gradient"
|
||||
gradientTransform={`rotate(${this.colors.bgColor[0]})`}
|
||||
>
|
||||
{gradients.map((grad, index) => {
|
||||
const offset = (index * 100) / (gradients.length - 1)
|
||||
return `<stop offset="${offset}%" stop-color="#${grad}" />`
|
||||
})}
|
||||
</linearGradient>
|
||||
</defs>
|
||||
) :
|
||||
''
|
||||
}
|
||||
|
||||
render(body: JSX.Element) {
|
||||
@ -141,14 +142,14 @@ export default class Card {
|
||||
stroke="#E4E2E2"
|
||||
width={this.width - 1}
|
||||
fill={
|
||||
typeof this.colors.bgColor === "object"
|
||||
? "url(#gradient)"
|
||||
: this.colors.bgColor
|
||||
typeof this.colors.bgColor === 'object' ?
|
||||
'url(#gradient)' :
|
||||
this.colors.bgColor
|
||||
}
|
||||
strokeOpacity={this.hideBorder ? 0 : 1}
|
||||
/>
|
||||
|
||||
{this.hideTitle ? "" : this.renderTitle()}
|
||||
{this.hideTitle ? '' : this.renderTitle()}
|
||||
|
||||
<g
|
||||
transform={`translate(0, ${
|
||||
@ -160,4 +161,5 @@ export default class Card {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,6 +7,6 @@ const icons = {
|
||||
issues: <path fillRule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"/>,
|
||||
icon: <path fillRule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>,
|
||||
contribs: <path fillRule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>,
|
||||
fork: <path fillRule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>,
|
||||
};
|
||||
fork: <path fillRule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
|
||||
}
|
||||
export default icons
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { CodeStatsResponse } from '../interfaces'
|
||||
import { CustomError } from './utils'
|
||||
import { CodeStatsResponse } from '../interfaces';
|
||||
|
||||
export default async function retryer<T = Promise<CodeStatsResponse>>(
|
||||
fetcher: (username: string) => T,
|
||||
@ -8,7 +8,7 @@ export default async function retryer<T = Promise<CodeStatsResponse>>(
|
||||
err?: any
|
||||
): Promise<T> {
|
||||
if (retries > 7) {
|
||||
throw new CustomError("Maximum retries exceeded" + err, 'MAX_RETRY')
|
||||
throw new CustomError('Maximum retries exceeded' + err, 'MAX_RETRY')
|
||||
}
|
||||
try {
|
||||
return await fetcher(
|
||||
|
@ -2,7 +2,7 @@ import fetch from 'node-fetch'
|
||||
import { Response } from 'express'
|
||||
import wrap from 'word-wrap'
|
||||
import themes from '../../themes/themes.json'
|
||||
import { CodeStatsResponse } from '../interfaces';
|
||||
import { CodeStatsResponse } from '../interfaces'
|
||||
import languageColor from '../../themes/language-bar.json'
|
||||
|
||||
/**
|
||||
@ -14,42 +14,38 @@ import languageColor from '../../themes/language-bar.json'
|
||||
export function encodeHTML(str: string) {
|
||||
return str
|
||||
.replace(/[\u00A0-\u9999<>&](?!#)/gim, (i) => {
|
||||
return "&#" + i.charCodeAt(0) + ";";
|
||||
return '&#' + i.charCodeAt(0) + ';'
|
||||
})
|
||||
.replace(/\u0008/gim, "");
|
||||
}
|
||||
|
||||
export const kFormatter = (num: number) =>
|
||||
Math.abs(num) > 999 ?
|
||||
export function kFormatter(num: number) {
|
||||
return Math.abs(num) > 999 ?
|
||||
trunc(num / 1000) + 'k' :
|
||||
num
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the `value` into a Boolean
|
||||
* Transform the `value` query string into a Boolean
|
||||
* @param value the value to transform
|
||||
*/
|
||||
export function parseBoolean(value: boolean | string | undefined) {
|
||||
if (value === "true" || value === true) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return value === 'true' || value === '' || value === true
|
||||
}
|
||||
|
||||
export function parseArray(str: string | undefined) {
|
||||
if (!str) return [];
|
||||
return str.split(",");
|
||||
if (!str) return []
|
||||
return str.split(',')
|
||||
}
|
||||
|
||||
export function clampValue(number: number, min: number, max?: number) {
|
||||
return Math.max(min, max ? Math.min(number, max) : number);
|
||||
return Math.max(min, max ? Math.min(number, max) : number)
|
||||
}
|
||||
|
||||
export async function request(username: string): Promise<CodeStatsResponse> {
|
||||
const resp = await fetch(
|
||||
`https://codestats.net/api/users/${username}`
|
||||
);
|
||||
return resp.json();
|
||||
)
|
||||
return resp.json()
|
||||
}
|
||||
|
||||
export async function profileGraphRequest<T>(body: string): Promise<T> {
|
||||
@ -63,25 +59,28 @@ export async function profileGraphRequest<T>(body: string): Promise<T> {
|
||||
return resp.json()
|
||||
}
|
||||
|
||||
export function getColor(color: keyof typeof themes.default, replacementColor?: string, theme: keyof typeof themes = 'default') {
|
||||
export function getColor(
|
||||
color: keyof typeof themes.default,
|
||||
replacementColor?: string,
|
||||
theme: keyof typeof themes = 'default'
|
||||
) {
|
||||
return '#' + (replacementColor ? replacementColor : themes[theme][color])
|
||||
}
|
||||
|
||||
export function wrapTextMultiline(text: string, width = 60, maxLines = 3) {
|
||||
const wrapped = wrap(encodeHTML(text), { width })
|
||||
.split("\n") // Split wrapped lines to get an array of lines
|
||||
.map((line) => line.trim()); // Remove leading and trailing whitespace of each line
|
||||
.split('\n') // Split wrapped lines to get an array of lines
|
||||
.map((line) => line.trim()) // Remove leading and trailing whitespace of each line
|
||||
|
||||
const lines = wrapped.slice(0, maxLines); // Only consider maxLines lines
|
||||
const lines = wrapped.slice(0, maxLines) // Only consider maxLines lines
|
||||
|
||||
// Add "..." to the last line if the text exceeds maxLines
|
||||
if (wrapped.length > maxLines) {
|
||||
lines[maxLines - 1] += "...";
|
||||
lines[maxLines - 1] += '...'
|
||||
}
|
||||
|
||||
// Remove empty lines if text fits in less than maxLines lines
|
||||
const multiLineText = lines.filter(Boolean);
|
||||
return multiLineText;
|
||||
return lines.filter(Boolean)
|
||||
}
|
||||
|
||||
export const CONSTANTS = {
|
||||
@ -89,23 +88,25 @@ export const CONSTANTS = {
|
||||
TWO_HOURS: 7200,
|
||||
FOUR_HOURS: 14400,
|
||||
ONE_DAY: 86400,
|
||||
LEVEL_FACTOR: 0.025,
|
||||
};
|
||||
LEVEL_FACTOR: 0.025
|
||||
}
|
||||
|
||||
export const SECONDARY_ERROR_MESSAGES = {
|
||||
MAX_RETRY: "Make sur your profile is not private"
|
||||
};
|
||||
MAX_RETRY: 'Make sur your profile is not private'
|
||||
}
|
||||
|
||||
export class CustomError extends Error {
|
||||
|
||||
public secondaryMessage: string
|
||||
|
||||
constructor(message: string, public type: keyof typeof SECONDARY_ERROR_MESSAGES) {
|
||||
super(message);
|
||||
this.secondaryMessage = SECONDARY_ERROR_MESSAGES[type] || "adsad";
|
||||
super(message)
|
||||
this.secondaryMessage = SECONDARY_ERROR_MESSAGES[type] || 'adsad'
|
||||
}
|
||||
|
||||
static MAX_RETRY = "MAX_RETRY";
|
||||
static USER_NOT_FOUND = "USER_NOT_FOUND";
|
||||
static MAX_RETRY = 'MAX_RETRY'
|
||||
static USER_NOT_FOUND = 'USER_NOT_FOUND'
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -127,15 +128,19 @@ export function getProgress(xp: number): number {
|
||||
return trunc((CONSTANTS.LEVEL_FACTOR * Math.sqrt(xp) - currentLvl) * 100, 2)
|
||||
}
|
||||
|
||||
export function getPercent(number: number, total: number) {
|
||||
return trunc(number * 100 / total, 2)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Truncate a number without moving it to a string and reparsing it
|
||||
* Round a number without moving it to a string and reparsing it
|
||||
*
|
||||
* https://stackoverflow.com/a/29494612/7335674
|
||||
* @param number the number to truncate
|
||||
* @param digits the number of digits after the dot
|
||||
*/
|
||||
export function trunc(number: number, digits: number = 0) {
|
||||
export function trunc(number: number, digits = 0) {
|
||||
const pow = Math.pow(10, digits)
|
||||
return Math.round(number * pow) / pow
|
||||
}
|
||||
@ -152,14 +157,12 @@ export function parseNumber(number: string | number | undefined): number | undef
|
||||
}
|
||||
|
||||
|
||||
export function calculateCircleProgress(percent: number) {
|
||||
let radius = 40;
|
||||
let c = Math.PI * radius * 2
|
||||
export function calculateCircleProgress(percent: number, radius = 40) {
|
||||
const c = Math.PI * radius * 2
|
||||
|
||||
percent = clampValue(percent, 0, 100)
|
||||
|
||||
let percentage = ((100 - percent) / 100) * c
|
||||
return percentage;
|
||||
return ((100 - percent) / 100) * c
|
||||
}
|
||||
|
||||
/**
|
||||
@ -167,7 +170,7 @@ export function calculateCircleProgress(percent: number) {
|
||||
* @param res the response object
|
||||
*/
|
||||
export function prepareResponse(res: Response) {
|
||||
res.setHeader("Content-Type", "image/svg+xml")
|
||||
res.setHeader('Content-Type', 'image/svg+xml')
|
||||
}
|
||||
|
||||
/**
|
||||
@ -175,7 +178,7 @@ export function prepareResponse(res: Response) {
|
||||
* @param res the Response object
|
||||
* @param cache The cache time in seconds
|
||||
*/
|
||||
export function setCache(res: Response, cache: number) {
|
||||
export function setCache(res: Response, cache = CONSTANTS.THIRTY_MINUTES) {
|
||||
if (isNaN(cache)) {
|
||||
cache = CONSTANTS.THIRTY_MINUTES
|
||||
}
|
||||
@ -204,5 +207,5 @@ export function formatDate(date: Date): string {
|
||||
|
||||
|
||||
export function getColorOfLanguage(name: string): string {
|
||||
return name in languageColor ? languageColor[name as keyof typeof languageColor].color || '#000' : '#000'
|
||||
return name in languageColor ? languageColor[name as keyof typeof languageColor].color || '#3e4053' : '#3e4053'
|
||||
}
|
||||
|
@ -16,9 +16,10 @@ export default class Error extends React.Component<{
|
||||
<rect x="0.5" y="0.5" width="494" height="99%" rx="4.5" fill="#FFFEFE" stroke="#E4E2E2"/>
|
||||
<text x="25" y="45" className="text">Something went wrong! file an issue at https://dze.io/3nL29</text>
|
||||
<text data-testid="message" x="25" y="55" className="text small">
|
||||
<tspan x="25" dy="18">{encodeHTML(this.props.message)}</tspan>
|
||||
<tspan x="25" dy="18" className="gray">{this.props.secondaryMessage}</tspan>
|
||||
<tspan x="25" dy="18">{encodeHTML(this.props.message)}</tspan>
|
||||
<tspan x="25" dy="18" className="gray">{this.props.secondaryMessage}</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ export default class FlexLayout extends React.Component<{
|
||||
gap: number
|
||||
direction?: 'column'
|
||||
}> {
|
||||
|
||||
public render() {
|
||||
return this.props.items.filter(Boolean).map((item, index) => (
|
||||
<g key={index} transform={this.getGap(index)}>{item}</g>
|
||||
@ -19,4 +20,5 @@ export default class FlexLayout extends React.Component<{
|
||||
}
|
||||
return transform
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { request, CONSTANTS, getLevel, profileGraphRequest, CustomError, formatDateNumber, formatDate, getColorOfLanguage } from '../common/utils'
|
||||
import { request, getLevel, profileGraphRequest, CustomError, formatDate, getColorOfLanguage } from '../common/utils'
|
||||
import retryer from '../common/retryer'
|
||||
import { CodeStatsHistoryGraph, TopLanguages } from '../interfaces'
|
||||
|
||||
@ -28,6 +28,7 @@ export async function fetchHistory(username: string, days: number) {
|
||||
}`
|
||||
|
||||
const response = await retryer<Promise<CodeStatsHistoryGraph>>(profileGraphRequest, body)
|
||||
|
||||
if (response.errors) {
|
||||
throw new CustomError(response.errors[0].message, 'MAX_RETRY')
|
||||
}
|
||||
@ -48,23 +49,19 @@ export async function fetchHistory(username: string, days: number) {
|
||||
xp: data.xp,
|
||||
language: data.language
|
||||
})
|
||||
if (data.language in languagesData) {
|
||||
languagesData[data.language] += data.xp
|
||||
} else {
|
||||
languagesData[data.language] = data.xp
|
||||
if (!(data.language in languagesData)) {
|
||||
languagesData[data.language] = 0
|
||||
}
|
||||
|
||||
languagesData[data.language] += data.xp
|
||||
result[data.date] = day
|
||||
}
|
||||
|
||||
for (const key of Object.keys(result)) {
|
||||
const item = result[key]
|
||||
result[key] = item.sort((a, b) => languagesData[b.language] - languagesData[a.language])
|
||||
}
|
||||
|
||||
const keys = Object.keys(result)
|
||||
|
||||
for (const day of keys) {
|
||||
const item = result[day]
|
||||
result[day] = item.sort((a, b) => languagesData[b.language] - languagesData[a.language])
|
||||
if (keys.indexOf(day) === 0 && day === formatDate(date)) {
|
||||
continue
|
||||
}
|
||||
@ -77,21 +74,19 @@ export async function fetchHistory(username: string, days: number) {
|
||||
|
||||
}
|
||||
|
||||
return Object.keys(result).map((el) => {
|
||||
return {
|
||||
data: result[el],
|
||||
day: el,
|
||||
total: result[el].reduce((prvs, crnt) => prvs + crnt.xp, 0)
|
||||
}
|
||||
}).sort((a, b) => a.day < b.day ? 1 : -1)
|
||||
return Object.keys(result).map((el) => ({
|
||||
data: result[el],
|
||||
day: el,
|
||||
total: result[el].reduce((prvs, crnt) => prvs + crnt.xp, 0)
|
||||
})).sort((a, b) => a.day < b.day ? 1 : -1)
|
||||
|
||||
|
||||
}
|
||||
|
||||
export async function fetchTopLanguages(username: string): Promise<TopLanguages> {
|
||||
if (!username) throw Error("Invalid username")
|
||||
if (!username) throw Error('Invalid username')
|
||||
|
||||
let res = await retryer(request, username)
|
||||
const res = await retryer(request, username)
|
||||
|
||||
const langs = res.languages
|
||||
|
||||
|
Reference in New Issue
Block a user