commited
This commit is contained in:
@@ -78,19 +78,51 @@ app.use(
|
||||
app.use(express.static(path.join(__dirname, 'resources')));
|
||||
|
||||
// *****************************************************
|
||||
// <!-- Section 4 : API Routes -->
|
||||
// <!-- Section 4 : Middleware -->
|
||||
// *****************************************************
|
||||
|
||||
/************************
|
||||
Header Scoreboard Routes
|
||||
*************************/
|
||||
|
||||
const fetchMatchesData = require('./resources/routes/navigation-bar/current-match-information');
|
||||
// Middleware to automatically update live scoreboard
|
||||
const fetchMatchesData = require('./resources/middleware/navigation-bar/current-match-information');
|
||||
app.use(fetchMatchesData);
|
||||
|
||||
const convert_time = require('./resources/js/navigation-bar/scoreboard-header/convert-time');
|
||||
//Middleware to automatically update in-game time abbreviations
|
||||
|
||||
const convert_time = require('./resources/middleware/navigation-bar/convert-time');
|
||||
app.use(convert_time);
|
||||
|
||||
|
||||
// Leagues Page Middleware
|
||||
|
||||
const fetchLeaguesData = require('./resources/middleware/leagues-page/get-current-league-information');
|
||||
const fetchLeagueScorerData = require('./resources/middleware/leagues-page/get-current-league-top-scorers');
|
||||
|
||||
app.get('/league/:leagueID', [fetchLeaguesData, fetchLeagueScorerData], (req, res) => {
|
||||
// Render the Handlebars view with league data
|
||||
res.render('pages/leagues-page', {
|
||||
leagueID: req.params.leagueID,
|
||||
leagues: res.locals.leagues,
|
||||
scorers: res.locals.topScorers // Assuming fetchLeagueScorerData sets the data in res.locals.scorers
|
||||
});
|
||||
});
|
||||
|
||||
// Clubs Page Middleware
|
||||
|
||||
const fetchClubsData = require('./resources/middleware/clubs-page/get-current-club-information');
|
||||
|
||||
app.get('/club/:clubID', [fetchClubsData], (req, res) => {
|
||||
// Render the Handlebars view with league data
|
||||
res.render('pages/clubs-page', {
|
||||
clubID: req.params.clubID,
|
||||
clubs: res.locals.club
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
// *****************************************************
|
||||
// <!-- Section 5 : API Routes -->
|
||||
// *****************************************************
|
||||
|
||||
/************************
|
||||
Login Page Routes
|
||||
*************************/
|
||||
@@ -98,17 +130,16 @@ app.use(convert_time);
|
||||
// Redirect to the /login endpoint
|
||||
app.get('/', (req, res) => {
|
||||
res.redirect('/home');
|
||||
});
|
||||
});
|
||||
|
||||
// Render login page for /login route
|
||||
app.get('/login', (req, res) => {
|
||||
// Render login page for /login route
|
||||
app.get('/login', (req, res) => {
|
||||
res.render('pages/home');
|
||||
});
|
||||
});
|
||||
|
||||
// Trigger login form to check database for matching username and password
|
||||
app.post('/login', async (req, res) => {
|
||||
// Trigger login form to check database for matching username and password
|
||||
app.post('/login', async (req, res) => {
|
||||
try {
|
||||
|
||||
// Check if username exists in DB
|
||||
const user = await db.oneOrNone('SELECT * FROM users WHERE username = $1', req.body.username);
|
||||
|
||||
@@ -120,7 +151,7 @@ app.get('/', (req, res) => {
|
||||
// Check if password from request matches with password in DB
|
||||
const match = await bcrypt.compare(req.body.password, user.password);
|
||||
|
||||
// Check if mathc returns no data
|
||||
// Check if match returns no data
|
||||
if (!match) {
|
||||
// Render the login page with the message parameter
|
||||
return res.render('pages/home', { message: 'Password does not match' });
|
||||
@@ -132,22 +163,20 @@ app.get('/', (req, res) => {
|
||||
|
||||
// Redirect user to the home page
|
||||
res.redirect('/home');
|
||||
|
||||
} catch (error) {
|
||||
// Direct user to login screen if no user is found with matching password
|
||||
res.redirect('/register');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/************************
|
||||
/************************
|
||||
Registration Page Routes
|
||||
*************************/
|
||||
*************************/
|
||||
|
||||
// Render registration page for /register route
|
||||
app.get('/register', (req, res) => {
|
||||
// Render registration page for /register route
|
||||
app.get('/register', (req, res) => {
|
||||
res.render('pages/register');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Trigger Registration Form to Post
|
||||
app.post('/register', async (req, res) => {
|
||||
@@ -170,7 +199,6 @@ app.post('/register', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/************************
|
||||
Home Page Routes
|
||||
*************************/
|
||||
@@ -179,10 +207,22 @@ app.get('/home', (req, res) => {
|
||||
res.render('pages/home');
|
||||
});
|
||||
|
||||
/************************
|
||||
League Page Routes
|
||||
*************************/
|
||||
|
||||
// Import and call generateLeagueRoutes function
|
||||
const generateLeagueRoutes = require('./resources/routes/league-pages/generate-league-routes');
|
||||
generateLeagueRoutes(app);
|
||||
|
||||
/************************
|
||||
Club Page Routes
|
||||
*************************/
|
||||
|
||||
// Import and call generateLeagueRoutes function
|
||||
const generateClubRoutes = require('./resources/routes/club-pages/generate-club-routes');
|
||||
generateClubRoutes(app);
|
||||
|
||||
// *****************************************************
|
||||
// <!-- Section 5 : Start Server-->
|
||||
// *****************************************************
|
||||
|
||||
5
ProjectSourceCode/src/resources/css/all-pages-style.css
Normal file
5
ProjectSourceCode/src/resources/css/all-pages-style.css
Normal file
@@ -0,0 +1,5 @@
|
||||
body {
|
||||
/* background-image: url('./../img/grass-background.png'); Specify the path to your background image */
|
||||
/* background-repeat: repeat; Set the background image to repeat both horizontally and vertically */
|
||||
/* background-size: 1000px; Set the size of the background image for each repeat */
|
||||
}
|
||||
66
ProjectSourceCode/src/resources/css/club-pages/club-page.css
Normal file
66
ProjectSourceCode/src/resources/css/club-pages/club-page.css
Normal file
@@ -0,0 +1,66 @@
|
||||
#club-page-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#club-information-container {
|
||||
height: 100px;
|
||||
width: fit-content;
|
||||
margin: 20px;
|
||||
background: linear-gradient(to right, white, rgb(245, 245, 245), rgb(227, 227, 227)); /* Gradient from white to gray */
|
||||
padding: 10px 20px; /* Adjust padding as needed */
|
||||
transform: skewX(-20deg); /* Skew the banner to create a triangular side */
|
||||
cursor: pointer; /* Change cursor to pointer on hover */
|
||||
box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.4);
|
||||
|
||||
display: flex; /* Use flexbox for layout */
|
||||
align-items: center; /* Center content vertically */
|
||||
|
||||
/* Add any other styling you need for the club information container */
|
||||
}
|
||||
|
||||
#club-information-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 8px; /* Width of the red strip */
|
||||
background-color: red; /* Red color */
|
||||
}
|
||||
|
||||
#club-logo {
|
||||
margin: 0px 30px;
|
||||
transform: skewX(20deg); /* Skew the banner to create a triangular side */
|
||||
}
|
||||
|
||||
#club-title {
|
||||
transform: skewX(20deg); /* Skew the banner to create a triangular side */
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
#club-flag {
|
||||
|
||||
transform: skewX(20deg); /* Skew the banner to create a triangular side */
|
||||
height: 20px;
|
||||
margin-right: 200px;
|
||||
}
|
||||
|
||||
#club-stats-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#club-stats-container .container {
|
||||
margin: 0 10px;
|
||||
|
||||
background-color: #eaeaea; /* Example background color */
|
||||
|
||||
border: 1px solid gray;
|
||||
border-radius: 8px;
|
||||
|
||||
box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.4);
|
||||
/* Add any other styling you need for the club table container */
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#club-top-scorers-container {
|
||||
flex: 1;
|
||||
background-color: #dcdcdc; /* Example background color */
|
||||
width: 40%;
|
||||
/* Add any other styling you need for the top scorers container */
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
#league-page-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#league-information-container {
|
||||
height: 100px;
|
||||
background-color: #f0f0f0; /* Example background color */
|
||||
/* Add any other styling you need for the league information container */
|
||||
}
|
||||
|
||||
#league-stats-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#league-table-container {
|
||||
flex: 1;
|
||||
background-color: #eaeaea; /* Example background color */
|
||||
/* Add any other styling you need for the league table container */
|
||||
}
|
||||
|
||||
#top-scorers-container {
|
||||
flex: 1;
|
||||
background-color: #dcdcdc; /* Example background color */
|
||||
/* Add any other styling you need for the top scorers container */
|
||||
}
|
||||
142
ProjectSourceCode/src/resources/css/league-pages/league-page.css
Normal file
142
ProjectSourceCode/src/resources/css/league-pages/league-page.css
Normal file
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
==============================================================
|
||||
OVERALL PAGE STYLES
|
||||
==============================================================
|
||||
*/
|
||||
|
||||
#league-page-body
|
||||
{
|
||||
/* --- SIZE CONTAINER --- */
|
||||
width: 100%; /* Set the width to the full width of screen */
|
||||
padding: 10px 100px; /* Create some distance between page boundries and elements */
|
||||
|
||||
/* --- FORMAT CHILD ITEMS (Table and Leading Scorers Cards) --- */
|
||||
display: flex; /* Enable flexbox layout */
|
||||
flex-direction: column; /* Arrange child elements vertically */
|
||||
}
|
||||
|
||||
/*
|
||||
=================================
|
||||
LEAGUE INFORMATION HEADER
|
||||
=================================
|
||||
*/
|
||||
|
||||
/* Stylization for League Information Header Card */
|
||||
#league-information-container
|
||||
{
|
||||
/* --- POSITION CONTAINER --- */
|
||||
align-items: center; /* Center content vertically */
|
||||
margin: 20px;
|
||||
|
||||
/* --- SIZE CONTAINER --- */
|
||||
height: 100px;
|
||||
width: fit-content;
|
||||
padding: 10px 20px; /* Adjust padding as needed */
|
||||
|
||||
/* --- STYLE CONTAINER --- */
|
||||
background: linear-gradient(to right, white, rgb(245, 245, 245), rgb(227, 227, 227)); /* Gradient from white to gray */
|
||||
transform: skewX(-20deg); /* Skew the banner to create a triangular side */
|
||||
box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.4);
|
||||
|
||||
/* --- FORMAT CHILD ITEMS (logo, league name, flag) --- */
|
||||
display: flex; /* Enable flexbox layout */
|
||||
flex-direction: row; /* Arrange child elements horizontally */
|
||||
}
|
||||
|
||||
/*
|
||||
===========================
|
||||
LEAGUE INFORMATION CHILD ITEMS
|
||||
===========================
|
||||
*/
|
||||
|
||||
/* Adds Red Diagonal Strip at the end of the #league-information-container */
|
||||
#league-information-container::after
|
||||
{
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 8px; /* Width of the red strip */
|
||||
background-color: red; /* Red color */
|
||||
}
|
||||
|
||||
/* Styling for League Logo in League Information Header */
|
||||
#league-logo
|
||||
{
|
||||
margin: 0px 30px;
|
||||
transform: skewX(20deg); /* Skew the banner to create a triangular side */
|
||||
height: 80%;
|
||||
}
|
||||
|
||||
/* Styling for League Title in League Information Header */
|
||||
#league-title
|
||||
{
|
||||
transform: skewX(20deg); /* Skew the banner to create a triangular side */
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
/* Styling for FLag in League Information Header */
|
||||
#league-flag
|
||||
{
|
||||
transform: skewX(20deg); /* Skew the banner to create a triangular side */
|
||||
height: 20px;
|
||||
margin-right: 200px;
|
||||
}
|
||||
|
||||
/*
|
||||
=============================================
|
||||
CARDS IN PAGE BODY
|
||||
=============================================
|
||||
*/
|
||||
|
||||
#table-and-top-scorers-containers {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
/* Styling for Standings and Top Scorers Cards */
|
||||
#table-stats-container, #top-scorers-stats-container
|
||||
{
|
||||
/* --- POSITION CONTAINER --- */
|
||||
margin: 0 10px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
/* --- SIZE CONTAINER --- */
|
||||
padding: 15px;
|
||||
|
||||
/* --- STYLE CONTAINER --- */
|
||||
background: linear-gradient(to top, rgb(216, 216, 216), rgb(236, 236, 236), rgb(241, 240, 240));
|
||||
border: 1px solid gray;
|
||||
box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/*
|
||||
==============================================================
|
||||
HOVER STYLES
|
||||
==============================================================
|
||||
*/
|
||||
|
||||
/* Hover Styling for Standings and Top Scorers Cards */
|
||||
#table-stats-container:hover, #top-scorers-stats-container:hover
|
||||
{
|
||||
transform: scale(1.01); /* Scale the row by 1.1 on hover */
|
||||
box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/*
|
||||
==============================================================
|
||||
DYNAMIC PAGE WIDTH STYLES
|
||||
==============================================================
|
||||
*/
|
||||
|
||||
/* Width of Screen is Less Than 950px */
|
||||
@media (max-width: 1230px)
|
||||
{
|
||||
#table-and-top-scorers-containers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
=================================
|
||||
TABLE ADN HEADER CONTAINER
|
||||
=================================
|
||||
*/
|
||||
|
||||
/* Container for table and header */
|
||||
#league-table-container
|
||||
{
|
||||
width: 60%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
margin-right: 30px;
|
||||
|
||||
transition: transform 0.3s ease; /* Add smooth transition effect */
|
||||
}
|
||||
|
||||
/*
|
||||
=================================
|
||||
TABLE CONTAINER
|
||||
=================================
|
||||
*/
|
||||
|
||||
/* Container for Table */
|
||||
#table-stats-container
|
||||
{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Table that holds all the standing information */
|
||||
#standings-table
|
||||
{
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
|
||||
/* Table Header Styling */
|
||||
th
|
||||
{
|
||||
border-bottom: 3px solid red; /* Add red bottom border */
|
||||
}
|
||||
|
||||
/* Data in Row Style */
|
||||
td
|
||||
{
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
/* Every Odd Row in Table Style */
|
||||
tbody tr:nth-child(odd)
|
||||
{
|
||||
background-color: #d2d2d2; /* Light gray for odd rows */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
===========================
|
||||
TABLE COLUMNS STYLE
|
||||
===========================
|
||||
*/
|
||||
|
||||
/* Club Logo Column in Table Style */
|
||||
#club-logo-column {
|
||||
width: 25px;
|
||||
|
||||
/* Club Logo Style */
|
||||
#table-club-logo
|
||||
{
|
||||
width: 25px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Club Name Column in Table Style */
|
||||
#club-name-column
|
||||
{
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Points Column in Table Style */
|
||||
#points-column
|
||||
{
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
==============================================================
|
||||
HOVER STYLES
|
||||
==============================================================
|
||||
*/
|
||||
|
||||
/* Add hover effect to table rows */
|
||||
#standings-table tbody tr:hover
|
||||
{
|
||||
/* Create border around row on hover */
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 10px;
|
||||
|
||||
/* Make club logo larger on hover */
|
||||
#table-club-logo
|
||||
{
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
/* Undeline club name on hover */
|
||||
#club-name-column
|
||||
{
|
||||
text-decoration: underline; /* Add underline effect */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
==============================================================
|
||||
DYNAMIC PAGE WIDTH STYLES
|
||||
==============================================================
|
||||
*/
|
||||
|
||||
/* Stlye for Screens Smaller than 1230px */
|
||||
@media (max-width: 1230px)
|
||||
{
|
||||
#league-table-container
|
||||
{
|
||||
align-items: center;
|
||||
width: 75%;
|
||||
min-width: 580px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
=================================
|
||||
TABLE ADN HEADER CONTAINER
|
||||
=================================
|
||||
*/
|
||||
|
||||
#top-scorers-container
|
||||
{
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
#top-scorers-stats-container {
|
||||
|
||||
/* Table Header Styling */
|
||||
th
|
||||
{
|
||||
border-bottom: 3px solid red; /* Add red bottom border */
|
||||
}
|
||||
|
||||
tr
|
||||
{
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
/* Data in Row Style */
|
||||
td
|
||||
{
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
===========================
|
||||
TABLE COLUMNS STYLE
|
||||
===========================
|
||||
*/
|
||||
|
||||
#goals-column
|
||||
{
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
#top-scorers-logo
|
||||
{
|
||||
width: 25px;
|
||||
margin: 0 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#player-name-column
|
||||
{
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
#club-name-column
|
||||
{
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
/*
|
||||
==============================================================
|
||||
HOVER STYLES
|
||||
==============================================================
|
||||
*/
|
||||
|
||||
#top-scorers-stats-container tbody tr:hover
|
||||
{
|
||||
|
||||
border: 1px solid lightgray;
|
||||
|
||||
#top-scorers-logo {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
#club-name-column {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
==============================================================
|
||||
DYNAMIC PAGE WIDTH STYLES
|
||||
==============================================================
|
||||
*/
|
||||
|
||||
@media (max-width: 1230px) {
|
||||
#top-scorers-container {
|
||||
align-items: center;
|
||||
width: 75%;
|
||||
}
|
||||
}
|
||||
BIN
ProjectSourceCode/src/resources/img/grass-background.jpeg
Normal file
BIN
ProjectSourceCode/src/resources/img/grass-background.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
ProjectSourceCode/src/resources/img/grass-background.png
Normal file
BIN
ProjectSourceCode/src/resources/img/grass-background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 MiB |
@@ -1,7 +0,0 @@
|
||||
function redirectToLeaguePage(leagueName) {
|
||||
// Append the league name to the URL
|
||||
var url = "/league/" + leagueName.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
// Redirect to the league page
|
||||
window.location.href = url;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
var goalDifferenceCells = document.querySelectorAll("#goal-difference-column"); // Selecting the cells in the goal_difference column
|
||||
|
||||
goalDifferenceCells.forEach(function(cell) {
|
||||
var goalDifference = parseInt(cell.textContent);
|
||||
var color;
|
||||
|
||||
if (goalDifference < 0)
|
||||
{
|
||||
// Gradually darken the text color for negative goal differences
|
||||
var darkenFactor = Math.ceil(goalDifference / -10); // Calculate the darken factor
|
||||
var shade = Math.max(0, 255 - darkenFactor * 25); // Calculate the shade of red
|
||||
color = "rgb(" + shade + ", 0, 0)"; // Create the color value
|
||||
}
|
||||
else if (goalDifference > 0)
|
||||
{
|
||||
// Gradually darken the text color for positive goal differences
|
||||
var darkenFactor = Math.floor(goalDifference / 10); // Calculate the darken factor
|
||||
var shade = Math.max(0, 155 - darkenFactor * 15); // Adjusted the starting point to make greens darker
|
||||
color = "rgb(0, " + shade + ", 0)"; // Create the color value
|
||||
}
|
||||
else
|
||||
{
|
||||
color = "inherit"; // If goal difference is 0, leave text color unchanged
|
||||
}
|
||||
|
||||
cell.style.color = color; // Apply the calculated color
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
const axios = require('axios');
|
||||
|
||||
// Middleware function to fetch clubs data
|
||||
const fetchClubsData = async (req, res, next) => {
|
||||
try {
|
||||
// Extract club ID from the URL
|
||||
const clubID = req.params.clubID;
|
||||
|
||||
// Make GET request to the API endpoint using the club ID
|
||||
const response = await axios.get(`http://api.football-data.org/v4/teams/${clubID}/?offset=&limit=`, {
|
||||
headers: {
|
||||
'X-Auth-Token': '0aa1ed31245d4a36b1ef5a79150324b3', // Add your API key here
|
||||
},
|
||||
});
|
||||
|
||||
// Extract relevant data from the API response
|
||||
const clubData = response.data;
|
||||
|
||||
// Attach the data to res.locals
|
||||
res.locals.club = {
|
||||
area: {
|
||||
id: clubData.area.id,
|
||||
name: clubData.area.name,
|
||||
code: clubData.area.code,
|
||||
club_flag: clubData.area.flag,
|
||||
},
|
||||
club_id: clubData.id,
|
||||
name: clubData.name,
|
||||
shortName: clubData.shortName,
|
||||
tla: clubData.tla,
|
||||
crest: clubData.crest,
|
||||
address: clubData.address,
|
||||
website: clubData.website,
|
||||
founded: clubData.founded,
|
||||
clubColors: clubData.clubColors,
|
||||
venue: clubData.venue,
|
||||
runningCompetitions: clubData.runningCompetitions.map(competition => ({
|
||||
id: competition.id,
|
||||
name: competition.name,
|
||||
code: competition.code,
|
||||
type: competition.type,
|
||||
emblem: competition.emblem
|
||||
})),
|
||||
coach: {
|
||||
id: clubData.coach.id,
|
||||
firstName: clubData.coach.firstName,
|
||||
lastName: clubData.coach.lastName,
|
||||
name: clubData.coach.name,
|
||||
dateOfBirth: clubData.coach.dateOfBirth,
|
||||
nationality: clubData.coach.nationality,
|
||||
contract: {
|
||||
start: clubData.coach.contract.start,
|
||||
until: clubData.coach.contract.until
|
||||
}
|
||||
},
|
||||
squad: clubData.squad.map(player => ({
|
||||
id: player.id,
|
||||
name: player.name,
|
||||
position: player.position,
|
||||
dateOfBirth: player.dateOfBirth,
|
||||
nationality: player.nationality
|
||||
})),
|
||||
staff: clubData.staff,
|
||||
lastUpdated: clubData.lastUpdated
|
||||
};
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Error fetching clubs data:', error);
|
||||
res.locals.club = null; // Set to null if there's an error
|
||||
next(); // Call next middleware or route handler
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = fetchClubsData;
|
||||
@@ -0,0 +1,44 @@
|
||||
const axios = require('axios');
|
||||
|
||||
// Middleware function to fetch leagues data
|
||||
const fetchLeagueScorerData = async (req, res, next) => {
|
||||
try {
|
||||
// Extract league ID from the URL
|
||||
const leagueID = req.params.leagueID;
|
||||
|
||||
// Make GET request to the API endpoint using the league ID
|
||||
const response = await axios.get(`http://api.football-data.org/v4/competitions/${leagueID}/scorers?season&limit=20`, {
|
||||
headers: {
|
||||
'X-Auth-Token': '0aa1ed31245d4a36b1ef5a79150324b3', // Add your API key here
|
||||
},
|
||||
});
|
||||
|
||||
// Extract relevant data from the API response
|
||||
const scorerData = response.data;
|
||||
|
||||
// Attach the data to res.locals
|
||||
res.locals.topScorers = {
|
||||
scorers: scorerData.scorers.map(player => ({
|
||||
player: {
|
||||
player_id: player.player.id,
|
||||
player_name: player.player.name,
|
||||
},
|
||||
team: {
|
||||
team_id: player.player.id,
|
||||
team_name: player.team.name,
|
||||
team_crest: player.team.crest,
|
||||
},
|
||||
games_played: player.playedMatches,
|
||||
goals: player.goals,
|
||||
}))
|
||||
};
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Error fetching leagues data:', error);
|
||||
res.locals.topScorers = null; // Set to null if there's an error
|
||||
next(); // Call next middleware or route handler
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = fetchLeagueScorerData;
|
||||
@@ -0,0 +1,52 @@
|
||||
const axios = require('axios');
|
||||
|
||||
// Middleware function to fetch leagues data
|
||||
const fetchLeaguesData = async (req, res, next) => {
|
||||
try {
|
||||
// Extract league ID from the URL
|
||||
const leagueID = req.params.leagueID;
|
||||
|
||||
// Make GET request to the API endpoint using the league ID
|
||||
const response = await axios.get(`http://api.football-data.org/v4/competitions/${leagueID}/standings?season`, {
|
||||
headers: {
|
||||
'X-Auth-Token': '0aa1ed31245d4a36b1ef5a79150324b3', // Add your API key here
|
||||
},
|
||||
});
|
||||
|
||||
// Extract relevant data from the API response
|
||||
const leagueData = response.data;
|
||||
|
||||
// Attach the data to res.locals
|
||||
res.locals.league = {
|
||||
area: {
|
||||
league_flag: leagueData.area.flag,
|
||||
},
|
||||
competition: {
|
||||
league_id: leagueData.competition.id,
|
||||
league_name: leagueData.competition.name,
|
||||
league_emblem: leagueData.competition.emblem
|
||||
},
|
||||
standings: leagueData.standings[0].table.map(team => ({
|
||||
table: {
|
||||
league_position: team.position,
|
||||
team_id: team.team.id,
|
||||
team_name: team.team.name,
|
||||
team_crest: team.team.crest
|
||||
},
|
||||
games_played: team.playedGames,
|
||||
wins: team.won,
|
||||
losses: team.lost,
|
||||
draws: team.draw,
|
||||
goal_difference: team.goalDifference,
|
||||
points: team.points
|
||||
}))
|
||||
};
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Error fetching leagues data:', error);
|
||||
res.locals.league = null; // Set to null if there's an error
|
||||
next(); // Call next middleware or route handler
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = fetchLeaguesData;
|
||||
@@ -0,0 +1,44 @@
|
||||
const axios = require('axios');
|
||||
|
||||
// Middleware function to fetch leagues data
|
||||
const fetchLeagueScorerData = async (req, res, next) => {
|
||||
try {
|
||||
// Extract league ID from the URL
|
||||
const leagueID = req.params.leagueID;
|
||||
|
||||
// Make GET request to the API endpoint using the league ID
|
||||
const response = await axios.get(`http://api.football-data.org/v4/competitions/${leagueID}/scorers?season&limit=20`, {
|
||||
headers: {
|
||||
'X-Auth-Token': '0aa1ed31245d4a36b1ef5a79150324b3', // Add your API key here
|
||||
},
|
||||
});
|
||||
|
||||
// Extract relevant data from the API response
|
||||
const scorerData = response.data;
|
||||
|
||||
// Attach the data to res.locals
|
||||
res.locals.topScorers = {
|
||||
scorers: scorerData.scorers.map(player => ({
|
||||
player: {
|
||||
player_id: player.player.id,
|
||||
player_name: player.player.name,
|
||||
},
|
||||
team: {
|
||||
team_id: player.team.id,
|
||||
team_name: player.team.name,
|
||||
team_crest: player.team.crest,
|
||||
},
|
||||
games_played: player.playedMatches,
|
||||
goals: player.goals,
|
||||
}))
|
||||
};
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Error fetching leagues data:', error);
|
||||
res.locals.topScorers = null; // Set to null if there's an error
|
||||
next(); // Call next middleware or route handler
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = fetchLeagueScorerData;
|
||||
@@ -9,6 +9,9 @@ const convert_time = (req, res, next) => {
|
||||
if (match.minute === "FINISHED") {
|
||||
match.minute = "FT";
|
||||
}
|
||||
else if (match.minute === "IN_PLAY") {
|
||||
match.minute = "IP"
|
||||
}
|
||||
else if (match.minute === "TIMED") {
|
||||
match.minute = "TM";
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
|
||||
// generate-league-routes.js
|
||||
|
||||
// Define a function to generate league routes
|
||||
module.exports = function generateClubRoutes(app) {
|
||||
// Define a route to handle requests to "/league/:leagueName"
|
||||
app.get('/club/:clubID', (req, res) => {
|
||||
// Extract the league name from the URL parameters
|
||||
const clubID = req.params.clubID;
|
||||
|
||||
// Render the league page template using Handlebars
|
||||
res.render('pages/club-page', { clubID: clubID });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Add click event listener to club logos
|
||||
document.querySelectorAll('#table-club-logo, #club-name, #top-scorers-logo, #club-name-column').forEach(element => {
|
||||
element.addEventListener('click', (event) => {
|
||||
// Get the club ID from the clicked club logo's clubID attribute
|
||||
const clubId = element.getAttribute('clubID');
|
||||
redirectToClubPage(clubId);
|
||||
});
|
||||
});
|
||||
|
||||
// Function to redirect to the club page
|
||||
function redirectToClubPage(clubID) {
|
||||
// Append the club ID to the URL
|
||||
var url = "/club/" + clubID;
|
||||
|
||||
// Redirect to the club page
|
||||
window.location.href = url;
|
||||
}
|
||||
@@ -6,12 +6,12 @@ const app = express();
|
||||
// Define a function to generate league routes
|
||||
module.exports = function generateLeagueRoutes(app) {
|
||||
// Define a route to handle requests to "/league/:leagueName"
|
||||
app.get('/league/:leagueName', (req, res) => {
|
||||
app.get('/league/:leagueID', (req, res) => {
|
||||
// Extract the league name from the URL parameters
|
||||
const leagueName = req.params.leagueName;
|
||||
const leagueID = req.params.leagueID;
|
||||
|
||||
// Render the league page template using Handlebars
|
||||
res.render('pages/league-page', { leagueName: leagueName });
|
||||
res.render('pages/leagues-page', { leagueID: leagueID });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
const axios = require('axios');
|
||||
|
||||
const fetchTeamNames = async (selectedLeague) => {
|
||||
try {
|
||||
const response = await axios({
|
||||
url: `http://api.football-data.org/v4/competitions/${selectedLeague}/teams`,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Auth-Token': '0aa1ed31245d4a36b1ef5a79150324b3',
|
||||
},
|
||||
});
|
||||
|
||||
const teams = response.data.teams.map(team => team.name);
|
||||
return teams;
|
||||
} catch (error) {
|
||||
console.error('Error fetching teams data:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = fetchTeamNames;
|
||||
@@ -0,0 +1,7 @@
|
||||
function redirectToLeaguePage(leagueID) {
|
||||
// Append the league name to the URL
|
||||
var url = "/league/" + leagueID;
|
||||
|
||||
// Redirect to the league page
|
||||
window.location.href = url;
|
||||
}
|
||||
24
ProjectSourceCode/src/views/pages/clubs-page.hbs
Normal file
24
ProjectSourceCode/src/views/pages/clubs-page.hbs
Normal file
@@ -0,0 +1,24 @@
|
||||
<div class="container" id="club-page-body">
|
||||
|
||||
<!-- Container for all club information (logo, name, country, etc. ) <- top 100px -->
|
||||
<div class="container" id="club-information-container">
|
||||
<img id="club-logo" src="{{club.crest}}" alt="{{club.name}} Emblem">
|
||||
<h1 id="club-title">{{club.name}}</h2>
|
||||
<img id="club-flag" src="{{club.area.club_flag}}" alt="{{club.clubData.name}} Flag">
|
||||
</div>
|
||||
|
||||
<!-- Container to display all stats for club <- bottom rest of the container -->
|
||||
<div class="container" id="club-stats-container">
|
||||
|
||||
<!-- Container to display club table <- split 50% -->
|
||||
<div class="container" id="club-players-container">
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Container to display top scorers for club <- Split 50% -->
|
||||
<div class="container" id="top-scorers-container">
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -3,32 +3,32 @@
|
||||
<div class="card-container">
|
||||
<div class="row g-3" id="card-row">
|
||||
<!-- 🇬🇧 Premier League -->
|
||||
<a href="#" onclick="redirectToLeaguePage('Premier League')" class="card-link" id="league-card">
|
||||
<a href="#" onclick="redirectToLeaguePage('2021')" class="card-link" id="league-card">
|
||||
{{> homepage/league-card leagueName="Premier League" logo="./img/homepage/premier-league/icon.png" title="./img/homepage/premier-league/title.png"}}
|
||||
</a>
|
||||
|
||||
<!-- 🇪🇸 La Liga -->
|
||||
<a href="#" onclick="redirectToLeaguePage('La Liga')" class="card-link" id="league-card">
|
||||
<a href="#" onclick="redirectToLeaguePage('2014')" class="card-link" id="league-card">
|
||||
{{> homepage/league-card leagueName="La Liga" logo="./img/homepage/la-liga/icon.png" title="./img/homepage/la-liga/title.png"}}
|
||||
</a>
|
||||
|
||||
<!-- 🇩🇪 Bundesliga -->
|
||||
<a href="#" onclick="redirectToLeaguePage('Bundesliga')" class="card-link" id="league-card">
|
||||
<a href="#" onclick="redirectToLeaguePage('2002')" class="card-link" id="league-card">
|
||||
{{> homepage/league-card leagueName="Bundesliga" logo="./img/homepage/bundesliga/icon.png" title="./img/homepage/bundesliga/title.png"}}
|
||||
</a>
|
||||
|
||||
<!-- 🇮🇹 Serie A -->
|
||||
<a href="#" onclick="redirectToLeaguePage('Serie A')" class="card-link" id="league-card">
|
||||
<a href="#" onclick="redirectToLeaguePage('2019')" class="card-link" id="league-card">
|
||||
{{> homepage/league-card leagueName="Serie A" logo="./img/homepage/serie-a/icon.png" title="./img/homepage/serie-a/title.png"}}
|
||||
</a>
|
||||
|
||||
<!-- 🇫🇷 Ligue 1 -->
|
||||
<a href="#" onclick="redirectToLeaguePage('Ligue 1')" class="card-link" id="league-card">
|
||||
<a href="#" onclick="redirectToLeaguePage('2015')" class="card-link" id="league-card">
|
||||
{{> homepage/league-card leagueName="Ligue 1" logo="./img/homepage/ligue-1/icon.png" title="./img/homepage/ligue-1/title.png"}}
|
||||
</a>
|
||||
|
||||
<!-- 🇧🇷 Brasileirao -->
|
||||
<a href="#" onclick="redirectToLeaguePage('Brasileirao')" class="card-link" id="league-card">
|
||||
<a href="#" onclick="redirectToLeaguePage('2013')" class="card-link" id="league-card">
|
||||
{{> homepage/league-card leagueName="Brasileirao" logo="./img/homepage/brasileirao/icon.png" title="./img/homepage/brasileirao/title.png"}}
|
||||
</a>
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<div class="container" id="league-page-body">
|
||||
|
||||
<!-- Container for all league information (logo, name, country, etc. ) <- top 100px -->
|
||||
<div class="container" id="league-information-container">
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Container to display all stats for league <- bottom rest of the container -->
|
||||
<div class="container" id="league-stats-container">
|
||||
|
||||
<!-- Container to display league table <- split 50% -->
|
||||
<div class="container" id="league-table-container">
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Container to display top scorers for league <- Split 50% -->
|
||||
<div class="container" id="top-scorers-container">
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
89
ProjectSourceCode/src/views/pages/leagues-page.hbs
Normal file
89
ProjectSourceCode/src/views/pages/leagues-page.hbs
Normal file
@@ -0,0 +1,89 @@
|
||||
<div id="league-page-body" class="page-container">
|
||||
|
||||
<!-- Container for all league information (logo, name, country, etc. ) <- top 100px -->
|
||||
<div id="league-information-container" class="information-container">
|
||||
<img id="league-logo" src="{{league.competition.league_emblem}}" alt="{{league.competition.league_name}} Emblem">
|
||||
<h1 id="league-title">{{league.competition.league_name}}</h2>
|
||||
<img id="league-flag" src="{{league.area.league_flag}}" alt="{{league.competition.league_name}} Flag">
|
||||
</div>
|
||||
|
||||
<!-- Container to display all stats for league <- bottom rest of the container -->
|
||||
<div id="table-and-top-scorers-containers" class="stats-container">
|
||||
|
||||
<!-- Container to display league table <- split 50% -->
|
||||
<div id="league-table-container" class="stats-container">
|
||||
|
||||
<!-- Put header above table container -->
|
||||
<div id="table-header-container" class="header">
|
||||
<h2>Table</h2>
|
||||
</div>
|
||||
|
||||
<!-- Container containing all league table stats -->
|
||||
<div id="table-stats-container" class="stats-container">
|
||||
<table id="standings-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th></th>
|
||||
<th>Club</th>
|
||||
<th>GP</th>
|
||||
<th>W</th>
|
||||
<th>L</th>
|
||||
<th>D</th>
|
||||
<th>GD</th>
|
||||
<th>Pts</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each league.standings}}
|
||||
<tr>
|
||||
<td>{{table.league_position}}</td>
|
||||
<td id="club-logo-column"><img id="table-club-logo" clubID="{{table.team_id}}" src="{{table.team_crest}}" alt="{{table.team_name}} Crest"></td>
|
||||
<td><span id="club-name-column" clubID="{{table.team_id}}">{{table.team_name}}</span></td>
|
||||
<td>{{games_played}}</td>
|
||||
<td>{{wins}}</td>
|
||||
<td>{{losses}}</td>
|
||||
<td>{{draws}}</td>
|
||||
<td id="goal-difference-column">{{goal_difference}}</td>
|
||||
<td id="points-column">{{points}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Container to display top scorers for league <- Split 50% -->
|
||||
<div id="top-scorers-container" class="stats-container">
|
||||
<div id="top-scorers-header-container" class="header">
|
||||
<h2>Top Scorers</h2>
|
||||
</div>
|
||||
|
||||
<div id="top-scorers-stats-container" class="stats-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Goals</th>
|
||||
<th></th>
|
||||
<th>Name</th>
|
||||
<th>Club</th>
|
||||
<th>GP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each topScorers.scorers}}
|
||||
<tr id="top-scorers-row">
|
||||
<td id="goals-column">{{goals}}</td>
|
||||
<td><img id="top-scorers-logo" clubID="{{team.team_id}}" src="{{team.team_crest}}" alt="{{table.team_name}} Crest"></img></td>
|
||||
<td id="player-name-column">{{player.player_name}}</td>
|
||||
<td id="club-name-column" clubID="{{team.team_id}}">{{team.team_name}}</td>
|
||||
<td>{{games_played}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -11,8 +11,11 @@
|
||||
<script src="/js/navigation-bar/user/login.js"></script>
|
||||
|
||||
<!-- Homepage Scripts -->
|
||||
<script src="/js/homepage/redirect-to-league-url.js"></script>
|
||||
<script src="/routes/league-pages/generate-league-routes.js"></script>
|
||||
<script src="/routes/league-pages/redirect-to-league-url.js"></script>
|
||||
|
||||
<!-- League Pages Scripts -->
|
||||
<script src="/js/league-page/change-goal-difference-color.js"></script>
|
||||
<script src="/routes/club-pages/redirect-to-club-url.js"></script>
|
||||
|
||||
|
||||
</footer>
|
||||
@@ -5,7 +5,8 @@
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' https://code.jquery.com https://cdn.jsdelivr.net https://stackpath.bootstrapcdn.com;">
|
||||
|
||||
|
||||
|
||||
<!-- CSS on All Pages -->
|
||||
<link rel="stylesheet" type="text/css" href="/css/all-pages-style.css">
|
||||
|
||||
<!-- Linking forms.css -->
|
||||
<link rel="stylesheet" type="text/css" href="/css/login_and_registration.css">
|
||||
@@ -22,7 +23,14 @@
|
||||
<link rel="stylesheet" type="text/css" href="/css/homepage/homepage.css">
|
||||
|
||||
<!-- League Page Stylesheets -->
|
||||
<link rel="stylesheet" type="text/css" href="/css/league-page/league-page.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/league-pages/league-page.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/league-pages/league-table.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/league-pages/top-scorers.css">
|
||||
|
||||
<!-- League Page Stylesheets -->
|
||||
<link rel="stylesheet" type="text/css" href="/css/club-pages/club-page.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/club-pages/players-table.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/club-pages/club-top-scorers.css">
|
||||
|
||||
<title>Group 6 Final Project</title>
|
||||
|
||||
|
||||
@@ -15,27 +15,27 @@
|
||||
<ul class="navbar-nav mr-auto mt-2 mt-lg-0" id="navbar-list">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/league/premier-league">Premier League</a>
|
||||
<a class="nav-link" href="/league/2021">Premier League</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/league/bundesliga">Bundesliga</a>
|
||||
<a class="nav-link" href="/league/2002">Bundesliga</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/league/la-liga">La Liga</a>
|
||||
<a class="nav-link" href="/league/2014">La Liga</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/league/serie-a">Serie A</a>
|
||||
<a class="nav-link" href="/league/2019">Serie A</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/league/ligue-1">Ligue 1</a>
|
||||
<a class="nav-link" href="/league/2015">Ligue 1</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/league/brasileirao">Brasileirao</a>
|
||||
<a class="nav-link" href="/league/2013">Brasileirao</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class="player-card" id="player-card">
|
||||
<div class="player-card-body">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Player Name</th>
|
||||
<th>Nationality</th>
|
||||
<th>Position</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>persons.name</td>
|
||||
<td>persons.nationality</td>
|
||||
<td>persons.position</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -11,3 +11,9 @@
|
||||
15 : 49 : overview of features and display of initial website
|
||||
15 : 52 : discussion on database
|
||||
15 : 53 : end of meeting
|
||||
|
||||
==== Meeting 3 04/11/2024 ====
|
||||
16 : 00 : meeting start
|
||||
16 : 01 : progress check and showcase
|
||||
16 : 08 : feedback
|
||||
16 : 10 : end of meeting
|
||||
|
||||
Reference in New Issue
Block a user