1
1
Fork 0

Redesign components in frontend (DRY)

This commit is contained in:
Jordy van Zeeland 2022-10-27 11:17:03 +02:00
parent 91c530bcd5
commit cfb3290194
9 changed files with 454 additions and 147 deletions

View File

@ -2,6 +2,8 @@ from django.urls import path
from .views import * from .views import *
urlpatterns = [ urlpatterns = [
path('books', getAllBooks),
path('books/challenge', getChallengeOfYear),
path('books/years', getYears), path('books/years', getYears),
path('books/stats', getStats), path('books/stats', getStats),

View File

@ -17,6 +17,15 @@ def getBooksData():
return df return df
def getBookChallenge(year = None):
engine = create_engine('mysql+mysqldb://' + ras.settings.DATABASES['default']['USER'] + ':' + ras.settings.DATABASES['default']['PASSWORD'] + '@' + ras.settings.DATABASES['default']['HOST'] + ':3306/' + ras.settings.DATABASES['default']['NAME'])
if(year):
df = pd.read_sql('SELECT * FROM book_challenge where year = ' + year, engine)
else:
df = pd.read_sql('SELECT * FROM book_challenge', engine)
return df
def filterData(df, datayear = None): def filterData(df, datayear = None):
df['readed'] = pd.to_datetime(df['readed'], format='%Y-%m-%d') df['readed'] = pd.to_datetime(df['readed'], format='%Y-%m-%d')
df['readed'] = df['readed'].dt.strftime('%m-%Y') df['readed'] = df['readed'].dt.strftime('%m-%Y')
@ -27,6 +36,43 @@ def filterData(df, datayear = None):
return df return df
@api_view(['GET'])
def getAllBooks(request):
data = []
books = getBooksData()
for index, row in books.iterrows():
data.append({
"id": row['id'],
"name": row['name'],
"author": row['author'],
"genre": row['genre'],
"author": row['author'],
"country": row['country'],
"country_code": row['country_code'],
"pages": row['pages'],
"readed": row['readed'],
"rating": row['rating'],
})
return Response(data)
@api_view(['GET'])
def getChallengeOfYear(request):
if request.META.get('HTTP_YEAR'):
data = []
df = getBookChallenge(request.META.get('HTTP_YEAR'))
for index, row in df.iterrows():
data.append({
"year": row['year'],
"nrofbooks": row['nrofbooks']
})
return Response(data)
else:
return Response("No year header included")
@api_view(['GET']) @api_view(['GET'])
def books_per_genre_per_month(request): def books_per_genre_per_month(request):

View File

@ -1,4 +1,6 @@
import React, { Component } from "react"; import React, { Component } from "react";
import Challenge from "./components/Challenge";
import BookStats from "./components/Stats";
export default class App extends Component { export default class App extends Component {
constructor(props) { constructor(props) {
@ -6,18 +8,14 @@ export default class App extends Component {
this.state = { this.state = {
year: new Date().getFullYear(), year: new Date().getFullYear(),
readingYears: [], readingYears: [],
totalbooks: 0, countries: [],
totalpages: 0, pagesStats: [],
totalauthors: 0,
totalcountries: 0,
totalgenres: 0,
countries: []
} }
this.yearsArray = []; this.yearsArray = [];
} }
getGenres(){ getGenres() {
fetch('/api/books/genres', { fetch('/api/books/genres', {
"method": "GET", "method": "GET",
"headers": { "headers": {
@ -27,10 +25,10 @@ export default class App extends Component {
.then(response => response.json()) .then(response => response.json())
.then(books => { .then(books => {
this.initChart(books, this.state.year); this.initChart(books, this.state.year);
}) })
} }
getCountries(init){ getCountries(init) {
fetch('/api/books/countries', { fetch('/api/books/countries', {
"method": "GET", "method": "GET",
"headers": { "headers": {
@ -43,7 +41,7 @@ export default class App extends Component {
countries: data countries: data
}) })
if(init == true){ if (init == true) {
$('#DataTable').DataTable({ $('#DataTable').DataTable({
paging: false, paging: false,
ordering: false, ordering: false,
@ -54,33 +52,31 @@ export default class App extends Component {
}) })
} }
getShortestLongestBook(currentyear) {
fetch('/api/books/pages/stats', {
"method": "GET",
"headers": {
"year": currentyear
}
})
.then(response => response.json())
.then(bookstats => {
this.setState({
pagesStats: bookstats
})
})
}
changeYear(event) { changeYear(event) {
this.setState({ this.setState({
year: event.target.value year: event.target.value
}) })
fetch('/api/books/stats', {
"method": "GET",
"headers": {
"year": event.target.value
}
})
.then(response => response.json())
.then(data => {
this.setState({
totalbooks: data.totalbooks,
totalpages: data.totalpages,
totalauthors: data.totalauthors,
totalcountries: data.totalcountries,
totalgenres: data.totalgenres
})
})
fetch('/api/books/countries', { fetch('/api/books/countries', {
"method": "GET", "method": "GET",
"headers": { "headers": {
"year": this.state.year "year": event.target.value
} }
}) })
.then(response => response.json()) .then(response => response.json())
@ -94,19 +90,32 @@ export default class App extends Component {
var $this = this; var $this = this;
this.getShortestLongestBook(event.target.value);
fetch('/api/books/genres/count', { fetch('/api/books/genres/count', {
"method": "GET", "method": "GET",
"headers": { "headers": {
"year": this.state.year "year": event.target.value
} }
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
this.initDoughnut(data); this.initDoughnut(data);
}) })
fetch('/api/books/genres', {
"method": "GET",
"headers": {
"year": event.target.value
}
})
.then(response => response.json())
.then(books => {
this.initChart(books, event.target.value);
})
} }
initHorBar(data){ initHorBar(data) {
var countries = []; var countries = [];
var counts = []; var counts = [];
@ -177,10 +186,10 @@ export default class App extends Component {
const legendMargin = { const legendMargin = {
id: 'legendMargin', id: 'legendMargin',
beforeInit(chart, legend, options){ beforeInit(chart, legend, options) {
const fitValue = chart.legend.fit; const fitValue = chart.legend.fit;
chart.legend.fit = function fit(){ chart.legend.fit = function fit() {
fitValue.bind(chart.legend)(); fitValue.bind(chart.legend)();
return this.height += 30; return this.height += 30;
} }
@ -205,19 +214,19 @@ export default class App extends Component {
borderColor: '#1f2940', borderColor: '#1f2940',
tooltip: { tooltip: {
callbacks: { callbacks: {
label: function(context) { label: function (context) {
let label = context.label; let label = context.label;
let value = context.formattedValue; let value = context.formattedValue;
if (!label) if (!label)
label = 'Unknown' label = 'Unknown'
let sum = 0; let sum = 0;
let dataArr = context.chart.data.datasets[0].data; let dataArr = context.chart.data.datasets[0].data;
dataArr.map(data => { dataArr.map(data => {
sum += Number(data); sum += Number(data);
}); });
let percentage = (value * 100 / sum).toFixed(1) + '%'; let percentage = (value * 100 / sum).toFixed(1) + '%';
return label + ": " + percentage; return label + ": " + percentage;
} }
@ -246,24 +255,24 @@ export default class App extends Component {
}, },
plugins: [{ plugins: [{
id: 'legendMargin', id: 'legendMargin',
beforeInit(chart, legend, options){ beforeInit(chart, legend, options) {
const fitValue = chart.legend.fit; const fitValue = chart.legend.fit;
chart.legend.fit = function fit(){ chart.legend.fit = function fit() {
fitValue.bind(chart.legend)(); fitValue.bind(chart.legend)();
return this.height += 30; return this.height += 30;
} }
} }
},{ }, {
afterDraw: chart => { afterDraw: chart => {
var ctx = chart.ctx; var ctx = chart.ctx;
ctx.save(); ctx.save();
var image = new Image(); var image = new Image();
image.src = 'https://www.iconsdb.com/icons/preview/gray/book-xxl.png'; image.src = 'https://www.iconsdb.com/icons/preview/gray/book-xxl.png';
var imageSize = 80; var imageSize = 80;
ctx.drawImage(image, chart.width / 2 - imageSize / 2, chart.height / 2 - imageSize / 6, imageSize, imageSize); ctx.drawImage(image, chart.width / 2 - imageSize / 2, chart.height / 2 - imageSize / 6, imageSize, imageSize);
ctx.restore(); ctx.restore();
} }
}], }],
}); });
} }
@ -335,10 +344,10 @@ export default class App extends Component {
const legendMargin = { const legendMargin = {
id: 'legendMargin', id: 'legendMargin',
beforeInit(chart, legend, options){ beforeInit(chart, legend, options) {
const fitValue = chart.legend.fit; const fitValue = chart.legend.fit;
chart.legend.fit = function fit(){ chart.legend.fit = function fit() {
fitValue.bind(chart.legend)(); fitValue.bind(chart.legend)();
return this.height += 30; return this.height += 30;
} }
@ -358,7 +367,7 @@ export default class App extends Component {
legend: { legend: {
display: true, display: true,
labels: { labels: {
usePointStyle: true, usePointStyle: true,
} }
}, },
interaction: { interaction: {
@ -405,15 +414,11 @@ export default class App extends Component {
}); });
} }
componentDidUpdate() {
this.getGenres();
}
componentDidMount() { componentDidMount() {
var $this = this; var $this = this;
var currentyear = this.state.year ? this.state.year : new Date().getFullYear() var currentyear = this.state.year ? this.state.year : new Date().getFullYear();
fetch('/api/books/genres', { fetch('/api/books/genres', {
"method": "GET", "method": "GET",
@ -426,6 +431,8 @@ export default class App extends Component {
this.initChart(books, currentyear); this.initChart(books, currentyear);
}) })
this.getShortestLongestBook(this.state.year);
fetch('/api/books/genres/count', { fetch('/api/books/genres/count', {
"method": "GET", "method": "GET",
"headers": { "headers": {
@ -439,48 +446,56 @@ export default class App extends Component {
this.getCountries(true); this.getCountries(true);
fetch('/api/books/stats', { fetch('/api/books/years', {
"method": "GET", "method": "GET",
"headers": {
"year": this.state.year
}
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
$this.setState({ this.setState({
totalbooks: data.totalbooks, readingYears: data
totalpages: data.totalpages,
totalauthors: data.totalauthors,
totalcountries: data.totalcountries,
totalgenres: data.totalgenres
}) })
}) })
fetch('/api/books/years', {
"method": "GET",
})
.then(response => response.json())
.then(data => {
this.setState({
readingYears: data
})
})
} }
render() { render() {
var url = window.location.href.split("/"); var url = window.location.href.split("/");
var ratingshort = '';
var ratinglong = '';
if (this.state.pagesStats.shortestbook) {
for (var i = 0; i < this.state.pagesStats.shortestbook.rating; i++) {
ratingshort += "<i class='fas fa-star'></i>";
}
}
if (document.getElementById("shortest_rating") !== null) {
document.getElementById('shortest_rating').innerHTML = ratingshort;
}
if (this.state.pagesStats.longestbook) {
for (var i = 0; i < this.state.pagesStats.longestbook.rating; i++) {
ratinglong += "<i class='fas fa-star'></i>";
}
}
if (document.getElementById("longest_rating") !== null) {
document.getElementById('longest_rating').innerHTML = ratinglong;
}
return ( return (
<React.Fragment> <React.Fragment>
<div className="sidebar"> <div className="sidebar">
<div className={`menu-item ${ url && url[3] == "" ? 'selected' : ''}`}> <div className={`menu-item ${url && url[3] == "" ? 'selected' : ''}`}>
<i class="fa fa-chart-bar"></i> <i className="fa fa-chart-bar"></i>
</div> </div>
<div className={`menu-item ${ url && url[3] == "books" ? 'selected' : ''}`}> <div className={`menu-item ${url && url[3] == "books" ? 'selected' : ''}`}>
<i class="fa fa-book"></i> <i className="fa fa-book"></i>
</div> </div>
</div> </div>
<div className="content"> <div className="content">
@ -492,105 +507,94 @@ export default class App extends Component {
<div className="row"> <div className="row">
<div className="col-md-2"> <div className="col-md-2">
<div className="stat-block"> <div className="stat-block">
<i class="fa fa-calendar"></i> <i className="fa fa-calendar"></i>
<span className="stats-number"> <span className="stats-number">
<select className="yearselector" defaultValue={this.state.year} onChange={(event) => this.changeYear(event)}> <select className="yearselector" defaultValue={this.state.year} onChange={(event) => this.changeYear(event)}>
{this.state.readingYears.map((year) => { {this.state.readingYears.map((year, i) => {
if(year === this.state.year){ if (year === this.state.year) {
var selected = 'selected' var selected = 'selected'
}else{ } else {
selected = '' selected = ''
} }
return(<option selected={selected} value={year}>{year}</option>) return (<option key={i} selected={selected} value={year}>{year}</option>)
})} })}
</select> </select>
</span> </span>
</div> </div>
</div> </div>
<div className="col-md-2"> <BookStats year={this.state.year} />
<div className="stat-block">
<i class="fa fa-book"></i>
<span className="stats-number">{this.state.totalbooks}</span>
<span className="stats-label">Boeken</span>
</div>
</div>
<div className="col-md-2">
<div className="stat-block">
<i class="fa fa-book-open"></i>
<span className="stats-number">{this.state.totalpages}</span>
<span className="stats-label">Bladzijdes</span>
</div>
</div>
<div className="col-md-2">
<div className="stat-block">
<i class="fa fa-pen"></i>
<span className="stats-number">{this.state.totalauthors}</span>
<span className="stats-label">Schrijvers</span>
</div>
</div>
<div className="col-md-2">
<div className="stat-block">
<i class="fa fa-book"></i>
<span className="stats-number">{this.state.totalgenres}</span>
<span className="stats-label">Genres</span>
</div>
</div>
<div className="col-md-2">
<div className="stat-block">
<i class="fa fa-globe"></i>
<span className="stats-number">{this.state.totalcountries}</span>
<span className="stats-label">Landen</span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<Challenge year={this.state.year} />
<div className="container-fluid"> <div className="container-fluid">
<div className="row"> <div className="row">
<div className="col-md-9"> <div className="col-md-9">
<div className="books-per-month"><span className="block_name">Boeken per maand per genre</span><canvas id="chart"></canvas></div> <div className="books-per-month"><span className="block_name">Boeken per maand per genre</span><canvas id="chart"></canvas></div>
<div className="row">
<div className="col-md-6">
<div className="book shortest">
<span className="block_name">Kortste boek</span>
<i className="fa fa-book book-icon"></i>
<div className="book_pages">{this.state.pagesStats.shortestbook ? this.state.pagesStats.shortestbook.pages : ''} pagina's</div>
<div className="book_title_author">{this.state.pagesStats.shortestbook ? this.state.pagesStats.shortestbook.name : ''} - {this.state.pagesStats.shortestbook ? this.state.pagesStats.shortestbook.author : ''}</div>
<div id="shortest_rating" className="book_rating"></div>
</div>
</div>
<div className="col-md-6">
<div className="book longest">
<span className="block_name">Langste boek</span>
<i className="fa fa-book book-icon"></i>
<div className="book_pages">{this.state.pagesStats.longestbook ? this.state.pagesStats.longestbook.pages : ''} pagina's</div>
<div className="book_title_author">{this.state.pagesStats.longestbook ? this.state.pagesStats.longestbook.name : ''} - {this.state.pagesStats.longestbook ? this.state.pagesStats.longestbook.author : ''}</div>
<div id="longest_rating" className="book_rating"></div>
</div>
</div>
</div>
</div> </div>
<div className="col-md-3"> <div className="col-md-3">
<div className="books-per-country"> <div className="books-per-country">
<span className="block_name">Landen</span> <span className="block_name">Landen</span>
<table id="DataTable" class="showHead table responsive nowrap" width="100%"> <table id="DataTable" className="showHead table responsive nowrap" width="100%">
<thead> <thead>
<tr> <tr>
<th>#</th> <th>#</th>
<th>Land</th> <th>Land</th>
<th>Boeken</th> <th>Boeken</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{this.state.countries.map((country, i) => { {this.state.countries.map((country, i) => {
var code = country.code.toLowerCase(); var code = country.code.toLowerCase();
return( return (
<React.Fragment> <React.Fragment>
<tr> <tr key="{i}">
<td>{i+1}</td> <td>{i + 1}</td>
<td><img src={`https://flagcdn.com/32x24/${code}.png`} /> {country.country}</td> <td><img src={`https://flagcdn.com/32x24/${code}.png`} /> {country.country}</td>
<td>{country.count}</td> <td>{country.count}</td>
</tr> </tr>
</React.Fragment> </React.Fragment>
) )
})} })}
</tbody> </tbody>
</table> </table>
</div> </div>
<div className="genresPercent"><span className="block_name">Genres</span><canvas id="chartGenres"></canvas></div> <div className="genresPercent"><span className="block_name">Genres</span><canvas id="chartGenres"></canvas></div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</React.Fragment> </React.Fragment>
) )

View File

@ -0,0 +1,65 @@
import React, { Component } from 'react';
import { getChallenge, getStats } from "./Data.js";
export default class Challenge extends Component {
constructor(props) {
super(props);
this.state = {
readingYears: [],
challenge: 0
}
}
getComponentData(){
var $this = this;
getStats(this.props.year).then(data => {
$this.setState({
totalbooks: data.totalbooks
})
});
getChallenge(this.props.year).then(data => {
this.setState({
challenge: data && data.length > 0 ? data[0].nrofbooks : 0
})
});
}
componentDidMount() {
this.getComponentData();
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.year !== this.props.year) {
this.getComponentData();
}
}
render() {
var challengePercentage = (this.state.totalbooks / this.state.challenge) * 100
return (
<React.Fragment>
{this.state.challenge && this.state.challenge !== 0 ?
<div className="container-fluid">
<div className="row">
<div className="col-md-12">
<div className="stat-block">
<span className="block_name">Book Challenge</span>
<div className="progress">
<div className="progress-bar progress-bar-striped" role="progressbar" style={{ width: challengePercentage + '%' }} aria-valuenow={challengePercentage} aria-valuemin="0" aria-valuemax="100">
<div className="progress-bar-number">{challengePercentage}%</div>
</div>
</div>
<span className="stats-number">{this.state.totalbooks}</span><span className="stats-label">van de</span><span className="stats-number">{this.state.challenge}</span><span className="stats-label">boeken gelezen</span>
</div>
</div>
</div>
</div>
: ''}
</React.Fragment>
)
}
}

View File

@ -0,0 +1,35 @@
export const getStats = (year) => {
return fetch('/api/books/stats', {
"method": "GET",
"headers": {
"year": year
}
})
.then(response => response.json())
.then(data => {
return data;
})
}
export const getChallenge = (year) => {
return fetch('/api/books/challenge', {
"method": "GET",
"headers": {
"year": year
}
})
.then(response => response.json())
.then(data => {
return data
})
}
export const getReadingYears = () => {
return fetch('/api/books/years', {
"method": "GET",
})
.then(response => response.json())
.then(data => {
return data
})
}

View File

@ -0,0 +1,88 @@
import React, { Component } from 'react';
import { getStats, getReadingYears } from "./Data.js";
export default class BookStats extends Component {
constructor(props) {
super(props);
this.state = {
readingYears: [],
totalbooks: 0,
totalpages: 0,
totalauthors: 0,
totalcountries: 0,
totalgenres: 0,
}
}
getComponentData(){
var $this = this;
getStats(this.props.year).then(data => {
$this.setState({
totalbooks: data.totalbooks,
totalpages: data.totalpages,
totalauthors: data.totalauthors,
totalcountries: data.totalcountries,
totalgenres: data.totalgenres
})
});
getReadingYears().then(data => {
this.setState({
readingYears: data
})
});
}
componentDidMount() {
this.getComponentData();
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.year !== this.props.year) {
this.getComponentData();
}
}
render() {
return (
<React.Fragment>
<div className="col-md-2">
<div className="stat-block">
<i className="fa fa-book"></i>
<span className="stats-number">{this.state.totalbooks}</span>
<span className="stats-label">Boeken</span>
</div>
</div>
<div className="col-md-2">
<div className="stat-block">
<i className="fa fa-book-open"></i>
<span className="stats-number">{this.state.totalpages}</span>
<span className="stats-label">Bladzijdes</span>
</div>
</div>
<div className="col-md-2">
<div className="stat-block">
<i className="fa fa-pen"></i>
<span className="stats-number">{this.state.totalauthors}</span>
<span className="stats-label">Schrijvers</span>
</div>
</div>
<div className="col-md-2">
<div className="stat-block">
<i className="fa fa-book"></i>
<span className="stats-number">{this.state.totalgenres}</span>
<span className="stats-label">Genres</span>
</div>
</div>
<div className="col-md-2">
<div className="stat-block">
<i className="fa fa-globe"></i>
<span className="stats-number">{this.state.totalcountries}</span>
<span className="stats-label">Landen</span>
</div>
</div>
</React.Fragment>
)
}
}

File diff suppressed because one or more lines are too long

View File

@ -6,10 +6,22 @@
!*** ./src/index.js ***! !*** ./src/index.js ***!
\**********************/ \**********************/
/*!********************************!*\
!*** ./src/components/Data.js ***!
\********************************/
/*!*********************************!*\
!*** ./src/components/Stats.js ***!
\*********************************/
/*!*************************************!*\ /*!*************************************!*\
!*** ./node_modules/react/index.js ***! !*** ./node_modules/react/index.js ***!
\*************************************/ \*************************************/
/*!*************************************!*\
!*** ./src/components/Challenge.js ***!
\*************************************/
/*!*****************************************!*\ /*!*****************************************!*\
!*** ./node_modules/react-dom/index.js ***! !*** ./node_modules/react-dom/index.js ***!
\*****************************************/ \*****************************************/

View File

@ -73,7 +73,7 @@
height:600px !important; height:600px !important;
} }
.books-per-month, .genresPercent, .books-per-country{ .books-per-month, .genresPercent, .books-per-country, .book{
background: #ffffff; background: #ffffff;
padding: 20px; padding: 20px;
box-shadow: 0 2px 0px 1px rgb(0 0 0 / 3%); box-shadow: 0 2px 0px 1px rgb(0 0 0 / 3%);
@ -81,6 +81,33 @@
border-radius: 10px; border-radius: 10px;
} }
.book .book-icon{
font-size: 60px;
float: left;
margin-right: 20px;
margin-bottom: 40px;
color: #808080;
}
.book_rating{
margin-top: 5px;
}
.book_rating i{
font-family: "Font Awesome 5 Free";
color: #ffbe0e;
}
.book .book_pages{
font-size: 18px;
font-weight: 600;
}
.book .book_title_author{
font-size: 16px;
color: #808080;
}
.sidebar{ .sidebar{
background: #363a53; background: #363a53;
width: 70px; width: 70px;
@ -116,7 +143,7 @@
padding: 50px; padding: 50px;
} }
.books-stats .stat-block{ .books-stats .stat-block, .stat-block{
background: #ffffff; background: #ffffff;
box-shadow: 0 2px 0px 1px rgb(0 0 0 / 3%); box-shadow: 0 2px 0px 1px rgb(0 0 0 / 3%);
padding: 15px 5px; padding: 15px 5px;
@ -163,7 +190,7 @@
/* box-shadow: 2px 2px 0px 0px rgba(0, 0, 0, 0.3); */ /* box-shadow: 2px 2px 0px 0px rgba(0, 0, 0, 0.3); */
} }
.books-stats .stat-block .stats-number{ .books-stats .stat-block .stats-number, .stats-number{
font-weight: 600; font-weight: 600;
display: inline-block; display: inline-block;
margin-left: 10px; margin-left: 10px;
@ -171,7 +198,7 @@
margin-right: 10px; margin-right: 10px;
} }
.books-stats .stat-block .stats-label{ .books-stats .stat-block .stats-label, .stats-label{
color: #a7adbd; color: #a7adbd;
font-weight: 400; font-weight: 400;
font-size: 20px; font-size: 20px;
@ -218,6 +245,34 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
.progress{
background: #f8f8fa;
height: 50px;
border: solid 2px #efefef;
padding: 5px;
border-radius: 0;
margin: 0 15px 15px 15px;
position: relative;
overflow: visible;
}
.progress-bar{
background-color: #8066ee;
position: relative;
overflow: visible;
border-right: solid 2px #333;
}
.progress-bar-number{
position: absolute;
right: 0;
background: #333;
border-radius: 50%;
padding: 10px;
top: -20px;
right: -20px;
}
</style> </style>
</head> </head>