diff --git a/code/backend/src/controllers/login.controller.ts b/code/backend/src/controllers/login.controller.ts index 2fc4c432..32ea5c7e 100644 --- a/code/backend/src/controllers/login.controller.ts +++ b/code/backend/src/controllers/login.controller.ts @@ -43,14 +43,19 @@ class LoginController { } const email = loginReq.userName; - const player = await PlayerModel.findOne({ email }); - if (player?.isVerified == "pending" || player?.isVerified == "rejected") { + const manager = await ManagerModel.findOne({ + teamId: loginReq.teamId, + email: email, + }); + console.log(300); + console.log(manager); + + console.log(manager?.isVerified); + + if (manager?.isVerified == "pending" || manager?.isVerified == "rejected") { throw new Error(HttpMsg.MANAGER_NOT_VERIFIED); } - - - try { // create refresh token const refreshToken = createRefreshTokenManager(loginReq, role); @@ -84,9 +89,6 @@ class LoginController { loginReq.password ); - - - if (!isMatch) { throw new Error(HttpMsg.PASSWORD_INCORRECT); } @@ -97,7 +99,6 @@ class LoginController { throw new Error(HttpMsg.PLAYER_NOT_VERIFIED); } - try { // create refresh token const refreshToken = createRefreshToken(loginReq, role); diff --git a/code/backend/src/controllers/manager.controller.ts b/code/backend/src/controllers/manager.controller.ts index 06897ef3..4677f368 100644 --- a/code/backend/src/controllers/manager.controller.ts +++ b/code/backend/src/controllers/manager.controller.ts @@ -1,4 +1,9 @@ -import { Manager, ManagerResponse, ManagerTeamResponse } from "../models/manager.model"; +import { + Manager, + ManagerResponse, + ManagerTeamResponse, + ManagersArrayResponse, +} from "../models/manager.model"; import TeamModel from "../db/team.schema"; import managerService from "../services/manager.service"; import managersInTeamService from "../services/managers.in.team.service"; @@ -92,7 +97,7 @@ class ManagerController { // check the manager exits in that team const managerExists = await managerService.checkManagerExistsInTeam( managerEmail, - teamId + teamId ); const newManagerExists = await managerService.checkManagerExistsInTeam( @@ -100,7 +105,7 @@ class ManagerController { teamId ); - if (newManagerExists){ + if (newManagerExists) { throw new Error("New Manager already exists in the team"); } @@ -176,34 +181,51 @@ class ManagerController { } // get all the teamPlayers with their details - async getPlayers(teamId: string): Promise<{ [jerseyId: number]: TeamPlayerResponse }>{ - - try{ + async getPlayers( + teamId: string + ): Promise<{ [jerseyId: number]: TeamPlayerResponse }> { + try { const response = await managersInTeamService.getPlayersInTeam(teamId); // console.log(response); return response; - }catch(error) { + } catch (error) { console.error(error); throw error; } + } + // get all the teamManagers with their details + async getManagers(teamId: string): Promise> { + try { + const response = await managersInTeamService.getManagersInTeam(teamId); + // console.log(response); + return response; + } catch (error) { + console.error(error); + throw error; + } } //get Team Analytics - async getTeamAnalytics(teamId: string, duration:string): Promise { - + async getTeamAnalytics( + teamId: string, + duration: string + ): Promise { // 'Last Week' , 'Last Month' , 'All Time' let durationNumber: number = 0; - if (duration == "All Time"){ + if (duration == "All Time") { durationNumber = Date.now(); - } else if (duration == "Last Month"){ + } else if (duration == "Last Month") { durationNumber = 30 * 24 * 60 * 60 * 1000; - } else if (duration == "Last Week"){ + } else if (duration == "Last Week") { durationNumber = 7 * 24 * 60 * 60 * 1000; } try { - const response = await managersInTeamService.getTeamAnalytics(teamId, durationNumber); + const response = await managersInTeamService.getTeamAnalytics( + teamId, + durationNumber + ); return response; } catch (error) { console.error(error); diff --git a/code/backend/src/controllers/player.controller.ts b/code/backend/src/controllers/player.controller.ts index ae0f0034..c49ecee3 100644 --- a/code/backend/src/controllers/player.controller.ts +++ b/code/backend/src/controllers/player.controller.ts @@ -2,7 +2,7 @@ import playersInTeamService from "../services/players.in.team.service"; import managerService from "../services/manager.service"; import playerService from "../services/player.service"; import { HttpMsg } from "../exceptions/http.codes.mgs"; -import { TeamIdEmailExistsResponse, TeamResponse} from "../models/team.model"; +import { TeamIdEmailExistsResponse, TeamResponse } from "../models/team.model"; import teamService from "../services/team.service"; import { v4 as uuidv4 } from "uuid"; import PlayerModel from "../db/player.schema"; @@ -10,24 +10,29 @@ import TeamModel from "../db/team.schema"; import { sendInvitationEmail } from "../email/playerInviteEmail"; import { sendVerificationEmail } from "../email/playerVerifyEmail"; import { findSourceMap } from "module"; -import { Player, PlayerInTeamResponse, PlayerRequestBody, PlayerResponse, PlayerTeamRequest } from "../models/player.model"; +import { + Player, + PlayerInTeamResponse, + PlayerRequestBody, + PlayerResponse, + PlayerTeamRequest, +} from "../models/player.model"; import PlayerTeamModel from "../db/players.in.team.schema"; import { AnalyticsSummary } from "../types/types"; class PlayerController { - // add player to the player team collection async addNewPlayer( jerseyId: number, fullName: string, newPlayerEmail: string, teamId: string, - managerEmail: string, - + managerEmail: string ): Promise { try { // check the team exist - const teamIdEmailExistsResponse: TeamIdEmailExistsResponse = await teamService.checkTeamEmailExist(teamId,managerEmail); + const teamIdEmailExistsResponse: TeamIdEmailExistsResponse = + await teamService.checkTeamEmailExist(teamId, managerEmail); if (!teamIdEmailExistsResponse.teamExists) { throw new Error(HttpMsg.TEAM_NOT_FOUND); } @@ -35,24 +40,21 @@ class PlayerController { throw new Error(HttpMsg.MANAGER_DEOS_NOT_EXIST); } // check if player already exists in the team - const playerExistsInTeam = await playersInTeamService.checkPlayerExistsInTeam( - jerseyId, - teamId, - ); + const playerExistsInTeam = + await playersInTeamService.checkPlayerExistsInTeam(jerseyId, teamId); - if (playerExistsInTeam) { throw new Error(HttpMsg.PLAYER_ALREADY_EXISTS_IN_TEAM); - // If player not exist in that team => added to the team (player in teams) - }else{ + // If player not exist in that team => added to the team (player in teams) + } else { // Create a player with an invitation token const invitationToken = generateInvitationToken(); const teamInstance = await TeamModel.findOne({ teamId }); const teamName = teamInstance?.teamName; // Add null check using optional chaining operator const playerInTeamResponse = await playersInTeamService.addPlayerToTeam( - newPlayerEmail, + newPlayerEmail, teamId, jerseyId, fullName, @@ -60,13 +62,19 @@ class PlayerController { ); // Send the invitation email - await sendInvitationEmail(fullName, newPlayerEmail, invitationToken, teamName!); - return playerInTeamResponse + + await sendInvitationEmail( + fullName, + newPlayerEmail, + invitationToken, + teamName! + ); + + return playerInTeamResponse; } - - // // check if new player already has an account - // //TODO: Remove if user will not be able to create an account within 30 days + // // check if new player already has an account + // //TODO: Remove if user will not be able to create an account within 30 days // const playerExists = await playerService.checkPlayerExists(newPlayerEmail); // // If player not exist in the player collection => added to the player collection @@ -78,58 +86,49 @@ class PlayerController { // newPlayerEmail // ); // } - - } catch (error) { console.error(error); throw error; } - } - // create player - async createPlayer( - email: string, - password: string - ): Promise { + // create player + async createPlayer(email: string, password: string): Promise { try { // check if player already has an account const exists: boolean = await playerService.checkPlayerExists(email); if (exists) { throw new Error(HttpMsg.PLAYER_ALREADY_HAS_ACCOUNT); - } else { // Create a new player account // const playerRequestBody: PlayerRequestBody = new PlayerRequestBody( // email, // password // ); - + // Create a player with an invitation token const invitationToken = generateInvitationToken(); - const playerResponse = await playerService.createPlayer(email, password, invitationToken); + const playerResponse = await playerService.createPlayer( + email, + password, + invitationToken + ); // Send the verification email await sendVerificationEmail(email, invitationToken); return playerResponse; - } } catch (error) { console.error(error); throw error; } - } //get player details - async getPlayer( - playerEmail: string - ): Promise { + async getPlayer(playerEmail: string): Promise { try { - const playerResponse = await playerService.getPlayer( - playerEmail - ); + const playerResponse = await playerService.getPlayer(playerEmail); return playerResponse; } catch (error) { console.error(error); @@ -152,15 +151,20 @@ class PlayerController { } // Update player in Team async updatePlayer( - playerTeamRequest: PlayerTeamRequest, - managerEmail: string, - teamId: string, - -): Promise { - - try{ + playerTeamRequest: PlayerTeamRequest, + managerEmail: string, + teamId: string + ): Promise { + try { + const existingPlayer = await PlayerTeamModel.findOne({ + teamId: teamId, + jerseyId: playerTeamRequest.jerseyId, + }); + const newEmail = playerTeamRequest.playerEmail; + const prevEmail = existingPlayer?.playerEmail; // check the team exist and manager exist - const teamIdEmailExistsResponse: TeamIdEmailExistsResponse = await teamService.checkTeamEmailExist(teamId,managerEmail); + const teamIdEmailExistsResponse: TeamIdEmailExistsResponse = + await teamService.checkTeamEmailExist(teamId, managerEmail); if (!teamIdEmailExistsResponse.teamExists) { throw new Error(HttpMsg.TEAM_NOT_FOUND); } @@ -168,37 +172,43 @@ class PlayerController { throw new Error(HttpMsg.MANAGER_DEOS_NOT_EXIST); } // check if player exists in the team - const playerExistsInTeam = await playersInTeamService.checkPlayerExistsInTeam( - playerTeamRequest.jerseyId, - teamId, - ); + const playerExistsInTeam = + await playersInTeamService.checkPlayerExistsInTeam( + playerTeamRequest.jerseyId, + teamId + ); - if (playerExistsInTeam){ - const playerInTeamResponse = await playersInTeamService.updatePlayerInTeam(playerTeamRequest); + if (playerExistsInTeam) { + const playerInTeamResponse = + await playersInTeamService.updatePlayerInTeam( + playerTeamRequest, + teamId + ); //If the email get changed => send verification email - if (playerInTeamResponse.playerEmail != playerTeamRequest.playerEmail){ - + if (prevEmail != newEmail) { // Create a player with an invitation token const invitationToken = generateInvitationToken(); const teamInstance = await TeamModel.findOne({ teamId }); const teamName = teamInstance?.teamName; // Add null check using optional chaining operator - // Send the invitation email - await sendInvitationEmail(playerInTeamResponse.fullName, playerInTeamResponse.playerEmail, invitationToken, teamName!); + // Send the invitation email + await sendInvitationEmail( + playerInTeamResponse.fullName, + playerInTeamResponse.playerEmail, + invitationToken, + teamName! + ); } - return playerInTeamResponse; - }else{ - throw new Error(HttpMsg.PLAYER_NOT_EXISTS_IN_TEAM) + } else { + throw new Error(HttpMsg.PLAYER_NOT_EXISTS_IN_TEAM); } - - }catch(error){ + } catch (error) { console.error(error); throw error; } - } // Remove player in team @@ -206,12 +216,11 @@ class PlayerController { jerseyId: number, teamId: string, managerEmail: string - - ): Promise{ - - try{ + ): Promise { + try { // check the team exist - const teamIdEmailExistsResponse: TeamIdEmailExistsResponse = await teamService.checkTeamEmailExist(teamId,managerEmail); + const teamIdEmailExistsResponse: TeamIdEmailExistsResponse = + await teamService.checkTeamEmailExist(teamId, managerEmail); if (!teamIdEmailExistsResponse.teamExists) { throw new Error(HttpMsg.TEAM_NOT_FOUND); } @@ -219,67 +228,61 @@ class PlayerController { throw new Error(HttpMsg.MANAGER_DEOS_NOT_EXIST); } // check if player already exists in the team - const playerExistsInTeam = await playersInTeamService.checkPlayerExistsInTeam( - jerseyId, - teamId, - ); - if (playerExistsInTeam){ + const playerExistsInTeam = + await playersInTeamService.checkPlayerExistsInTeam(jerseyId, teamId); + if (playerExistsInTeam) { const isRemoved = await playersInTeamService.removePlayerInTeam( jerseyId, teamId ); return isRemoved; - }else{ + } else { throw new Error(HttpMsg.PLAYER_NOT_EXISTS_IN_TEAM); - } - - - }catch(error){ + } + } catch (error) { console.error(error); throw error; } } // Get all the teams of player - async getTeamsForPlayer( - playerEmail: string - ): Promise>{ - - try { - const teams = await playerService.getTeamsForPlayer(playerEmail); - return teams; - } catch (error) { - console.error(error); - throw new Error("Error in player service"); - } + async getTeamsForPlayer(playerEmail: string): Promise> { + try { + const teams = await playerService.getTeamsForPlayer(playerEmail); + return teams; + } catch (error) { + console.error(error); + throw new Error("Error in player service"); + } } - + async getAnalyticsSummary( email: string, duration: string - ): Promise{ + ): Promise { // 'Last Week' , 'Last Month' , 'All Time' let durationNumber: number = 0; - if (duration == "All Time"){ + if (duration == "All Time") { durationNumber = Date.now(); - } else if (duration == "Last Month"){ + } else if (duration == "Last Month") { durationNumber = 30 * 24 * 60 * 60 * 1000; - } else if (duration == "Last Week"){ + } else if (duration == "Last Week") { durationNumber = 7 * 24 * 60 * 60 * 1000; } // console.log(durationNumber); try { - const analyticsSummary = await playerService.getAnalyticsSummary(email, durationNumber); + const analyticsSummary = await playerService.getAnalyticsSummary( + email, + durationNumber + ); return analyticsSummary; } catch (error) { console.error(error); throw new Error("Error in player service"); } - } - } function generateInvitationToken(): string { // Generate a UUID (v4) using the uuid library diff --git a/code/backend/src/controllers/team.controller.ts b/code/backend/src/controllers/team.controller.ts index 57adc5a3..16a210cc 100644 --- a/code/backend/src/controllers/team.controller.ts +++ b/code/backend/src/controllers/team.controller.ts @@ -93,6 +93,8 @@ class TeamController { managerEmail, teamId ); + + // console.log(managerExists); return managerExists; } catch (error) { console.error(error); diff --git a/code/backend/src/exceptions/http.codes.mgs.ts b/code/backend/src/exceptions/http.codes.mgs.ts index d04443c9..f7b4beb7 100644 --- a/code/backend/src/exceptions/http.codes.mgs.ts +++ b/code/backend/src/exceptions/http.codes.mgs.ts @@ -30,7 +30,10 @@ enum HttpMsg { MANAGER_NOT_FOUND = "Manager not found", TEAM_NOT_FOUND = "Team not found", MANAGER_NOT_VERIFIED = "Manager not verified", + MANAGER_REMOVE_FAILED = "Manager remove failed", MANAGER_DEOS_NOT_EXIST = "Manager does not exist", + MANAGER_REMOVE_SUCCESS = "Manager remove success", + MANAGER_JOINED_SUCCESS = "Manager joined success", PLAYER_EXIT_ERROR = "Player exited error", PLAYER_ALREADY_HAS_ACCOUNT = "Player already has an account", PLAYER_ALREADY_EXISTS_IN_TEAM = "Player already exists in team", diff --git a/code/backend/src/models/manager.model.ts b/code/backend/src/models/manager.model.ts index 6b0d048f..563ee37c 100644 --- a/code/backend/src/models/manager.model.ts +++ b/code/backend/src/models/manager.model.ts @@ -51,7 +51,7 @@ class ManagerRequestBody { public firstName: string; public lastName: string; public email: string; - public password: string; + public password: string; public invitationToken: string; public isVerified: string; @@ -79,15 +79,30 @@ class ManagerTeamResponse { public teamId: string; public accepted: string; - public constructor( - managerEmail: string, - teamId: string, - accepted: string - ) { + public constructor(managerEmail: string, teamId: string, accepted: string) { this.managerEmail = managerEmail; this.teamId = teamId; this.accepted = accepted; } } -export { ManagerExistsResponse, Manager, ManagerResponse, ManagerRequestBody, ManagerTeamResponse }; +class ManagersArrayResponse { + public name?: string; + public email: string; + public verification: string; + + public constructor(email: string, name: string, verification: string) { + this.name = name; + this.email = email; + this.verification = verification; + } +} + +export { + ManagerExistsResponse, + Manager, + ManagerResponse, + ManagerRequestBody, + ManagerTeamResponse, + ManagersArrayResponse, +}; diff --git a/code/backend/src/routes/manager.route.ts b/code/backend/src/routes/manager.route.ts index 0a98dd5e..79679700 100644 --- a/code/backend/src/routes/manager.route.ts +++ b/code/backend/src/routes/manager.route.ts @@ -14,7 +14,6 @@ import ManagerModel from "../db/manager.schema"; import ROLES from "../config/roles"; import authService from "../services/auth.service"; - // Create an instance of the Express Router const router = Router(); @@ -42,12 +41,14 @@ router.post("/add", async (req: Request, res: Response) => { managerEmail, newManagerEmail, teamId - ); + ); if (state) { - res.send(HttpMsg.MANAGER_ADD_SUCCESS); + res.status(HttpCode.OK).send({ message: HttpMsg.MANAGER_ADD_SUCCESS }); } else { - res.send({ message: HttpMsg.MANAGER_ADD_FAILED }); + res + .status(HttpCode.BAD_REQUEST) + .send({ message: HttpMsg.MANAGER_ADD_FAILED }); } } catch (err) { if (err instanceof Error) { @@ -82,18 +83,21 @@ router.get("/exists/:email/:teamId", async (req: Request, res: Response) => { try { // Check if a manager with the given email exists in team - const exists: boolean = await managerController.checkManagerExistsInTeam(email, teamId); + const exists: boolean = await managerController.checkManagerExistsInTeam( + email, + teamId + ); // const existsResponse: ManagerExistsResponse = new ManagerExistsResponse( // exists // ); if (exists) { res.send(exists); - }else{ + } else { const teamExistsRes = await teamController.checkTeamExist(teamId); - if (teamExistsRes.teamExists){ + if (teamExistsRes.teamExists) { throw new Error(HttpMsg.MANAGER_DEOS_NOT_EXIST); - }else{ + } else { throw new Error(HttpMsg.TEAM_NOT_FOUND); } } @@ -113,7 +117,7 @@ router.post("/", async (req: Request, res: Response) => { // Extract manager details from the request body const teamId = req.body.teamId; const fullName = req.body.fullName; - const names = fullName.split(' '); + const names = fullName.split(" "); const firstName = names[0]; const lastName = names[1]; const email = req.body.email; @@ -225,9 +229,58 @@ router.get("/", async (req: Request, res: Response) => { } }); -// Endpoint to get player details of the team -router.get("/getTeamPlayers",async (req:Request, res: Response) => { +// Endpoint to remove a manager from a team +router.delete("/remove", async (req: Request, res: Response) => { + // Extract the 'teamId' and 'email' from the request body + const teamId = req.body.teamId; + const email = req.body.userName; + const managerEmail = req.body.email; + + console.log(teamId, email, managerEmail); + // Validate the 'teamId' and 'email' + if (!teamId || !email || !managerEmail) { + console.log(HttpMsg.BAD_REQUEST); + res.status(HttpCode.BAD_REQUEST).send({ message: HttpMsg.BAD_REQUEST }); + return; + } + + try { + //Check if the respond sending manager different from the manager to be removed + if (email == managerEmail) { + throw new Error(HttpMsg.MANAGER_REMOVE_FAILED); + } + // Check if the manager exists in the team + const manager = await ManagerModel.findOne({ + teamId: teamId, + email: managerEmail, + }); + console.log(manager); + if (!manager) { + console.log(HttpMsg.MANAGER_NOT_FOUND); + res + .status(HttpCode.BAD_REQUEST) + .send({ message: HttpMsg.MANAGER_NOT_FOUND }); + return; + } + + // Remove the manager from the team + await manager.deleteOne(); + // Send a success response to the client + res.send({ message: "Manager removed from team successfully" }); + res.status(HttpCode.OK).send({ message: HttpMsg.MANAGER_REMOVE_SUCCESS }); + } catch (error) { + if (error instanceof Error) { + // If 'error' is an instance of Error, send the error message + res.status(HttpCode.BAD_REQUEST).send({ message: error.message }); + } else { + // If 'error' is of unknown type, send a generic error message + res.status(HttpCode.BAD_REQUEST).send({ message: HttpMsg.BAD_REQUEST }); + } + } +}); +// Endpoint to get player details of the team +router.get("/getTeamPlayers", async (req: Request, res: Response) => { const managerEmail = req.body.userName; const teamId = req.body.teamId; // check the request comes from the manager @@ -243,21 +296,61 @@ router.get("/getTeamPlayers",async (req:Request, res: Response) => { return; } - - - try{ - const managerExists = await managerController.checkManagerExistsInTeam(managerEmail, teamId); + try { + const managerExists = await managerController.checkManagerExistsInTeam( + managerEmail, + teamId + ); - if (managerExists){ + if (managerExists) { const teamPlayerResponse = await managerController.getPlayers(teamId); res.send(teamPlayerResponse); - }else{ + res.status(HttpCode.OK); + } else { throw new Error(HttpMsg.MANAGER_DEOS_NOT_EXIST); } - + } catch (err) { + if (err instanceof Error) { + // If 'err' is an instance of Error, send the error message + res.status(HttpCode.BAD_REQUEST).send({ message: err.message }); + } else { + // If 'err' is of unknown type, send a generic error message + res.status(HttpCode.BAD_REQUEST).send({ message: HttpMsg.BAD_REQUEST }); + } + } +}); +// Endpoint to get manager details of the team +router.get("/getTeamManagers", async (req: Request, res: Response) => { + const managerEmail = req.body.userName; + const teamId = req.body.teamId; + // check the request comes from the manager + if (req.body.role != ROLES.MANAGER) { + console.log(HttpMsg.UNAUTHORIZED); + res.status(HttpCode.UNAUTHORIZED).send({ message: HttpMsg.BAD_REQUEST }); + return; + } + + if (!managerEmail) { + console.log(HttpMsg.BAD_REQUEST); + res.status(HttpCode.BAD_REQUEST).send({ message: HttpMsg.BAD_REQUEST }); + return; + } + + try { + const managerExists = await managerController.checkManagerExistsInTeam( + managerEmail, + teamId + ); - }catch (err) { + if (managerExists) { + const teamPlayerResponse = await managerController.getManagers(teamId); + res.send(teamPlayerResponse); + res.status(HttpCode.OK); + } else { + throw new Error(HttpMsg.MANAGER_DEOS_NOT_EXIST); + } + } catch (err) { if (err instanceof Error) { // If 'err' is an instance of Error, send the error message res.status(HttpCode.BAD_REQUEST).send({ message: err.message }); @@ -277,8 +370,6 @@ router.put("/join-team", async (req: Request, res: Response) => { const lastName = req.body.lastName; const password = req.body.password; - - // Check if either Team ID or Team Name is missing if (!teamId || !email || !firstName || !lastName || !password) { console.log(HttpMsg.BAD_REQUEST); @@ -293,62 +384,60 @@ router.put("/join-team", async (req: Request, res: Response) => { return; } - - try { - - // Check if a manager with the given email exists - const exists: boolean = await teamController.checkManagerExistsInTeam( + // Check if a manager with the given email exists + const exists: boolean = await teamController.checkManagerExistsInTeam( + email, + teamId + ); + + if (!exists) { + console.log(HttpMsg.MANAGER_DEOS_NOT_EXIST); + res.send({ message: HttpMsg.MANAGER_DEOS_NOT_EXIST }); + return; + } else { + const manager = await ManagerModel.findOne({ + email: email, + teamId: teamId, + }); + const managerAuth = await authService.checkAuthExistsForManager( email, teamId ); - if (!exists) { - console.log(HttpMsg.MANAGER_DEOS_NOT_EXIST); - res.send({ message: HttpMsg.MANAGER_DEOS_NOT_EXIST }); - return; - } else { - const manager = await ManagerModel.findOne({ - email: email, - teamId: teamId - }); - - if (manager) { - - if (manager.isVerified == "pending"){ - - throw new Error(HttpMsg.MANAGER_NOT_VERIFIED); - }else{ - manager.firstName = firstName; - manager.lastName = lastName; - - await manager.save(); - - await authService.createAuthManager( - email, - password, - teamId - ); - - const managerResponse = new ManagerResponse( - { - teamId: teamId, - firstName: manager.firstName, - lastName: manager.lastName, - email: manager.email, - password: "##", - invitationToken: " ", - isVerified: manager.isVerified - - }); - res.send(managerResponse); - } - - }else{ - res.send(HttpMsg.MANAGER_DEOS_NOT_EXIST) + console.log(manager, managerAuth); + if (manager && !managerAuth) { + if (manager.isVerified == "pending") { + throw new Error(HttpMsg.MANAGER_NOT_VERIFIED); + } else { + manager.firstName = firstName; + manager.lastName = lastName; + + await manager.save(); + + await authService.createAuthManager(email, password, teamId); + + const managerResponse = new ManagerResponse({ + teamId: teamId, + firstName: manager.firstName, + lastName: manager.lastName, + email: manager.email, + password: "##", + invitationToken: " ", + isVerified: manager.isVerified, + }); + res.send(managerResponse); + res + .status(HttpCode.OK) + .send({ message: HttpMsg.MANAGER_JOINED_SUCCESS }); } + } else { + res + .status(HttpCode.BAD_REQUEST) + .send({ message: HttpMsg.MANAGER_DEOS_NOT_EXIST }); + // res.send(HttpMsg.MANAGER_DEOS_NOT_EXIST); } - + } } catch (err) { if (err instanceof Error) { // If 'err' is an instance of Error, send the error message @@ -360,64 +449,68 @@ router.put("/join-team", async (req: Request, res: Response) => { } }); - - // Endpoint to get Team Analytics -router.get("/analytics-summary/:duration", async (req: Request, res: Response) => { - const managerEmail = req.body.userName; - const teamId = req.body.teamId; - // check the request comes from the manager - if (req.body.role != ROLES.MANAGER) { - console.log(HttpMsg.UNAUTHORIZED); - res.status(HttpCode.UNAUTHORIZED).send({ message: HttpMsg.BAD_REQUEST }); - return; - } +router.get( + "/analytics-summary/:duration", + async (req: Request, res: Response) => { + const managerEmail = req.body.userName; + const teamId = req.body.teamId; + // check the request comes from the manager + if (req.body.role != ROLES.MANAGER) { + console.log(HttpMsg.UNAUTHORIZED); + res.status(HttpCode.UNAUTHORIZED).send({ message: HttpMsg.BAD_REQUEST }); + return; + } - if (!managerEmail) { - console.log(HttpMsg.BAD_REQUEST); - res.status(HttpCode.BAD_REQUEST).send({ message: HttpMsg.BAD_REQUEST }); - return; - } + if (!managerEmail) { + console.log(HttpMsg.BAD_REQUEST); + res.status(HttpCode.BAD_REQUEST).send({ message: HttpMsg.BAD_REQUEST }); + return; + } - try { - const managerExists = await managerController.checkManagerExistsInTeam( - managerEmail, - teamId - ); + try { + const managerExists = await managerController.checkManagerExistsInTeam( + managerEmail, + teamId + ); - if (managerExists) { - const teamAnalyticsResponse = - await managerController.getTeamAnalytics(teamId, req.params.duration); - res.send(teamAnalyticsResponse); - } else { - throw new Error(HttpMsg.MANAGER_DEOS_NOT_EXIST); - } - } catch (err) { - if (err instanceof Error) { - // If 'err' is an instance of Error, send the error message - res.status(HttpCode.BAD_REQUEST).send({ message: err.message }); - } else { - // If 'err' is of unknown type, send a generic error message - res.status(HttpCode.BAD_REQUEST).send({ message: HttpMsg.BAD_REQUEST }); + if (managerExists) { + const teamAnalyticsResponse = await managerController.getTeamAnalytics( + teamId, + req.params.duration + ); + res.send(teamAnalyticsResponse); + res.status(HttpCode.OK); + } else { + throw new Error(HttpMsg.MANAGER_DEOS_NOT_EXIST); + } + } catch (err) { + if (err instanceof Error) { + // If 'err' is an instance of Error, send the error message + res.status(HttpCode.BAD_REQUEST).send({ message: err.message }); + } else { + // If 'err' is of unknown type, send a generic error message + res.status(HttpCode.BAD_REQUEST).send({ message: HttpMsg.BAD_REQUEST }); + } } } -}); +); // Endpoint Accept Invitation router.get("/accept-invitation/token/:token", async (req, res) => { const token = req.params.token; - const manager = await ManagerModel.findOne({ invitationToken: token }); - const managerInTeam = await ManagerModel.findOne({ invitationToken: token }); - if (manager && (manager.isVerified == "pending")) { + const manager = await ManagerModel.findOne({ invitationToken: token }); + const managerInTeam = await ManagerModel.findOne({ invitationToken: token }); + if (manager && manager.isVerified == "pending") { // Update manager status manager.isVerified = "verified"; await manager.save(); res.send("Invitation accepted successfully!"); - } else if (managerInTeam && (managerInTeam.isVerified == "pending")){ + } else if (managerInTeam && managerInTeam.isVerified == "pending") { // Update manager status managerInTeam.isVerified = "verified"; await managerInTeam.save(); - } else{ + } else { res.status(400).send("Invalid or expired token."); } }); diff --git a/code/backend/src/routes/player.route.ts b/code/backend/src/routes/player.route.ts index 9db1ab8a..08c0fa43 100644 --- a/code/backend/src/routes/player.route.ts +++ b/code/backend/src/routes/player.route.ts @@ -5,7 +5,10 @@ import { validateEmail } from "../utils/utils"; import playerController from "../controllers/player.controller"; import PlayerModel from "../db/player.schema"; import PlayerTeamModel from "../db/players.in.team.schema"; -import { PlayerInTeamResponse, PlayerTeamRequest } from "../models/player.model"; +import { + PlayerInTeamResponse, + PlayerTeamRequest, +} from "../models/player.model"; // Create an instance of the Express Router const router = Router(); @@ -26,7 +29,7 @@ router.post("/add", async (req: Request, res: Response) => { } // Validate email format - if (!validateEmail(newPlayerEmail)) { + if (newPlayerEmail !== "" && !validateEmail(newPlayerEmail)) { console.log(HttpMsg.INVALID_EMAIL); res.status(HttpCode.BAD_REQUEST).send({ message: HttpMsg.INVALID_EMAIL }); return; @@ -38,7 +41,7 @@ router.post("/add", async (req: Request, res: Response) => { fullName, newPlayerEmail, teamId, - managerEmail, + managerEmail ); // This is just for adding playes with verification status @@ -51,12 +54,10 @@ router.post("/add", async (req: Request, res: Response) => { // isVerified: isVerified, // }); - // Save the manager to the database // const savedManager = await playerTeamInstance.save(); res.send(playerInTeamResponse); - } catch (err) { if (err instanceof Error) { // If 'err' is an instance of Error, send the error message @@ -89,13 +90,12 @@ router.post("/", async (req: Request, res: Response) => { } try { - const playerResponse = await playerController.createPlayer( email, password); + const playerResponse = await playerController.createPlayer(email, password); if (playerResponse) { res.send(playerResponse); } else { res.send({ message: "Player create new account failed" }); } - } catch (err) { if (err instanceof Error) { // If 'err' is an instance of Error, send the error message @@ -116,9 +116,7 @@ router.get("/", async (req: Request, res: Response) => { } try { // Get Player details - const playerResponse = await playerController.getPlayer( - req.body.userName - ); + const playerResponse = await playerController.getPlayer(req.body.userName); res.send(playerResponse); } catch (err) { @@ -138,18 +136,18 @@ router.put("/update", async (req: Request, res: Response) => { const jerseyId = req.body.jerseyId; const fullName = req.body.fullName; const managerEmail = req.body.userName; // extract from middleware - const teamId = req.body.teamId; // extract from middleware + const teamId = req.body.teamId; // extract from middleware // Check if any required field is missing - if (!jerseyId || !fullName || !newPlayerEmail ) { + if (!jerseyId || !fullName || !newPlayerEmail) { const missingFields = []; - if (!jerseyId) missingFields.push("jerseyId"); - if (!fullName) missingFields.push("fullName"); - if (!newPlayerEmail) missingFields.push("newPlayerEmail"); + if (!jerseyId) missingFields.push("jerseyId"); + if (!fullName) missingFields.push("fullName"); + if (!newPlayerEmail) missingFields.push("newPlayerEmail"); - const errorMessage = `Missing required fields: ${missingFields.join(", ")}`; - console.log(errorMessage); - res.status(HttpCode.BAD_REQUEST).send({ message: errorMessage }); + const errorMessage = `Missing required fields: ${missingFields.join(", ")}`; + console.log(errorMessage); + res.status(HttpCode.BAD_REQUEST).send({ message: errorMessage }); } // Validate email format @@ -160,11 +158,13 @@ router.put("/update", async (req: Request, res: Response) => { } try { - const player = await PlayerTeamModel.find({ - jesryId: jerseyId, - teamId: teamId }); + // console.log(jerseyId, teamId); + const player = await PlayerTeamModel.findOne({ + teamId: teamId, + jerseyId: jerseyId, + }); - console.log(player ); + // console.log(player); const playerTeamRequest = new PlayerTeamRequest( newPlayerEmail, @@ -172,15 +172,16 @@ router.put("/update", async (req: Request, res: Response) => { fullName ); - - let playerInTeamResponse; + // console.log(playerTeamRequest); - if (player){ + let playerInTeamResponse; + if (player) { playerInTeamResponse = await playerController.updatePlayer( - playerTeamRequest, - managerEmail, - teamId); + playerTeamRequest, + managerEmail, + teamId + ); // if (player.playerEmail != newPlayerEmail) { // // Player with the same email already exists, update the existing player @@ -197,16 +198,13 @@ router.put("/update", async (req: Request, res: Response) => { // res.send({ message: "Player created successfully", playerInTeamResponse }); // } - - res.send({ message: "Player updated successfully", playerInTeamResponse }); - - }else{ + res.send({ + message: "Player updated successfully", + playerInTeamResponse, + }); + } else { throw new Error(HttpMsg.PLAYER_NOT_EXISTS_IN_TEAM); } - - - - } catch (err) { if (err instanceof Error) { // If 'err' is an instance of Error, send the error message @@ -219,44 +217,45 @@ router.put("/update", async (req: Request, res: Response) => { }); // Endpoint to remove player from team -router.delete("/remove",async (req:Request, res: Response) => { - const jerseyId = req.body.jerseyId; - const teamId = req.body.teamId; - const managerEmail = req.body.userName; - - // Check if any required field is missing - if (!jerseyId) { - - console.log(HttpMsg.BAD_REQUEST); - res.status(HttpCode.BAD_REQUEST).send({ message: HttpMsg.BAD_REQUEST }); - return; - } - - try{ - const isRemoved = await playerController.removePlayer(jerseyId, teamId, managerEmail); +router.delete("/remove", async (req: Request, res: Response) => { + const jerseyId = req.body.jerseyId; + const teamId = req.body.teamId; + const managerEmail = req.body.userName; - if (isRemoved) { - return res.send({ message: "Player removed from team successfully" }); - } else { - throw new Error(HttpMsg.PLAYER_REMOVE_FAILED); - } + // Check if any required field is missing + if (!jerseyId) { + console.log(HttpMsg.BAD_REQUEST); + res.status(HttpCode.BAD_REQUEST).send({ message: HttpMsg.BAD_REQUEST }); + return; + } - } catch (err) { - if (err instanceof Error) { - // If 'err' is an instance of Error, send the error message - res.status(HttpCode.BAD_REQUEST).send({ message: err.message }); - } else { - // If 'err' is of unknown type, send a generic error message - res.status(HttpCode.BAD_REQUEST).send({ message: HttpMsg.BAD_REQUEST }); - } + try { + const isRemoved = await playerController.removePlayer( + jerseyId, + teamId, + managerEmail + ); + + if (isRemoved) { + return res.send({ message: "Player removed from team successfully" }); + } else { + throw new Error(HttpMsg.PLAYER_REMOVE_FAILED); } + } catch (err) { + if (err instanceof Error) { + // If 'err' is an instance of Error, send the error message + res.status(HttpCode.BAD_REQUEST).send({ message: err.message }); + } else { + // If 'err' is of unknown type, send a generic error message + res.status(HttpCode.BAD_REQUEST).send({ message: HttpMsg.BAD_REQUEST }); + } + } }); // Endpoint to get all the teams of Player router.get("/myTeams", async (req, res) => { const playerEmail = req.body.userName; try { - const teams = await playerController.getTeamsForPlayer(playerEmail); if (teams.length > 0) { @@ -265,7 +264,6 @@ router.get("/myTeams", async (req, res) => { res.send({ message: "Player is not part of any teams" }); } } catch (err) { - if (err instanceof Error) { // If 'err' is an instance of Error, send the error message res.status(HttpCode.BAD_REQUEST).send({ message: err.message }); @@ -277,65 +275,75 @@ router.get("/myTeams", async (req, res) => { }); // Endpoint to get analytics summary -router.get("/analytics-summary/:duration",async (req:Request, res: Response) => { - if (!req.body.userName) { - console.log(HttpMsg.BAD_REQUEST); - res.status(HttpCode.BAD_REQUEST).send({ message: HttpMsg.BAD_REQUEST }); - return; - } - try { - - const playerEmail = req.body.userName; // player email is retrieved from the request body +router.get( + "/analytics-summary/:duration", + async (req: Request, res: Response) => { + if (!req.body.userName) { + console.log(HttpMsg.BAD_REQUEST); + res.status(HttpCode.BAD_REQUEST).send({ message: HttpMsg.BAD_REQUEST }); + return; + } + try { + const playerEmail = req.body.userName; // player email is retrieved from the request body + console.log(playerEmail); - // Check if player exists - const playerExists = await playerController.checkPlayerExists(playerEmail); + // Check if player exists + const playerExists = await playerController.checkPlayerExists( + playerEmail + ); - if (!playerExists) { - throw new Error(HttpMsg.PLAYER_DOES_NOT_EXIST); - } + if (!playerExists) { + throw new Error(HttpMsg.PLAYER_DOES_NOT_EXIST); + } - // Assuming 'getAnalyticsSummary' is a function in your playerController - // const analyticsSummary = await playerController.getAnalyticsSummary(playerEmail, req.params.duration); - const analyticsSummary = await playerController.getAnalyticsSummary(playerEmail, req.params.duration); - res.send({ analyticsSummary }); - - } catch (error) { - console.error(error); - res.status(500).json({ message: "Internal Server Error" }); + // Assuming 'getAnalyticsSummary' is a function in your playerController + // const analyticsSummary = await playerController.getAnalyticsSummary(playerEmail, req.params.duration); + const analyticsSummary = await playerController.getAnalyticsSummary( + playerEmail, + req.params.duration + ); + res.send({ analyticsSummary }); + } catch (error) { + console.error(error); + res.status(500).json({ message: "Internal Server Error" }); + } } -}); +); // Endpoint Accept Invitation router.get("/accept-invitation/token/:token", async (req, res) => { const token = req.params.token; - // const player = await PlayerModel.findOne({ invitationToken: token }); - const playerInTeam = await PlayerTeamModel.findOne({ invitationToken: token }); + // const player = await PlayerModel.findOne({ invitationToken: token }); + const playerInTeam = await PlayerTeamModel.findOne({ + invitationToken: token, + }); if (playerInTeam && playerInTeam.isVerified == "pending") { // Update player status playerInTeam.isVerified = "verified"; await playerInTeam.save(); res.send("Invitation accepted successfully!"); - } - else{ + } else { res.status(400).send("Invalid or expired token."); } }); // Endpoint verify email -router.get("/verify-email/token/:token", async (req: Request, res: Response) => { - const token = req.params.token; - const player = await PlayerModel.findOne({ invitationToken: token }); - // const playerInTeam = await PlayerTeamModel.findOne({ invitationToken: token }); - if (player && player.isVerified == "pending") { - // Update player status - player.isVerified = "verified"; - await player.save(); - res.send("Invitation accepted successfully!"); - } - else{ - res.status(400).send("Invalid or expired token."); +router.get( + "/verify-email/token/:token", + async (req: Request, res: Response) => { + const token = req.params.token; + const player = await PlayerModel.findOne({ invitationToken: token }); + // const playerInTeam = await PlayerTeamModel.findOne({ invitationToken: token }); + if (player && player.isVerified == "pending") { + // Update player status + player.isVerified = "verified"; + await player.save(); + res.send("Invitation accepted successfully!"); + } else { + res.status(400).send("Invalid or expired token."); + } } -}); +); // Endpoint accept or denied invitation from user profile router.get("/accept-invite/:teamId/:isAccepted", async (req, res) => { @@ -346,7 +354,10 @@ router.get("/accept-invite/:teamId/:isAccepted", async (req, res) => { console.log(playerEmail, teamId, isAccepted, typeof isAccepted); try { - const playerInTeam = await PlayerTeamModel.findOne({ playerEmail: playerEmail, teamId: teamId }); + const playerInTeam = await PlayerTeamModel.findOne({ + playerEmail: playerEmail, + teamId: teamId, + }); if (playerInTeam) { if (isAccepted == "1" && playerInTeam.isVerified == "pending") { // Update player status @@ -359,7 +370,7 @@ router.get("/accept-invite/:teamId/:isAccepted", async (req, res) => { playerInTeam.isVerified = "rejected"; await playerInTeam.save(); res.send("Invitation denied successfully!"); - }else{ + } else { res.send("Invitation already accepted or denied!"); } } else { diff --git a/code/backend/src/routes/team.route.ts b/code/backend/src/routes/team.route.ts index e872a552..78f8a643 100644 --- a/code/backend/src/routes/team.route.ts +++ b/code/backend/src/routes/team.route.ts @@ -48,7 +48,8 @@ router.get("/exists/teamId/:id", async (req: Request, res: Response) => { }); // Endpoint to validate both Team ID and email existence -router.get("/exists", +router.get( + "/exists", async (req: Request<{}, {}, {}, TeamManagerInterface>, res: Response) => { // Extract Team ID and email from query parameters const teamId = req.query.teamId; @@ -175,6 +176,8 @@ router.post("/manager", async (req, res) => { teamId ); + // console.log(exists); + if (exists) { console.log(HttpMsg.MANAGER_EXISTS); res.status(HttpCode.BAD_REQUEST).send({ message: HttpMsg.MANAGER_EXISTS }); @@ -194,8 +197,6 @@ router.post("/manager", async (req, res) => { "pending" ); - - // Create the Team and get the response const teamResponse: TeamResponse | undefined = await teamController.createTeam(team); @@ -279,6 +280,5 @@ router.get("/:id", async (req: Request, res: Response) => { } }); - // Export the router for use in other files export default router; diff --git a/code/backend/src/services/managers.in.team.service.ts b/code/backend/src/services/managers.in.team.service.ts index 94cf02dd..e083da11 100644 --- a/code/backend/src/services/managers.in.team.service.ts +++ b/code/backend/src/services/managers.in.team.service.ts @@ -2,9 +2,17 @@ import ManagerModel from "../db/manager.schema"; // import ManagerTeamModel from "../db/managers.in.team.schema"; import PlayerTeamModel from "../db/players.in.team.schema"; import SessionModel from "../db/session.schema"; -import { ManagerTeamResponse } from "../models/manager.model"; +import { + ManagerTeamResponse, + ManagersArrayResponse, +} from "../models/manager.model"; import { Impact, SessionResponse } from "../models/session.model"; -import { AnalyticsSummaryTeam, ImpactStats, TeamPlayerResponse, ImpactDirection} from "../types/types"; +import { + AnalyticsSummaryTeam, + ImpactStats, + TeamPlayerResponse, + ImpactDirection, +} from "../types/types"; class ManagersInTeamService { // create team manager instance @@ -32,7 +40,6 @@ class ManagersInTeamService { invitationToken: invitationToken, }); - // Save the manager to the database const savedManager = await managerInstance.save(); @@ -54,15 +61,16 @@ class ManagersInTeamService { teamId: string ): Promise { try { + console.log(managerEmail, teamId); // check entry exists - const managerTeam = await ManagerModel.findOne({ + const managerTeam = await ManagerModel.find({ email: managerEmail, teamId: teamId, }); - - if (managerTeam) { + console.log(managerTeam); + if (managerTeam.length !== 0) { return true; - }else{ + } else { return false; } } catch (error) { @@ -103,71 +111,123 @@ class ManagersInTeamService { //{ [jerseyId: number]: TeamPlayerResponse } // get the players in the team - async getPlayersInTeam(teamId: string):Promise<{ [jerseyId: number]: TeamPlayerResponse }>{ - + async getPlayersInTeam( + teamId: string + ): Promise<{ [jerseyId: number]: TeamPlayerResponse }> { const teamPlayers: { [jerseyId: number]: TeamPlayerResponse } = {}; - try{ - + try { // Get the player teams for the given team ID - const playerTeams = await PlayerTeamModel.find({ teamId: teamId}, 'jerseyId -_id'); + const playerTeams = await PlayerTeamModel.find( + { teamId: teamId }, + "jerseyId -_id" + ); // For each player team, get the player details for (const playerTeam of playerTeams) { - const player = await PlayerTeamModel.findOne({ jerseyId: playerTeam.jerseyId, teamId: teamId }, 'fullName playerEmail isVerified -_id'); + const player = await PlayerTeamModel.findOne( + { jerseyId: playerTeam.jerseyId, teamId: teamId }, + "fullName playerEmail isVerified -_id" + ); - if (player){ + if (player) { // Add the player details to the team players object teamPlayers[playerTeam.jerseyId] = { name: player.fullName, email: player.playerEmail, - verification: player.isVerified + verification: player.isVerified, }; } } return teamPlayers; - } catch (error) { console.error(error); throw error; } + } + + // get the players in the team + async getManagersInTeam( + teamId: string + ): Promise> { + const managers: Array = []; + try { + // Get the player teams for the given team ID + const managerTeams = await ManagerModel.find( + { teamId: teamId }, + "teamId email -_id" + ); + + // For each player team, get the player details + for (const managerTeam of managerTeams) { + const manager = await ManagerModel.findOne( + { email: managerTeam.email, teamId: teamId }, + "email isVerified firstName lastName -_id" + ); + + if (manager) { + // Check if the manager has a first and last name + if (manager.firstName && manager.lastName) { + // Add the manager details to the managers array with the full name + managers.push({ + name: manager.firstName + " " + manager.lastName, + email: manager.email, + verification: manager.isVerified, + }); + } else { + // Add the manager details to the managers array with only the email and verification status + managers.push({ + email: manager.email, + verification: manager.isVerified, + }); + } + } + } + + return managers; + } catch (error) { + console.error(error); + throw error; + } } //get the team analytics - async getTeamAnalytics(teamId: string, duration: number): Promise { - + async getTeamAnalytics( + teamId: string, + duration: number + ): Promise { // Initialize variable to store data let analyticsSummary: AnalyticsSummaryTeam = { summaryData: [ { title: "Sessions", value: 0, - trend: 0, + trend: "--", }, { title: "Impacts Recorded", value: 0, - trend: 0, + trend: "--", }, { title: "Contributing Players", value: 0, - trend: 0, + trend: "--", }, { title: "Highest Contributor", - value: 0 - } + value: "--", + }, ], - tableData: [] + tableData: [], }; - + try { // Find the all the sessions for the team const sessions = await SessionModel.find({ teamId: teamId }); - // console.log(sessions); + console.log(sessions); //get all the playrs of the team (players in team) const teamPlayers = await this.getPlayersInTeam(teamId); @@ -177,7 +237,6 @@ class ManagersInTeamService { // For calculations of trends (previous) let tableDataPrev = [] as AnalyticsSummaryTeam["tableData"]; - // Table data with player name analyticsSummary.tableData = jerseyIds.map((jerseyId) => { return { @@ -186,12 +245,15 @@ class ManagersInTeamService { impacts_recorded: 0, average_impact: 0, highest_impact: 0, - dominant_direction: 'none', + dominant_direction: "none", cumulative_impact: 0, - concussions: 0 + concussions: 0, }; }); + console.log("analyticsSummary.tableData:"); + console.log(analyticsSummary.tableData); + tableDataPrev = jerseyIds.map((jerseyId) => { return { jersey_number: jerseyId, @@ -199,60 +261,80 @@ class ManagersInTeamService { impacts_recorded: 0, average_impact: 0, highest_impact: 0, - dominant_direction: 'none', + dominant_direction: "none", cumulative_impact: 0, - concussions: 0 + concussions: 0, }; }); - // Get Time period need to be get analytics - const now = Date.now(); - const previous= now - (2 * duration); // timestamp of 2 * duration ago - const current= now - (duration); // timestamp of 2 * duration ago + const now = Date.now(); + const previous = now - 2 * duration; // timestamp of 2 * duration ago + const current = now - duration; // timestamp of 2 * duration ago // In previous time period, no of sessions - let prevSessions : number= 0; + let prevSessions: number = 0; let filteredSessionsPrevious: SessionResponse[] = []; // Sessions in previous time period - if (previous>=0){ - filteredSessionsPrevious = sessions.filter(session => { + if (previous >= 0) { + filteredSessionsPrevious = sessions.filter((session) => { const createdAt = new Date(session.createdAt).getTime(); return createdAt >= previous && createdAt <= current; }); - // console.log(filteredSessionsPrevious); - await this.calculationForSessions(filteredSessionsPrevious, tableDataPrev); + console.log("filteredSessionsPrevious:"); + console.log(filteredSessionsPrevious); + await this.calculationForSessions( + filteredSessionsPrevious, + tableDataPrev + ); // Get the number of sessions - prevSessions = filteredSessionsPrevious.length + prevSessions = filteredSessionsPrevious.length; } - // Sessions in cuurent period - const filteredSessionsCurrent = sessions.filter(session => { + const filteredSessionsCurrent = sessions.filter((session) => { const createdAt = new Date(session.createdAt).getTime(); return createdAt >= current && createdAt <= now; }); + console.log("filteredSessionsCurrent:"); + console.log(filteredSessionsCurrent); // Get the number of sessions - const numberOfSessions = filteredSessionsCurrent.length + const numberOfSessions = filteredSessionsCurrent.length; analyticsSummary.summaryData[0].value = numberOfSessions; - analyticsSummary.summaryData[0].trend = Math.round(((numberOfSessions - prevSessions)*100/prevSessions)); + if (prevSessions > 0) { + analyticsSummary.summaryData[0].trend = Math.round( + ((numberOfSessions - prevSessions) * 100) / prevSessions + ); + } else { + analyticsSummary.summaryData[0].trend = "--"; + } // Fill up table data - await this.calculationForSessions(filteredSessionsCurrent, analyticsSummary.tableData); - await this.calculationForSessions(filteredSessionsPrevious, tableDataPrev); - - // console.log("analyticsSummary.tableData:"); - // console.log(analyticsSummary.tableData); - // console.log("tableDataPrev:"); - // console.log(tableDataPrev); - // console.log(filteredSessionsCurrent); - - await this.calculationSummaryData(tableDataPrev, analyticsSummary.tableData, analyticsSummary.summaryData, jerseyIds); + await this.calculationForSessions( + filteredSessionsCurrent, + analyticsSummary.tableData + ); + await this.calculationForSessions( + filteredSessionsPrevious, + tableDataPrev + ); + console.log("analyticsSummary.tableData:"); + console.log(analyticsSummary.tableData); + console.log("tableDataPrev:"); + console.log(tableDataPrev); + console.log(filteredSessionsCurrent); + + await this.calculationSummaryData( + tableDataPrev, + analyticsSummary.tableData, + analyticsSummary.summaryData, + jerseyIds + ); console.log("analyticsSummary:"); console.log(analyticsSummary); @@ -268,9 +350,8 @@ class ManagersInTeamService { async calculationForSessions( sessions: SessionResponse[], tableData: AnalyticsSummaryTeam["tableData"] - ): Promise { - try{ - + ): Promise { + try { for (const playerData of tableData) { // console.log(Number(jerseyId)); // let playerData = tableData[jerseyId]; @@ -280,29 +361,28 @@ class ManagersInTeamService { // } else { // continue; // } - + // console.log(playerData); let directionCount: { - front: number, - back: number, - left: number, - right: number + Front: number; + Back: number; + Left: number; + Right: number; } = { - front: 0, - back: 0, - left: 0, - right: 0 + Front: 0, + Back: 0, + Left: 0, + Right: 0, }; - + // For each session for (const session of sessions) { // console.log("Session: " + session.sessionId); // console.log(session.impactHistory); for (const impactPlayer of session.impactHistory) { // console.log(impactPlayer.jerseyId, Number(jerseyId)); - if (impactPlayer.jerseyId === playerData.jersey_number ){ - + if (impactPlayer.jerseyId === playerData.jersey_number) { // For each impact in the impact Player for (const impact of impactPlayer.impact) { playerData.impacts_recorded += 1; @@ -311,8 +391,12 @@ class ManagersInTeamService { playerData.highest_impact = impact.magnitude; } - playerData.average_impact = playerData.cumulative_impact / playerData.impacts_recorded; - directionCount[impact.direction as keyof typeof directionCount] += 1; + playerData.average_impact = + playerData.cumulative_impact / playerData.impacts_recorded; + + directionCount[ + impact.direction as keyof typeof directionCount + ] += 1; // console.log(playerData) // console.log(directionCount); @@ -329,16 +413,25 @@ class ManagersInTeamService { playerData.average_impact = Math.round(playerData.average_impact); if (playerData.impacts_recorded > 0) { - // Find the dominant direction + // Find the dominant direction const maxValueCurr = Math.max(...Object.values(directionCount)); - const maxKeyCurr = Object.keys(directionCount).find(key => directionCount[key as keyof typeof directionCount] === maxValueCurr); + const maxKeyCurr = Object.keys(directionCount).find( + (key) => + directionCount[key as keyof typeof directionCount] === + maxValueCurr + ); playerData.dominant_direction = maxKeyCurr as ImpactDirection; + console.log("maxValueCurr: " + maxValueCurr); + console.log("maxKeyCurr: " + maxKeyCurr); + console.log( + "playerData.dominant_direction: " + playerData.dominant_direction + ); } - - // console.log(playerData); + console.log("playerData:"); + console.log(playerData); } - }catch (error) { + } catch (error) { console.error(error); throw new Error("Error while fetching teams for player"); } @@ -349,11 +442,10 @@ class ManagersInTeamService { tableData: AnalyticsSummaryTeam["tableData"], summaryData: AnalyticsSummaryTeam["summaryData"], jerseyIds: number[] - ): Promise { - try{ - + ): Promise { + try { let highestImpactsRecorded = 0; - let playerNameWithHighestImpactsRecorded = ''; + let playerNameWithHighestImpactsRecorded = ""; let summaryDataPrev = [ { title: "Sessions", @@ -372,23 +464,28 @@ class ManagersInTeamService { }, { title: "Highest Contributor", - value: 0 - } + value: 0, + }, ]; // Fill up values for summary data; for (const jerseyId of jerseyIds) { - - const playerData = tableData.find((entry) => entry.jersey_number === jerseyId); - const playerDataPrev = tableDataPrev.find((entry) => entry.jersey_number === jerseyId); + const playerData = tableData.find( + (entry: { jersey_number: number }) => entry.jersey_number === jerseyId + ); + const playerDataPrev = tableDataPrev.find( + (entry: { jersey_number: number }) => entry.jersey_number === jerseyId + ); // Fill up summary data value ==> Sessions (already filled earlier) // Fill up summary data value ==> Impacts Recorded if (playerData) { - summaryData[1].value = Number(summaryData[1].value) + playerData.impacts_recorded; + summaryData[1].value = + Number(summaryData[1].value) + playerData.impacts_recorded; } if (playerDataPrev) { - summaryDataPrev[1].value = Number(summaryDataPrev[1].value) + playerDataPrev.impacts_recorded; + summaryDataPrev[1].value = + Number(summaryDataPrev[1].value) + playerDataPrev.impacts_recorded; } // Fill up summary data value ==> Contributors @@ -401,15 +498,16 @@ class ManagersInTeamService { } // Fill up summary data value ==> Highest Contributor - if (playerData && playerData.impacts_recorded > highestImpactsRecorded) { + if ( + playerData && + playerData.impacts_recorded > highestImpactsRecorded + ) { highestImpactsRecorded = playerData.impacts_recorded; playerNameWithHighestImpactsRecorded = playerData.name; } summaryData[3].value = playerNameWithHighestImpactsRecorded; - } - // Fill up trends // // Fill up summary data trend ==> Sessions @@ -418,20 +516,24 @@ class ManagersInTeamService { // } // Fill up summary data trend ==> Impacts Recorded - if (summaryDataPrev[1].value > 0){ - summaryData[1].trend = Math.round(((Number(summaryData[1].value) - summaryDataPrev[1].value)*100/summaryDataPrev[1].value)); + if (summaryDataPrev[1].value > 0) { + summaryData[1].trend = Math.round( + ((Number(summaryData[1].value) - summaryDataPrev[1].value) * 100) / + summaryDataPrev[1].value + ); } // Fill up summary data trend ==> Contributors - if (summaryDataPrev[2].value > 0){ - summaryData[2].trend = Math.round(((Number(summaryData[2].value) - summaryDataPrev[2].value)*100/summaryDataPrev[2].value)); + if (summaryDataPrev[2].value > 0) { + summaryData[2].trend = Math.round( + ((Number(summaryData[2].value) - summaryDataPrev[2].value) * 100) / + summaryDataPrev[2].value + ); } - - }catch (error) { + } catch (error) { console.error(error); throw new Error("Error while calculation summary data"); } } - } export default new ManagersInTeamService(); diff --git a/code/backend/src/services/player.service.ts b/code/backend/src/services/player.service.ts index 7a436b34..ee290b72 100644 --- a/code/backend/src/services/player.service.ts +++ b/code/backend/src/services/player.service.ts @@ -1,10 +1,18 @@ import PlayerModel from "../db/player.schema"; import authService from "./auth.service"; import { PlayerRequestBody, PlayerResponse } from "../models/player.model"; -import { TeamResponseWithIsVerified, TeamResponseWithJerseyId } from "../models/team.model"; +import { + TeamResponseWithIsVerified, + TeamResponseWithJerseyId, +} from "../models/team.model"; import PlayerTeamModel from "../db/players.in.team.schema"; import TeamModel from "../db/team.schema"; -import { AnalyticsSummary, ImpactStats, ImpactDirection, SessionAnalytics } from "../types/types"; +import { + AnalyticsSummary, + ImpactStats, + ImpactDirection, + SessionAnalytics, +} from "../types/types"; import { Impact, ImpactPlayer, SessionResponse } from "../models/session.model"; import SessionModel from "../db/session.schema"; @@ -38,16 +46,13 @@ class PlayerService { } // Is this use? - async addPlayer( - firstName: string, - lastName: string, - email: string,) { + async addPlayer(firstName: string, lastName: string, email: string) { try { const playerInstanceNoPassword = new PlayerModel({ firstName: firstName, lastName: lastName, email: email, - password: null + password: null, }); // Save the player to the database @@ -58,52 +63,50 @@ class PlayerService { console.error(error); throw new Error("Error adding player"); } - } async createPlayer( - email: string, + email: string, password: string, invitationToken: string - ): Promise { - try { - const playerInstance = new PlayerModel({ - email: email, - invitationToken: invitationToken, - isVerified: "pending", - }); - - // Save the player to the database - const savedPlayer = await playerInstance.save(); + ): Promise { + try { + const playerInstance = new PlayerModel({ + email: email, + invitationToken: invitationToken, + isVerified: "pending", + }); - await authService.createAuth( - email, - password, - ); + // Save the player to the database + const savedPlayer = await playerInstance.save(); + + await authService.createAuth(email, password); // Create a PalyerResponse object const playerResponse: PlayerResponse = new PlayerResponse( playerInstance.email, - playerInstance.isVerified, + playerInstance.isVerified ); - - return playerResponse; - } catch (error) { - console.error(error); - throw new Error("Error adding player"); - } - + + return playerResponse; + } catch (error) { + console.error(error); + throw new Error("Error adding player"); + } } - async updatePlayerPassword(email: string, password: string): Promise { - const player = await PlayerModel.findOne({ email: email }); - - if (player) { - const playerResponse = await authService.createAuth( - player.email, - password, - ); - return playerResponse; - } - return false; + async updatePlayerPassword( + email: string, + password: string + ): Promise { + const player = await PlayerModel.findOne({ email: email }); + + if (player) { + const playerResponse = await authService.createAuth( + player.email, + password + ); + return playerResponse; + } + return false; } async getPlayer(email: string): Promise { try { @@ -118,7 +121,7 @@ class PlayerService { // Create a PlayerResponse object const playerResponse = new PlayerResponse( playerInstance.email, - playerInstance.isVerified, + playerInstance.isVerified ); return playerResponse; @@ -128,29 +131,48 @@ class PlayerService { } } - async getTeamsForPlayer(email: string): Promise> { + async getTeamsForPlayer( + email: string + ): Promise> { try { // Fetch playerTeams - const playerTeams = await PlayerTeamModel.find({ playerEmail: email }, 'teamId isVerified'); - + const playerTeams = await PlayerTeamModel.find( + { playerEmail: email }, + "teamId isVerified" + ); + if (playerTeams.length === 0) { return []; } - const teamIds = playerTeams.map(playerTeam => playerTeam.teamId); - + const teamIds = playerTeams.map((playerTeam) => playerTeam.teamId); + // Fetch teams from TeamModel - const teams = await TeamModel.find({ teamId: { $in: teamIds } }, 'teamId teamName isVerified -_id'); + const teams = await TeamModel.find( + { teamId: { $in: teamIds } }, + "teamId teamName isVerified -_id" + ); const teamsWithIsVerified: Array = teams - .map(team => { - const matchingPlayerTeam = playerTeams.find(playerTeam => playerTeam.teamId === team.teamId); + .map((team) => { + const matchingPlayerTeam = playerTeams.find( + (playerTeam) => playerTeam.teamId === team.teamId + ); return matchingPlayerTeam - ? new TeamResponseWithIsVerified(team.teamId, team.teamName, matchingPlayerTeam.isVerified) + ? new TeamResponseWithIsVerified( + team.teamId, + team.teamName, + matchingPlayerTeam.isVerified + ) : null; }) - .filter((teamWithIsVerified): teamWithIsVerified is TeamResponseWithIsVerified => teamWithIsVerified !== null); - + .filter( + ( + teamWithIsVerified + ): teamWithIsVerified is TeamResponseWithIsVerified => + teamWithIsVerified !== null + ); + console.log(teamsWithIsVerified); return teamsWithIsVerified; } catch (error) { @@ -158,86 +180,101 @@ class PlayerService { throw new Error("Error while fetching teams for player"); } } - - async getAnalyticsSummary(email: string, duration:number): Promise{ + async getAnalyticsSummary( + email: string, + duration: number + ): Promise { let analyticsSummary: AnalyticsSummary = { summaryData: [ { title: "Cumulative Impacts", value: 0, - trend: '--', + trend: "--", }, { title: "Impacts Recorded", value: 0, - trend: '--', + trend: "--", }, { title: "Average Impact", value: 0, - trend: '--', + trend: "--", }, { title: "Highest Impact", value: 0, - trend: '--', + trend: "--", }, { title: "Dominant Direction", - value: '--', - trend: '--', - } + value: "--", + trend: "--", + }, ], histogramData: { - left: new Array(10).fill(0), - right: new Array(10).fill(0), - front: new Array(10).fill(0), - back: new Array(10).fill(0), + Left: new Array(10).fill(0), + Right: new Array(10).fill(0), + Front: new Array(10).fill(0), + Back: new Array(10).fill(0), }, criticalSessions: [], }; - try{ - + try { // Fetch playerTeams - const playerTeams = await PlayerTeamModel.find({ playerEmail: email }, 'teamId jerseyId'); - + const playerTeams = await PlayerTeamModel.find( + { playerEmail: email }, + "teamId jerseyId" + ); + if (playerTeams.length === 0) { // Should be change after finish this all // return []; console.log("No teams found for player"); } - const teamIds = playerTeams.map(playerTeam => playerTeam.teamId); - + const teamIds = playerTeams.map((playerTeam) => playerTeam.teamId); + // Fetch teams from TeamModel - const teams = await TeamModel.find({ teamId: { $in: teamIds } }, 'teamId jerseyId -_id'); + const teams = await TeamModel.find( + { teamId: { $in: teamIds } }, + "teamId jerseyId -_id" + ); //Array of player's Team id and jerseyId in the team const teamResponsesWithJerseyId: Array = teams - .map(team => { - const matchingPlayerTeam = playerTeams.find(playerTeam => playerTeam.teamId === team.teamId); + .map((team) => { + const matchingPlayerTeam = playerTeams.find( + (playerTeam) => playerTeam.teamId === team.teamId + ); return matchingPlayerTeam - ? new TeamResponseWithJerseyId(team.teamId, matchingPlayerTeam.jerseyId) + ? new TeamResponseWithJerseyId( + team.teamId, + matchingPlayerTeam.jerseyId + ) : null; }) - .filter((teamWithJerseyId): teamWithJerseyId is TeamResponseWithJerseyId => teamWithJerseyId !== null); - - // console.log(teamResponsesWithJerseyId); - + .filter( + (teamWithJerseyId): teamWithJerseyId is TeamResponseWithJerseyId => + teamWithJerseyId !== null + ); + + console.log(teamResponsesWithJerseyId); + // Initialize To store Impact stats for previous and current duration let impactStatsPrev: ImpactStats = { impactsCumulative: 0, impactsRecorded: 0, highestImpact: 0, directionCount: { - front: 0, - back: 0, - left: 0, - right: 0 + Front: 0, + Back: 0, + Left: 0, + Right: 0, }, - sessionAnalytics: [] + sessionAnalytics: [], }; let impactStatsCurr: ImpactStats = { @@ -245,22 +282,24 @@ class PlayerService { impactsRecorded: 0, highestImpact: 0, directionCount: { - front: 0, - back: 0, - left: 0, - right: 0 + Front: 0, + Back: 0, + Left: 0, + Right: 0, }, - sessionAnalytics: [] + sessionAnalytics: [], }; // Flag to check whether player has at least one impact in the previous and current duration - let flagPrev: boolean = false; - let flagCurrent: boolean = false; + const flagsObject = { + flagPrev: false, + flagCurr: false, + }; // Get Time period need to be get analytics - const now = Date.now(); - const previous= now - (2 * duration); // timestamp of 2 * duration ago - const current= now - (duration); // timestamp of 2 * duration ago + const now = Date.now(); + const previous = now - 2 * duration; // timestamp of 2 * duration ago + const current = now - duration; // timestamp of 2 * duration ago //get sessions by teamId in teamResponsesWithJerseyId for (const team of teamResponsesWithJerseyId) { @@ -269,320 +308,369 @@ class PlayerService { let sessions: Array = []; sessions = sessions.concat(await getSessionsForTeam(team.teamId)); - - - // console.log(team.teamId, sessions ); + console.log(team.teamId, sessions); let filteredSessionsPrevious: Array = []; - - if (previous>=0){ - filteredSessionsPrevious = sessions.filter(session => { + + if (previous >= 0) { + filteredSessionsPrevious = sessions.filter((session) => { const createdAt = new Date(session.createdAt).getTime(); return createdAt >= previous && createdAt <= current; }); - impactStatsPrev = await calculationForSessionsPrev(filteredSessionsPrevious, impactStatsPrev, team.jerseyId, flagPrev); + impactStatsPrev = await calculationForSessionsPrev( + filteredSessionsPrevious, + impactStatsPrev, + team.jerseyId, + flagsObject + ); // console.log("impactStatsPrev:", flagPrev); } - // console.log("Previous" + team.teamId ); - // console.log(filteredSessionsPrevious); - - - const filteredSessionsCurrent = sessions.filter(session => { + console.log("Previous" + team.teamId); + console.log(filteredSessionsPrevious); + + const filteredSessionsCurrent = sessions.filter((session) => { const createdAt = new Date(session.createdAt).getTime(); return createdAt >= current && createdAt <= now; }); - // console.log("Current" + team.teamId ); - // console.log(filteredSessionsCurrent); - - - + console.log("Current" + team.teamId); + console.log(filteredSessionsCurrent); impactStatsCurr = await calculationForSessions( - filteredSessionsCurrent, - impactStatsCurr, - team.jerseyId, + filteredSessionsCurrent, + impactStatsCurr, + team.jerseyId, analyticsSummary.histogramData, analyticsSummary.criticalSessions, - flagCurrent); - - // console.log("impactStatsCurr:", flagCurrent); - - + flagsObject + ); + console.log("After calling calculationForSessions"); + console.log("FLAGCurr:", flagsObject.flagCurr); } - - if (flagPrev || flagCurrent){ + + console.log("FLAGPrev:", flagsObject.flagPrev); + + if (flagsObject.flagPrev || flagsObject.flagCurr) { + console.log("flagPrev || flagCurrent"); // Fill up the analytics summary from impact stats ==> values - analyticsSummary.summaryData[0].value = impactStatsCurr.impactsCumulative; + analyticsSummary.summaryData[0].value = + impactStatsCurr.impactsCumulative; analyticsSummary.summaryData[1].value = impactStatsCurr.impactsRecorded; - analyticsSummary.summaryData[2].value = Math.round(impactStatsCurr.impactsCumulative / impactStatsCurr.impactsRecorded); + analyticsSummary.summaryData[2].value = Math.round( + impactStatsCurr.impactsCumulative / impactStatsCurr.impactsRecorded + ); analyticsSummary.summaryData[3].value = impactStatsCurr.highestImpact; - - const allValuesZero = Object.values(impactStatsCurr.directionCount).every((value) => value === 0); - - if (!allValuesZero){ - const maxValueCurr = Math.max(...Object.values(impactStatsCurr.directionCount)); - const maxKeyCurr = Object.keys(impactStatsCurr.directionCount).find(key => impactStatsCurr.directionCount[key as keyof typeof impactStatsCurr.directionCount] === maxValueCurr); + const allValuesZero = Object.values( + impactStatsCurr.directionCount + ).every((value) => value === 0); + + if (!allValuesZero) { + const maxValueCurr = Math.max( + ...Object.values(impactStatsCurr.directionCount) + ); + const maxKeyCurr = Object.keys(impactStatsCurr.directionCount).find( + (key) => + impactStatsCurr.directionCount[ + key as keyof typeof impactStatsCurr.directionCount + ] === maxValueCurr + ); analyticsSummary.summaryData[4].value = maxKeyCurr as string; } - // All time no need of trend - if (previous>=0){ - - + if (previous >= 0 && flagsObject.flagPrev) { // Fill up the analytics summary from impact stats ==> trends - analyticsSummary.summaryData[0].trend = Math.round((impactStatsCurr.impactsCumulative - impactStatsPrev.impactsCumulative)*100/impactStatsPrev.impactsCumulative); - analyticsSummary.summaryData[1].trend = Math.round((impactStatsCurr.impactsRecorded - impactStatsPrev.impactsRecorded)*100/impactStatsPrev.impactsRecorded); - - const averageImpactPrev = impactStatsPrev.impactsCumulative / impactStatsPrev.impactsRecorded; - const averageImpactCurr = impactStatsCurr.impactsCumulative / impactStatsCurr.impactsRecorded; - analyticsSummary.summaryData[2].trend = Math.round((averageImpactCurr - averageImpactPrev)*100/averageImpactPrev); - analyticsSummary.summaryData[3].trend = Math.round((impactStatsCurr.highestImpact - impactStatsPrev.highestImpact)*100/impactStatsPrev.highestImpact); - - const allValuesZero = Object.values(impactStatsCurr.directionCount).every((value) => value === 0); - - if (!allValuesZero){ - - const maxValuePrev = Math.max(...Object.values(impactStatsPrev.directionCount)); - const maxKeyPrev = Object.keys(impactStatsPrev.directionCount).find(key => impactStatsPrev.directionCount[key as keyof typeof impactStatsPrev.directionCount] === maxValuePrev); - analyticsSummary.summaryData[4].trend = maxKeyPrev as ImpactDirection; + analyticsSummary.summaryData[0].trend = Math.round( + ((impactStatsCurr.impactsCumulative - + impactStatsPrev.impactsCumulative) * + 100) / + impactStatsPrev.impactsCumulative + ); + analyticsSummary.summaryData[1].trend = Math.round( + ((impactStatsCurr.impactsRecorded - + impactStatsPrev.impactsRecorded) * + 100) / + impactStatsPrev.impactsRecorded + ); + + const averageImpactPrev = + impactStatsPrev.impactsCumulative / impactStatsPrev.impactsRecorded; + const averageImpactCurr = + impactStatsCurr.impactsCumulative / impactStatsCurr.impactsRecorded; + analyticsSummary.summaryData[2].trend = Math.round( + ((averageImpactCurr - averageImpactPrev) * 100) / averageImpactPrev + ); + analyticsSummary.summaryData[3].trend = Math.round( + ((impactStatsCurr.highestImpact - impactStatsPrev.highestImpact) * + 100) / + impactStatsPrev.highestImpact + ); + + const allValuesZero = Object.values( + impactStatsCurr.directionCount + ).every((value) => value === 0); + + if (!allValuesZero) { + const maxValuePrev = Math.max( + ...Object.values(impactStatsPrev.directionCount) + ); + const maxKeyPrev = Object.keys(impactStatsPrev.directionCount).find( + (key) => + impactStatsPrev.directionCount[ + key as keyof typeof impactStatsPrev.directionCount + ] === maxValuePrev + ); + analyticsSummary.summaryData[4].trend = + maxKeyPrev as ImpactDirection; // console.log("maxKey:"); // console.log(maxKeyPrev, maxValuePrev); - - // Sort the critical sessions array by cumulative impact in descending order - analyticsSummary["criticalSessions"].sort((a, b) => b.cumulative - a.cumulative); + // Sort the critical sessions array by cumulative impact in descending order + analyticsSummary["criticalSessions"].sort( + (a, b) => b.cumulative - a.cumulative + ); } // console.log( analyticsSummary["criticalSessions"]); // console.log("analyticsSummary:"); // console.log(analyticsSummary); - - - }else{ - analyticsSummary.summaryData[0].trend = '--'; - analyticsSummary.summaryData[1].trend = '--'; - analyticsSummary.summaryData[2].trend = '--'; - analyticsSummary.summaryData[3].trend = '--'; - analyticsSummary.summaryData[4].trend = '--'; + } else { + analyticsSummary.summaryData[0].trend = "--"; + analyticsSummary.summaryData[1].trend = "--"; + analyticsSummary.summaryData[2].trend = "--"; + analyticsSummary.summaryData[3].trend = "--"; + analyticsSummary.summaryData[4].trend = "--"; } - - }else{ + } else { analyticsSummary.criticalSessions = []; } - + console.log("analyticsSummary:"); return analyticsSummary; -}catch (error) { + } catch (error) { console.error(error); throw new Error("Error while fetching teams for player"); } - } + } } - // Map the sessions to SessionResponse objects - async function getSessionsForTeam(teamId: string): Promise> { - try { - // Fetch sessions from the database - const sessions = await SessionModel.find({ teamId }); - - // Map the sessions to SessionResponse objects - const sessionResponses = sessions.map(session => { - return new SessionResponse( - session.teamId, - session.sessionId, - session.sessionName, - session.createdAt, - session.updatedAt, - session.impactHistory.map(player => { - return new ImpactPlayer( - player.jerseyId, - player.impact.map(impact => { - return new Impact( - impact.magnitude, - impact.direction, - impact.timestamp, - impact.isConcussion - ); - }) - ); - }) - ); - }); - - return sessionResponses; - } catch (error) { - console.error(error); - throw new Error("Error while fetching sessions for team"); - } +// Map the sessions to SessionResponse objects +async function getSessionsForTeam( + teamId: string +): Promise> { + try { + // Fetch sessions from the database + const sessions = await SessionModel.find({ teamId }); + + // Map the sessions to SessionResponse objects + const sessionResponses = sessions.map((session) => { + return new SessionResponse( + session.teamId, + session.sessionId, + session.sessionName, + session.createdAt, + session.updatedAt, + session.impactHistory.map((player) => { + return new ImpactPlayer( + player.jerseyId, + player.impact.map((impact) => { + return new Impact( + impact.magnitude, + impact.direction, + impact.timestamp, + impact.isConcussion + ); + }) + ); + }) + ); + }); + + return sessionResponses; + } catch (error) { + console.error(error); + throw new Error("Error while fetching sessions for team"); } +} - //Calculate the analytics for each sesseions - async function calculationForSessions( - sessions: Array, - stats: ImpactStats, - jerseyId: Number, - histogramData: AnalyticsSummary["histogramData"], - criticalSessions: AnalyticsSummary["criticalSessions"], - flagCurrent: boolean - ): Promise { - try{ - - // console.log("Curr#####"); - // For each session - for (const session of sessions) { - // console.log("Session: " + session.sessionId); - - //For storing session analytics - let sessionAnalyticsItem: SessionAnalytics = { - name: session.sessionName, - date: new Date(session.createdAt).toLocaleDateString("en-US", { - - day: "numeric", - month: "short", - year: "numeric", - }), - cumulative: 0, - average: 0, - highest: 0, - }; - - let impactCountForSession:number = 0; - for (const impactPlayer of session.impactHistory) { - - // console.log(impactPlayer.jerseyId, jerseyId) - if (impactPlayer.jerseyId === jerseyId) { - - // Player has at least one impact in the current duration - flagCurrent = true; - - // For each impact in the impact Player - for (const impact of impactPlayer.impact) { - - //Session Analytics - impactCountForSession += 1; - // console.log("impactCountForSession: " + impactCountForSession); - - sessionAnalyticsItem.cumulative += impact.magnitude; - // console.log("sessionAnalyticsItem.cumulative: " + sessionAnalyticsItem.cumulative); - - if (sessionAnalyticsItem.highest < impact.magnitude){ - sessionAnalyticsItem.highest = impact.magnitude; - // console.log("sessionAnalyticsItem.highest: " + sessionAnalyticsItem.highest); - } - - sessionAnalyticsItem.average = sessionAnalyticsItem.cumulative/impactCountForSession; - // console.log("sessionAnalyticsItem.average: " + sessionAnalyticsItem.average); - - // console.log("sessionAnalyticsItem###########3:") - // console.log(sessionAnalyticsItem); - - // Update the impact stats - stats.impactsCumulative += impact.magnitude; - // console.log("stats.impactsCumulative: " + stats.impactsCumulative); - - stats.impactsRecorded += 1; - // console.log("stats.impactsRecorded: " + stats.impactsRecorded); - if (stats.highestImpact < impact.magnitude) { - - stats.highestImpact = impact.magnitude; - // console.log("stats.highestImpact: " + stats.highestImpact); - } - stats.directionCount[impact.direction as keyof typeof stats.directionCount] += 1; - // console.log("stats.directionCount: " + stats.directionCount); - - const index = Math.floor(impact.magnitude / 20); - // console.log("index: " + index); - - histogramData[impact.direction as keyof typeof histogramData][index] += 1; - // console.log("histogramData:"); - // console.log(impact.magnitude, impact.direction); - // console.log(histogramData); - - - // console.log(stats) +//Calculate the analytics for each sesseions +async function calculationForSessions( + sessions: Array, + stats: ImpactStats, + jerseyId: Number, + histogramData: AnalyticsSummary["histogramData"], + criticalSessions: AnalyticsSummary["criticalSessions"], + flagsObject: { flagCurr: boolean; flagPrev: boolean } +): Promise { + try { + console.log("Curr#####"); + // For each session + for (const session of sessions) { + // console.log("Session: " + session.sessionId); + + //For storing session analytics + let sessionAnalyticsItem: SessionAnalytics = { + name: session.sessionName, + date: new Date(session.createdAt).toLocaleDateString("en-US", { + day: "numeric", + month: "short", + year: "numeric", + }), + cumulative: 0, + average: 0, + highest: 0, + }; + + let impactCountForSession: number = 0; + for (const impactPlayer of session.impactHistory) { + console.log(impactPlayer.jerseyId, jerseyId); + if (impactPlayer.jerseyId === jerseyId) { + // Player has at least one impact in the current duration + console.log("impactPlayer.jerseyId, jerseyId Current"); + + flagsObject.flagCurr = true; + console.log("flagCurrebt:" + flagsObject.flagCurr); + // For each impact in the impact Player + for (const impact of impactPlayer.impact) { + //Session Analytics + impactCountForSession += 1; + // console.log("impactCountForSession: " + impactCountForSession); + + sessionAnalyticsItem.cumulative += impact.magnitude; + // // console.log( + // "sessionAnalyticsItem.cumulative: " + + // sessionAnalyticsItem.cumulative + // ); + + if (sessionAnalyticsItem.highest < impact.magnitude) { + sessionAnalyticsItem.highest = impact.magnitude; + // console.log( + // "sessionAnalyticsItem.highest: " + sessionAnalyticsItem.highest + // ); } - // console.log("End of each impact in the impact Player Loop"); - // Round off the average impact - - } + sessionAnalyticsItem.average = + sessionAnalyticsItem.cumulative / impactCountForSession; + console.log( + "sessionAnalyticsItem.average: " + sessionAnalyticsItem.average + ); + console.log("sessionAnalyticsItem###########3:"); + // console.log(sessionAnalyticsItem); - } + // Update the impact stats + stats.impactsCumulative += impact.magnitude; + // console.log("stats.impactsCumulative: " + stats.impactsCumulative); - sessionAnalyticsItem.average = Math.round(sessionAnalyticsItem.average); - // console.log("sessionAnalyticsItem.average: " + sessionAnalyticsItem.average); + stats.impactsRecorded += 1; + // console.log("stats.impactsRecorded: " + stats.impactsRecorded); + if (stats.highestImpact < impact.magnitude) { + stats.highestImpact = impact.magnitude; + // console.log("stats.highestImpact: " + stats.highestImpact); + } + stats.directionCount[ + impact.direction as keyof typeof stats.directionCount + ] += 1; + console.log("stats.directionCount: " + stats.directionCount); + + const index = Math.floor(impact.magnitude / 20); + // console.log("index: " + index); + // console.log(impact.direction as keyof typeof histogramData); + + histogramData[impact.direction as keyof typeof histogramData][ + index + ] += 1; + // console.log(histogramData); + // console.log("histogramData:"); + // console.log(impact.magnitude, impact.direction); + // console.log(histogramData); + + // console.log(stats); + } + console.log("End of each impact in the impact Player Loop"); + console.log("flagCurrebt:" + flagsObject.flagCurr); + // Round off the average impact + } + } - // console.log("End of each impactPlayer in the session Loop"); - - // If length of crirtical sessions<3 => directly push to the critical sessions - // Else: sort by cumulative impact => remove the least one and push only if least directly push to the critical sessions + // Else: sort by cumulative impact => remove the least one and push only if least b.cumulative - a.cumulative); + console.log(sessionAnalyticsItem); + + if ( + sessionAnalyticsItem.cumulative > + criticalSessions[criticalSessions.length - 1].cumulative + ) { + criticalSessions.pop(); criticalSessions.push(sessionAnalyticsItem); - - }else { - // Sort the critical sessions array by cumulative impact in descending order - criticalSessions.sort((a, b) => b.cumulative - a.cumulative); - //console.log(sessionAnalyticsItem); - - if (sessionAnalyticsItem.cumulative > criticalSessions[criticalSessions.length - 1].cumulative) { - criticalSessions.pop(); - criticalSessions.push(sessionAnalyticsItem); - } } } - // console.log(criticalSessions); } - return stats; - }catch (error) { - console.error(error); - throw new Error("Error while fetching teams for player"); + // console.log(criticalSessions); } + return stats; + } catch (error) { + console.error(error); + throw new Error("Error while fetching teams for player"); } +} - //Calculate the analytics for each sesseions for previous - async function calculationForSessionsPrev( - sessions: Array, - stats: ImpactStats, - jerseyId: Number, - flagPrev: boolean - ): Promise { - try{ - // console.log("Previous##") - // console.log(sessions); - for (const session of sessions) { - // console.log("Session: " + session.sessionId); - for (const impactPlayer of session.impactHistory) { - if (impactPlayer.jerseyId === jerseyId) { - - // Player has at least one impact in the previous duration - flagPrev = true; - // For each impact in the impact Player - for (const impact of impactPlayer.impact) { - - // Update the impact stats - stats.impactsCumulative += impact.magnitude; - stats.impactsRecorded += 1; - if (stats.highestImpact < impact.magnitude) { - stats.highestImpact = impact.magnitude; - } - stats.directionCount[impact.direction as keyof typeof stats.directionCount] += 1; - - - // console.log(stats) +//Calculate the analytics for each sesseions for previous +async function calculationForSessionsPrev( + sessions: Array, + stats: ImpactStats, + jerseyId: Number, + flagsObject: { flagCurr: boolean; flagPrev: boolean } +): Promise { + try { + // console.log("Previous##") + // console.log(sessions); + for (const session of sessions) { + // console.log("Session: " + session.sessionId); + for (const impactPlayer of session.impactHistory) { + console.log("impactPlayer.jerseyId, jerseyId"); + console.log(impactPlayer.jerseyId, jerseyId); + if (impactPlayer.jerseyId === jerseyId) { + // Player has at least one impact in the previous duration + flagsObject.flagPrev = true; + // For each impact in the impact Player + for (const impact of impactPlayer.impact) { + // Update the impact stats + stats.impactsCumulative += impact.magnitude; + stats.impactsRecorded += 1; + if (stats.highestImpact < impact.magnitude) { + stats.highestImpact = impact.magnitude; } + stats.directionCount[ + impact.direction as keyof typeof stats.directionCount + ] += 1; + + // console.log(stats) } } } - return stats; - }catch (error) { - console.error(error); - throw new Error("Error while fetching teams for player"); } + return stats; + } catch (error) { + console.error(error); + throw new Error("Error while fetching teams for player"); } +} export default new PlayerService(); diff --git a/code/backend/src/services/players.in.team.service.ts b/code/backend/src/services/players.in.team.service.ts index 33f39d1d..9bd31fe2 100644 --- a/code/backend/src/services/players.in.team.service.ts +++ b/code/backend/src/services/players.in.team.service.ts @@ -1,6 +1,9 @@ import PlayerModel from "../db/player.schema"; import PlayerTeamModel from "../db/players.in.team.schema"; -import { PlayerInTeamResponse, PlayerTeamRequest } from "../models/player.model"; +import { + PlayerInTeamResponse, + PlayerTeamRequest, +} from "../models/player.model"; class PlayerInTeamService { // create team player instance @@ -10,7 +13,6 @@ class PlayerInTeamService { jerseyId: number, fullName: string, invitationToken: string - ): Promise { try { // check entry exists in player in teams @@ -32,7 +34,6 @@ class PlayerInTeamService { isVerified: "pending", }); - // Save the manager to the database const savedManager = await playerTeamInstance.save(); const playerInTeamResponse = new PlayerInTeamResponse( @@ -40,7 +41,7 @@ class PlayerInTeamService { teamId, jerseyId, fullName, - "pending", + "pending" ); return playerInTeamResponse; @@ -50,8 +51,6 @@ class PlayerInTeamService { } } - - async checkPlayerExistsInTeam( jerseyId: number, teamId: string @@ -73,59 +72,56 @@ class PlayerInTeamService { return false; } async updatePlayerInTeam( - playerTeamRequest : PlayerTeamRequest - ): Promise{ - const existingPlayer = await PlayerTeamModel.findOne({ - jerseyId: playerTeamRequest.jerseyId}); - - if (existingPlayer) { - // Update properties based on your requirements - // existingPlayer.playerEmail = playerTeamRequest.playerEmail; - existingPlayer.jerseyId = playerTeamRequest.jerseyId; - existingPlayer.fullName = playerTeamRequest.fullName; - - - await existingPlayer.save(); + playerTeamRequest: PlayerTeamRequest, + teamId: string + ): Promise { + const existingPlayer = await PlayerTeamModel.findOne({ + teamId: teamId, + jerseyId: playerTeamRequest.jerseyId, + }); - const playerInTeamResponse = new PlayerInTeamResponse( - existingPlayer.playerEmail, - existingPlayer.teamId, - existingPlayer.jerseyId, - existingPlayer.fullName, - existingPlayer.isVerified, - ); - return playerInTeamResponse; + // console.log(existingPlayer); + if (existingPlayer) { + // Update properties based on your requirements + // existingPlayer.playerEmail = playerTeamRequest.playerEmail; + existingPlayer.jerseyId = playerTeamRequest.jerseyId; + existingPlayer.fullName = playerTeamRequest.fullName; + existingPlayer.playerEmail = playerTeamRequest.playerEmail; - } else { - // Handle case where player is not found - throw new Error("Player not found"); + await existingPlayer.save(); - } + const playerInTeamResponse = new PlayerInTeamResponse( + existingPlayer.playerEmail, + existingPlayer.teamId, + existingPlayer.jerseyId, + existingPlayer.fullName, + existingPlayer.isVerified + ); + return playerInTeamResponse; + } else { + // Handle case where player is not found + throw new Error("Player not found"); + } } - async removePlayerInTeam( - jerseyId: number, - teamId: string - ): Promise{ - try{ - + async removePlayerInTeam(jerseyId: number, teamId: string): Promise { + try { const playerInTeam = await PlayerTeamModel.findOne({ teamId: teamId, - jerseyId: jerseyId - }) + jerseyId: jerseyId, + }); - if (playerInTeam != null){ + if (playerInTeam != null) { await playerInTeam.deleteOne(); return true; - }else{ - return false - } - - }catch (error) { - console.error(error); - throw error; + } else { + return false; } - return false; + } catch (error) { + console.error(error); + throw error; + } + return false; } } export default new PlayerInTeamService(); diff --git a/code/backend/src/services/team.service.ts b/code/backend/src/services/team.service.ts index 743bb77b..10d827f0 100644 --- a/code/backend/src/services/team.service.ts +++ b/code/backend/src/services/team.service.ts @@ -9,9 +9,14 @@ Team; import TeamModel from "../db/team.schema"; // import ManagerTeamModel from "../db/managers.in.team.schema"; import managersInTeamService from "./managers.in.team.service"; -import { AnalyticsSummaryTeam, ImpactStats, ImpactDirection } from "../types/types"; +import { + AnalyticsSummaryTeam, + ImpactStats, + ImpactDirection, +} from "../types/types"; import SessionModel from "../db/session.schema"; import ManagerModel from "../db/manager.schema"; +import authService from "./auth.service"; class TeamService { // delete team @@ -43,11 +48,11 @@ class TeamService { // Save the manager to the database const savedTeam = await teamInstance.save(); - await managersInTeamService.addManagerToTeam( - team.teamManager, - team.teamId, - "accessToken" - ); + // await managersInTeamService.addManagerToTeam( + // team.teamManager, + // team.teamId, + // "accessToken" + // ); // Create a TeamResponse object const teamResponse = new TeamResponse({ @@ -103,18 +108,14 @@ class TeamService { // } // Initialize response with both flags set to false - const teamIdEmailExistsResponseWithIsVerified = new TeamIdEmailExistsResponseWithIsVerified( - false, - false, - "pending" - ); + const teamIdEmailExistsResponseWithIsVerified = + new TeamIdEmailExistsResponseWithIsVerified(false, false, "pending"); try { // Check if team exists // const team = await TeamModel.findOne({ teamId: teamId , managerEmail: email}); // console.log(team, email, teamId); - // const teams = await ManagerModel.find({ email: email }); // const team = teams.find(team => team.teamId === teamId); @@ -123,7 +124,7 @@ class TeamService { if (team) { console.log(team); } else { - console.log('Team not found'); + console.log("Team not found"); } if (team) { teamIdEmailExistsResponseWithIsVerified.teamExists = true; @@ -133,9 +134,19 @@ class TeamService { email: email, teamId: teamId, }); + const managerAuth = await authService.checkAuthExistsForManager( + email, + teamId + ); + if (manager) { - teamIdEmailExistsResponseWithIsVerified.managerExists = true; - teamIdEmailExistsResponseWithIsVerified.isVerified = manager.isVerified; + if (!managerAuth) { + teamIdEmailExistsResponseWithIsVerified.managerExists = false; + } else { + teamIdEmailExistsResponseWithIsVerified.managerExists = true; + } + teamIdEmailExistsResponseWithIsVerified.isVerified = + manager.isVerified; } } } catch (error) { @@ -165,7 +176,5 @@ class TeamService { return teamIdExistsResponse; } - - } export default new TeamService(); diff --git a/code/backend/src/types/types.ts b/code/backend/src/types/types.ts index 4391b069..55b291fd 100644 --- a/code/backend/src/types/types.ts +++ b/code/backend/src/types/types.ts @@ -1,25 +1,25 @@ export type AnalyticsSummary = { - summaryData: Array<{ - title: string; - value: string | number; - trend: number | ImpactDirection | '--'; - }>; - histogramData: { - left: number[]; - right: number[]; - front: number[]; - back: number[]; - }; - criticalSessions: Array<{ - name: string; - date: string; - cumulative: number; - average: number; - highest: number; - }>; + summaryData: Array<{ + title: string; + value: string | number; + trend: number | ImpactDirection | "--"; + }>; + histogramData: { + Left: number[]; + Right: number[]; + Front: number[]; + Back: number[]; }; - -export type ImpactDirection = 'left' | 'right' | 'front' | 'back'| 'none'; + criticalSessions: Array<{ + name: string; + date: string; + cumulative: number; + average: number; + highest: number; + }>; +}; + +export type ImpactDirection = "Left" | "Right" | "Front" | "Back" | "none"; export type SessionAnalytics = { name: string; @@ -30,17 +30,17 @@ export type SessionAnalytics = { }; export type ImpactStats = { - impactsCumulative: number; - impactsRecorded: number; - highestImpact: number; - directionCount: { - front: number; - back: number; - left: number; - right: number; - }; - sessionAnalytics: SessionAnalytics[]; + impactsCumulative: number; + impactsRecorded: number; + highestImpact: number; + directionCount: { + Front: number; + Back: number; + Left: number; + Right: number; }; + sessionAnalytics: SessionAnalytics[]; +}; export type TeamPlayerResponse = { name: string; @@ -52,19 +52,16 @@ export type AnalyticsSummaryTeam = { summaryData: Array<{ title: string; value: string | number; - trend?: string | number | '--'; + trend?: string | number | "--"; }>; tableData: Array<{ - jersey_number: number; - name: string; - impacts_recorded: number; - average_impact: number; - highest_impact: number; - dominant_direction: ImpactDirection; - cumulative_impact: number; - concussions: number; - }>; + jersey_number: number; + name: string; + impacts_recorded: number; + average_impact: number; + highest_impact: number; + dominant_direction: ImpactDirection; + cumulative_impact: number; + concussions: number; + }>; }; - - - \ No newline at end of file diff --git a/code/backend/swagger.json b/code/backend/swagger.json index ccd3356d..f66e4d18 100644 --- a/code/backend/swagger.json +++ b/code/backend/swagger.json @@ -19,7 +19,7 @@ "url": "http://localhost:5000" }, { - "url": "http://13.235.86.11:5000" + "url": "http://16.170.235.219:5000" } ], @@ -397,11 +397,43 @@ } } }, + + "/manager/remove": { + "delete": { + "tags": ["ManagerEndpoints"], + "summary": "Remove a manager from a team by email and Team ID - Access Token required (Only Manager can access)", + "requestBody": { + "$ref": "#/components/requestBodies/ManagerRemoveRequest" + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "example": { + "message": "Manager removed from team successfully" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "example": { + "message": "Bad Request" + } + } + } + } + } + } + }, "/manager/getTeamPlayers": { "get": { "tags": ["ManagerEndpoints"], "summary": "Get all players of a team - Access Token required", - + "responses": { "200": { "description": "Success", @@ -490,9 +522,34 @@ } } }, + "/manager/getTeamManagers": { + "get": { + "tags": ["ManagerEndpoints"], + "summary": "Get all managers of a team - Access Token required", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "$ref": "#/components/schemas/TeamManagersResponse" + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "example": { + "message": "Bad Request" + } + } + } + } + } + } + }, - "/player": { "post": { "tags": ["PlayerEndpoints"], @@ -524,7 +581,7 @@ "get": { "tags": ["PlayerEndpoints"], "summary": "Get player details", - + "responses": { "200": { "description": "Success", @@ -546,7 +603,6 @@ } } } - }, "/player/myTeams": { @@ -576,7 +632,7 @@ } } }, - + "/player/analytics-summary/{duration}": { "get": { "tags": ["PlayerEndpoints"], @@ -658,7 +714,6 @@ "content": { "application/json": { "$ref": "#/components/schemas/PlayerResponse" - } } }, @@ -674,7 +729,7 @@ } } } - }, + }, "/player/remove": { "delete": { "tags": ["PlayerEndpoints"], @@ -756,11 +811,6 @@ } } }, - - - - - "/hub/credetials": { "post": { @@ -880,7 +930,7 @@ } } }, - + "/login/player": { "post": { "tags": ["LoginEndpoints"], @@ -1002,16 +1052,10 @@ "type": "string" }, "value": { - "anyOf": [ - { "type": "string" }, - { "type": "number" } - ] + "anyOf": [{ "type": "string" }, { "type": "number" }] }, "trend": { - "anyOf": [ - { "type": "string" }, - { "type": "number" } - ] + "anyOf": [{ "type": "string" }, { "type": "number" }] } } } @@ -1019,32 +1063,30 @@ "tableData": { "type": "object", "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "impactsRecorded": { - "type": "number" - }, - "cumulativeImpact": { - "type": "number" - }, - "highestImpact": { - "type": "number" - }, - "averageImpact": { - "type": "number" - }, - "dominantDirection": { - "type": "number" - }, - "concussions": { - "type": "number" - } + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "impactsRecorded": { + "type": "number" + }, + "cumulativeImpact": { + "type": "number" + }, + "highestImpact": { + "type": "number" + }, + "averageImpact": { + "type": "number" + }, + "dominantDirection": { + "type": "number" + }, + "concussions": { + "type": "number" } - - + } } } }, @@ -1077,31 +1119,28 @@ } ], "tableData": { - "69": { - "name": "Angelo Mathews", - "impactsRecorded": 50, - "cumulativeImpact": 850, - "highestImpact": 25, - "averageImpact": 20, - "dominantDirection": "Left", - "concussions": 2 - }, - "55": { - "name": "Kusal Perera", - "impactsRecorded": 50, - "cumulativeImpact": 850, - "highestImpact": 25, - "averageImpact": 20, - "dominantDirection": "Left", - "concussions": 2 - } - + "69": { + "name": "Angelo Mathews", + "impactsRecorded": 50, + "cumulativeImpact": 850, + "highestImpact": 25, + "averageImpact": 20, + "dominantDirection": "Left", + "concussions": 2 + }, + "55": { + "name": "Kusal Perera", + "impactsRecorded": 50, + "cumulativeImpact": 850, + "highestImpact": 25, + "averageImpact": 20, + "dominantDirection": "Left", + "concussions": 2 + } } - } - }, + }, - "SessionRequest": { "type": "object", "properties": { @@ -1150,7 +1189,7 @@ } } }, - + "HubRequest": { "type": "object", "properties": { @@ -1249,9 +1288,29 @@ "email": "kusal_perera@gmail.com", "verification": "verified" } - } }, + "TeamManagersResponse": { + "type": "array", + "items": { + "type": "object", + "schemas": { + "$ref": "#/components/schemas/ManagersResponse" + } + }, + "example": [ + { + "name": "Angelo Mathews", + "email": "angelomathews@gmail.com", + "verification": "verified" + }, + { + "name": "Kusal Perera", + "email": "kusal_perera@gmail.com", + "verification": "verified" + } + ] + }, "Player": { "type": "object", @@ -1271,140 +1330,136 @@ } }, - "AnalyticsSummary": { - "type": "object", - "properties": { - "summaryData": { - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "value": { - "anyOf": [ - { "type": "string" }, - { "type": "number" } - ] - }, - "trend": { - "anyOf": [ - { "type": "number" }, - { "$ref": "#/components/schemas/ImpactDirection" } - ] - } - } - } - }, - "histogramData": { + "type": "object", + "properties": { + "summaryData": { + "type": "array", + "items": { "type": "object", "properties": { - "left": { - "type": "array", - "items": { - "type": "number" - } - }, - "right": { - "type": "array", - "items": { - "type": "number" - } + "title": { + "type": "string" }, - "front": { - "type": "array", - "items": { - "type": "number" - } + "value": { + "anyOf": [{ "type": "string" }, { "type": "number" }] }, - "back": { - "type": "array", - "items": { - "type": "number" - } - } - } - }, - "criticalSessions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "session_name": { - "type": "string" - }, - "session_date": { - "type": "string" - }, - "cumulative_impact": { - "type": "number" - }, - "average_impact": { - "type": "number" - }, - "largest_impact": { - "type": "number" - } + "trend": { + "anyOf": [ + { "type": "number" }, + { "$ref": "#/components/schemas/ImpactDirection" } + ] } } } }, - "example": { - "summaryData": [ - { - "title": "Cumulative Impacts", - "value": "850 g", - "trend": -20 + "histogramData": { + "type": "object", + "properties": { + "left": { + "type": "array", + "items": { + "type": "number" + } }, - { - "title": "Impacts Recorded", - "value": "50", - "trend": 20 + "right": { + "type": "array", + "items": { + "type": "number" + } }, - { - "title": "Average Impact", - "value": "43 g", - "trend": 40 + "front": { + "type": "array", + "items": { + "type": "number" + } }, - { - "title": "Dominant Direction", - "value": "Left", - "trend": 10 + "back": { + "type": "array", + "items": { + "type": "number" + } } - ], - "histogramData": { - "left": [0, 1, 4, 0, 0, 0, 0, 0, 0, 0], - "right": [0, 1, 4, 0, 0, 0, 0, 0, 0, 0], - "front": [0, 1, 4, 0, 0, 0, 0, 0, 0, 0], - "back": [0, 1, 4, 0, 0, 0, 0, 0, 0, 0] - }, - "criticalSessions": [ - { - "session_name": "Session 1", - "session_date": "2024-01-22", - "cumulative_impact": 50, - "average_impact": 20, - "largest_impact": 25 - }, - { - "session_name": "Session 2", - "session_date": "2024-01-23", - "cumulative_impact": 70, - "average_impact": 20, - "largest_impact": 20 - }, - { - "session_name": "Session 3", - "session_date": "2024-01-24", - "cumulative_impact": 90, - "average_impact": 20, - "largest_impact": 25 + } + }, + "criticalSessions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "session_name": { + "type": "string" + }, + "session_date": { + "type": "string" + }, + "cumulative_impact": { + "type": "number" + }, + "average_impact": { + "type": "number" + }, + "largest_impact": { + "type": "number" + } } - ] + } } - }, + }, + "example": { + "summaryData": [ + { + "title": "Cumulative Impacts", + "value": "850 g", + "trend": -20 + }, + { + "title": "Impacts Recorded", + "value": "50", + "trend": 20 + }, + { + "title": "Average Impact", + "value": "43 g", + "trend": 40 + }, + { + "title": "Dominant Direction", + "value": "Left", + "trend": 10 + } + ], + "histogramData": { + "left": [0, 1, 4, 0, 0, 0, 0, 0, 0, 0], + "right": [0, 1, 4, 0, 0, 0, 0, 0, 0, 0], + "front": [0, 1, 4, 0, 0, 0, 0, 0, 0, 0], + "back": [0, 1, 4, 0, 0, 0, 0, 0, 0, 0] + }, + "criticalSessions": [ + { + "session_name": "Session 1", + "session_date": "2024-01-22", + "cumulative_impact": 50, + "average_impact": 20, + "largest_impact": 25 + }, + { + "session_name": "Session 2", + "session_date": "2024-01-23", + "cumulative_impact": 70, + "average_impact": 20, + "largest_impact": 20 + }, + { + "session_name": "Session 3", + "session_date": "2024-01-24", + "cumulative_impact": 90, + "average_impact": 20, + "largest_impact": 25 + } + ] + } + }, "ImpactDirection": { "type": "string", @@ -1423,7 +1478,7 @@ "jerseyId": 80 } }, - + "PlayerRemoveResponse": { "type": "object", "properties": { @@ -1438,14 +1493,13 @@ "PlayerTeamRequest": { "type": "object", "properties": { - "jerseyId": { "type": "number" }, "fullName": { "type": "string" }, - + "playerEmail": { "type": "string" } @@ -1459,18 +1513,17 @@ "PlayerTeamResponse": { "type": "object", "properties": { - "jerseyId": { "type": "number" }, "fullName": { "type": "string" }, - + "playerEmail": { "type": "string" }, - "teamId":{ + "teamId": { "type": "string" } }, @@ -1552,10 +1605,7 @@ } } }, - - - "LoginResponse": { "type": "object", "properties": { @@ -1632,6 +1682,37 @@ "email": "example@example.com" } }, + "ManagersResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "verification": { + "type": "string" + } + }, + "example": { + "name": "John Doe", + "email": "example@example.com", + "verification": "verified" + } + }, + "ManagerRemoveRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + }, + "required": ["email"], + "example": { + "email": "example@example.com" + } + }, "TeamIdExistsResponse": { "type": "object", @@ -1711,10 +1792,6 @@ } }, - - - - "SessionDetailsResponse": { "type": "object", "properties": { @@ -1756,9 +1833,8 @@ "players": ["player1", "player2"] } } - - }, - + }, + "requestBodies": { "TeamCreateRequest": { "content": { @@ -1805,6 +1881,15 @@ } } }, + "ManagerRemoveRequest": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagerRemoveRequest" + } + } + } + }, "ManagerTeamRequest": { "content": { diff --git a/code/buddy-firmware/impax-buddy-firmware/src/sensors/combinedOutput.cpp b/code/buddy-firmware/impax-buddy-firmware/src/sensors/combinedOutput.cpp index 05092a56..486ed31a 100644 --- a/code/buddy-firmware/impax-buddy-firmware/src/sensors/combinedOutput.cpp +++ b/code/buddy-firmware/impax-buddy-firmware/src/sensors/combinedOutput.cpp @@ -82,9 +82,9 @@ String CombinedOutput::getDirection() else if (std::abs(aY) >= std::abs(aX) && std::abs(aY) >= std::abs(aZ)) { if (aY > 0) - return "Top"; + return "Front"; // TOP else - return "Bottom"; + return "Back"; // BOTTOM } else { diff --git a/code/client/impax/electron-builder.json5 b/code/client/impax/electron-builder.json5 index d9b7ea07..6714f878 100644 --- a/code/client/impax/electron-builder.json5 +++ b/code/client/impax/electron-builder.json5 @@ -13,6 +13,7 @@ extraResources: ["src/assets"], mac: { target: ["dmg"], + icon: "public/Icon.ico", artifactName: "${productName}-Mac-${version}-Installer.${ext}", }, win: { @@ -35,7 +36,8 @@ deleteAppDataOnUninstall: false, }, linux: { - target: ["AppImage"], + icon: "public/Icon.png", + target: ["deb"], artifactName: "${productName}-Linux-${version}.${ext}", }, } diff --git a/code/client/impax/package.json b/code/client/impax/package.json index ab33ffd3..1d542e60 100644 --- a/code/client/impax/package.json +++ b/code/client/impax/package.json @@ -6,10 +6,12 @@ }, "description": "Impax is an head impact tracking and monitoring system for athletes", "private": true, - "version": "1.0.0", + "version": "1.0.1", + "homepage": "./", "scripts": { "dev": "vite", "build": "tsc && vite build && electron-builder", + "build-linux": "tsc && vite build && electron-builder --linux", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, diff --git a/code/client/impax/public/Icon.png b/code/client/impax/public/Icon.png new file mode 100644 index 00000000..7c2753a0 Binary files /dev/null and b/code/client/impax/public/Icon.png differ diff --git a/code/client/impax/src/App.tsx b/code/client/impax/src/App.tsx index aa6e589c..13a4317f 100644 --- a/code/client/impax/src/App.tsx +++ b/code/client/impax/src/App.tsx @@ -6,17 +6,19 @@ import { HashRouter, Route, Routes } from "react-router-dom"; import routes from "./routes/routeConfig"; import { useAppState } from "./states/appState"; import { getPlayers, uploadSession } from "./services/httpClient"; +import { useSignupState } from "./states/formState"; function App() { MqttClient.getInstance(); const setIsInternetAvailable = useAppState( (state) => state.setIsInternetAvailable ); + const isLoggedInManager = useSignupState((state) => state.isLoggedInManager); return ( { - if (online) { + if (online && isLoggedInManager) { uploadSession(); getPlayers(); console.log("Back Online..."); diff --git a/code/client/impax/src/components/Analytics/ImpactSummaryCard.module.scss b/code/client/impax/src/components/Analytics/ImpactSummaryCard.module.scss index 084a21a3..aebe8f12 100644 --- a/code/client/impax/src/components/Analytics/ImpactSummaryCard.module.scss +++ b/code/client/impax/src/components/Analytics/ImpactSummaryCard.module.scss @@ -27,11 +27,11 @@ } &.longText { - font-size: 3em; + font-size: 2.4em; } &.longlongText { - font-size: 2.4em; + font-size: 2em; line-height: 0.9; letter-spacing: 0.8; } diff --git a/code/client/impax/src/components/Analytics/ImpactSummaryCard.tsx b/code/client/impax/src/components/Analytics/ImpactSummaryCard.tsx index f128c43d..3ed424f6 100644 --- a/code/client/impax/src/components/Analytics/ImpactSummaryCard.tsx +++ b/code/client/impax/src/components/Analytics/ImpactSummaryCard.tsx @@ -34,12 +34,13 @@ const ImpactSummaryCard: React.FC<{ metric: Metric; timeSpan: TimeSpan }> = ({ (String(metric.value).length > 10 && cardStyles.longText) }`} > - {metric.value} + {metric.value === "" ? "--" : metric.value} + {/* This was a backend bug, fixed in the front end */} {metric.metaUnits && ( {metric.metaUnits} )}

- {timeSpan != "All Time" && metric.trend && ( + {timeSpan != "All Time" && metric.trend !== undefined && (

{trendElement} {timeSpan} diff --git a/code/client/impax/src/components/Analytics/PlayerAnalytics/PlayerAnalytics.module.scss b/code/client/impax/src/components/Analytics/PlayerAnalytics/PlayerAnalytics.module.scss index d26b7a46..6b488514 100644 --- a/code/client/impax/src/components/Analytics/PlayerAnalytics/PlayerAnalytics.module.scss +++ b/code/client/impax/src/components/Analytics/PlayerAnalytics/PlayerAnalytics.module.scss @@ -142,6 +142,9 @@ $button-bg: rgb(14, 61, 127); font-size: 1.2em; margin-left: 1em; } + .noSessions { + margin-left: 1.2em; + } .criticalSessionContainer { margin-top: 1em; } @@ -162,8 +165,8 @@ $button-bg: rgb(14, 61, 127); .spinnerContainer { display: flex; - justify-content: start; - align-items: start; + justify-content: center; + align-items: center; height: 10em; width: 100%; } diff --git a/code/client/impax/src/components/Analytics/PlayerAnalytics/PlayerAnalytics.tsx b/code/client/impax/src/components/Analytics/PlayerAnalytics/PlayerAnalytics.tsx index de39b6d4..f4a08488 100644 --- a/code/client/impax/src/components/Analytics/PlayerAnalytics/PlayerAnalytics.tsx +++ b/code/client/impax/src/components/Analytics/PlayerAnalytics/PlayerAnalytics.tsx @@ -15,8 +15,10 @@ import { useAppState } from "../../../states/appState"; import NoInternetConnection from "../../StatusScreens/NoInternetConnection"; import ImpactSummarySkeleton from "../ImpactSummarySkeleton"; import Spinner from "../../StatusScreens/Spinner"; +import { useLoginState } from "../../../states/profileState"; const PlayerAnalytics = () => { const [timeSpan, setTimeSpan] = useState("Last Week"); + const loginInfo = useLoginState((state) => state.loginInfo); const { data: AnalyticsSummaryPlayer, isLoading } = useQuery({ queryFn: () => fetchAnalyticsSummaryPlayer(), @@ -53,13 +55,15 @@ const PlayerAnalytics = () => { } } + console.log(AnalyticsSummaryPlayer); + return (

<div className={styles.summary}> <div className={styles.info}> - <h2>John Doe's Individual Analytics</h2>{" "} - <span>0 marked concussion</span> + <h2>Analyze your Individual Impacts</h2>{" "} + <span>{loginInfo.email}</span> </div> <div className={styles.controls}> <DropdownMenu.Root> @@ -122,22 +126,24 @@ const PlayerAnalytics = () => { <div className={styles.criticalSessions}> <h2>Critical Sessions</h2> {AnalyticsSummaryPlayer?.criticalSessions?.length == 0 && ( - <p>No sessions recorded</p> + <p className={styles.noSessions}>No sessions recorded</p> )} - {AnalyticsSummaryPlayer?.criticalSessions?.map((session) => ( - <div - className={styles.criticalSessionContainer} - key={session.name} - > - <CriticalSession - name={session.name} - date={session.date} - cumulative={session.cumulative} - average={session.average} - highest={session.highest} - /> - </div> - ))} + {AnalyticsSummaryPlayer?.criticalSessions + ?.sort((a, b) => b.cumulative - a.cumulative) + .map((session) => ( + <div + className={styles.criticalSessionContainer} + key={session.name} + > + <CriticalSession + name={session.name} + date={session.date} + cumulative={session.cumulative} + average={session.average} + highest={session.highest} + /> + </div> + ))} </div> </div> </> diff --git a/code/client/impax/src/components/Analytics/PlayerAnalytics/StackedBarChart.tsx b/code/client/impax/src/components/Analytics/PlayerAnalytics/StackedBarChart.tsx index 56c1735b..56df3414 100644 --- a/code/client/impax/src/components/Analytics/PlayerAnalytics/StackedBarChart.tsx +++ b/code/client/impax/src/components/Analytics/PlayerAnalytics/StackedBarChart.tsx @@ -4,10 +4,10 @@ import { ChartOptions } from "chart.js"; import { HistogramData } from "../../../types"; export const StackedBarChart: React.FC<HistogramData> = ({ - left, - right, - front, - back, + Left, + Right, + Front, + Back, }) => { const xVals = [10, 30, 50, 70, 90, 110, 130, 150, 170, 190]; @@ -24,10 +24,10 @@ export const StackedBarChart: React.FC<HistogramData> = ({ // Math.floor(Math.random() * 50) // ); - const dataLeft = xVals.map((k, i) => ({ x: k, y: left[i] })); - const dataRight = xVals.map((k, i) => ({ x: k, y: right[i] })); - const dataFront = xVals.map((k, i) => ({ x: k, y: front[i] })); - const dataBack = xVals.map((k, i) => ({ x: k, y: back[i] })); + const dataLeft = xVals.map((k, i) => ({ x: k, y: Left[i] })); + const dataRight = xVals.map((k, i) => ({ x: k, y: Right[i] })); + const dataFront = xVals.map((k, i) => ({ x: k, y: Front[i] })); + const dataBack = xVals.map((k, i) => ({ x: k, y: Back[i] })); const backgroundColorLeft = Array(xVals.length).fill( "rgba(255, 99, 180, 0.2)" diff --git a/code/client/impax/src/components/Analytics/TeamAnalytics/TeamAnalytics.tsx b/code/client/impax/src/components/Analytics/TeamAnalytics/TeamAnalytics.tsx index 218dad66..fff8dfbb 100644 --- a/code/client/impax/src/components/Analytics/TeamAnalytics/TeamAnalytics.tsx +++ b/code/client/impax/src/components/Analytics/TeamAnalytics/TeamAnalytics.tsx @@ -11,6 +11,7 @@ import { // TeamAnalyticsColumns, TimeSpan, TeamAnalyticsSummary, + Metric, } from "../../../types"; // import TeamAnalyticsTable from "./TeamAnalyticsTable"; import { useQuery } from "@tanstack/react-query"; @@ -51,7 +52,7 @@ const TeamAnalytics = () => { const responseData = await response.json(); return responseData; } - + console.log(AnalyticsSummaryManager); const isInternetAvailable = useAppState((state) => state.isInternetAvailable); if (!isInternetAvailable) { //show no internet connection component @@ -115,7 +116,7 @@ const TeamAnalytics = () => { ) : ( <> <div className={styles.impactSummaryContainer}> - {AnalyticsSummaryManager?.summaryData?.map((metric) => ( + {AnalyticsSummaryManager?.summaryData?.map((metric: Metric) => ( <ImpactSummaryCard metric={metric} timeSpan={timeSpan} @@ -124,13 +125,14 @@ const TeamAnalytics = () => { ))} </div> <div className={styles.tableContainer}> - {AnalyticsSummaryManager?.tableData ? ( + {AnalyticsSummaryManager?.tableData && + AnalyticsSummaryManager?.tableData.length > 0 ? ( <TeamAnalyticsTable teamAnalyticsTableData={AnalyticsSummaryManager?.tableData} key={Date.now()} /> ) : ( - <p>No Data</p> + <p>No Player Data Available</p> )} </div> </> diff --git a/code/client/impax/src/components/PlayerManagement/PlayerManagement.tsx b/code/client/impax/src/components/PlayerManagement/PlayerManagement.tsx index 56795b0e..3aaaeebd 100644 --- a/code/client/impax/src/components/PlayerManagement/PlayerManagement.tsx +++ b/code/client/impax/src/components/PlayerManagement/PlayerManagement.tsx @@ -173,6 +173,7 @@ const PlayerManagement = () => { return; } + console.log(data); const response = await fetch(`${BASE_URL}/player/add`, { method: "POST", body: JSON.stringify({ @@ -253,13 +254,13 @@ const PlayerManagement = () => { placeholder="Johnathan Doe" /> <label htmlFor="email"> - Player's Email (Optional) + Player's Email <span className={styles.additionalInfo}> Link Impax Account </span> </label> <input - {...register("email", { required: false })} + {...register("email", { required: true })} type="email" name="email" id="email" diff --git a/code/client/impax/src/components/Profile/JoinTeam.tsx b/code/client/impax/src/components/Profile/JoinTeam.tsx index 234d99f2..38d51987 100644 --- a/code/client/impax/src/components/Profile/JoinTeam.tsx +++ b/code/client/impax/src/components/Profile/JoinTeam.tsx @@ -31,8 +31,8 @@ const JoinTeam = () => { // console.log(data); console.log(request); // TODO: Change the URL to the backend - const response = await fetch(`${BASE_URL}/%%%%%`, { - method: "POST", + const response = await fetch(`${BASE_URL}/manager/join-team`, { + method: "PUT", body: JSON.stringify(request), headers: { "Content-Type": "application/json", diff --git a/code/client/impax/src/components/Profile/LoginManager.tsx b/code/client/impax/src/components/Profile/LoginManager.tsx index 716f0412..e128cc81 100644 --- a/code/client/impax/src/components/Profile/LoginManager.tsx +++ b/code/client/impax/src/components/Profile/LoginManager.tsx @@ -32,7 +32,7 @@ const LoginManager = () => { }, }); const responseData = await response.json(); - return responseData.teamName; + return responseData.team_name; } catch (error) { console.log(error); } @@ -60,7 +60,7 @@ const LoginManager = () => { }); const teamName = await getTeamInfo(teamId, responseData.accessToken); - setLoginInfo({ teamId, teamName: teamName, email }); + setLoginInfo({ teamId, teamName, email }); // FETCH PLAYERS array and store it in local storage diff --git a/code/client/impax/src/components/Profile/ManagerProfile/ManagerActions.tsx b/code/client/impax/src/components/Profile/ManagerProfile/ManagerActions.tsx index 45bb3c81..0a9534b9 100644 --- a/code/client/impax/src/components/Profile/ManagerProfile/ManagerActions.tsx +++ b/code/client/impax/src/components/Profile/ManagerProfile/ManagerActions.tsx @@ -4,12 +4,40 @@ import styles from "./ManagerActions.module.scss"; import Btn from "../../Buttons/Btn"; import AlertModal from "../../Modal/AlertModal"; +import { BASE_URL } from "../../../config/config"; +import { renewAccessToken } from "../../../services/authService"; -const ManagerActions: React.FC<{ name: string; email: string }> = ({ +const ManagerActions: React.FC<{ + name: string; + email: string; + handleAction: () => void; +}> = ({ // name, email, + handleAction, }) => { // const [openEdit, setOpenEdit] = useState<boolean>(false); + const removeManager = async () => { + // react Delete request + renewAccessToken(); + const response = await fetch(`${BASE_URL}/manager/remove`, { + method: "DELETE", + body: JSON.stringify({ + email: email, + }), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, + }, + }); + const responseData = await response.json(); + if (response.ok) { + // for debugging + handleAction(); + console.log("response OK", responseData); + } + }; + return ( <div className={styles.actions}> {/* <DialogModal @@ -90,6 +118,7 @@ const ManagerActions: React.FC<{ name: string; email: string }> = ({ buttonStyle="secondary" Icon={FaTrash} iconSizeEm={1} + onClick={() => removeManager()} > Remove </Btn> diff --git a/code/client/impax/src/components/Profile/ManagerProfile/ManagerProfile.module.scss b/code/client/impax/src/components/Profile/ManagerProfile/ManagerProfile.module.scss index 795966c0..3bbf77e1 100644 --- a/code/client/impax/src/components/Profile/ManagerProfile/ManagerProfile.module.scss +++ b/code/client/impax/src/components/Profile/ManagerProfile/ManagerProfile.module.scss @@ -31,3 +31,11 @@ .addManagerForm { @include modalInnerForm; } + +.spinnerContainer { + width: 100%; + height: 40dvh; + display: flex; + justify-content: center; + align-items: center; +} diff --git a/code/client/impax/src/components/Profile/ManagerProfile/ManagerProfile.tsx b/code/client/impax/src/components/Profile/ManagerProfile/ManagerProfile.tsx index 0fd5912c..11f3e217 100644 --- a/code/client/impax/src/components/Profile/ManagerProfile/ManagerProfile.tsx +++ b/code/client/impax/src/components/Profile/ManagerProfile/ManagerProfile.tsx @@ -12,6 +12,13 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; import NoInternetConnection from "../../StatusScreens/NoInternetConnection"; import { useAppState } from "../../../states/appState"; +import { useQuery } from "@tanstack/react-query"; +import { renewAccessToken } from "../../../services/authService"; +import { BASE_URL } from "../../../config/config"; +import Spinner from "../../StatusScreens/Spinner"; +import { Manager } from "../../../types"; +import { FieldValues, useForm } from "react-hook-form"; +import { showErrorPopup } from "../../../utils/popup"; const ManagerProfile = () => { // Get team-id @@ -27,6 +34,63 @@ const ManagerProfile = () => { const [addManagerOpen, setAddManagerOpen] = useState<boolean>(false); const isInternetAvailable = useAppState((state) => state.isInternetAvailable); + + const { + data: managerProfileData, + isLoading, + refetch: refetchManagers, + } = useQuery({ + queryFn: () => fetchManagersTableData(), + queryKey: ["data"], + }); + async function fetchManagersTableData(): Promise<Manager[]> { + // Renew access Token + await renewAccessToken(); + const response = await fetch(`${BASE_URL}/manager/getTeamManagers`, { + // Use the constructed URL with query params + method: "GET", // Change the method to GET + headers: { + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, + "Content-Type": "application/json", // Keep the Content-Type header for consistency + }, + }); + const responseData = await response.json(); + console.log(responseData); + return responseData; + } + + const { + register, + handleSubmit, + formState: { isSubmitting }, + reset, + } = useForm(); + + const onSubmit = async (data: FieldValues) => { + renewAccessToken(); + const response = await fetch(`${BASE_URL}/manager/add`, { + method: "POST", + body: JSON.stringify({ + managerEmail: data.email, + }), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, + }, + }); + // const responseData = await response.json(); + setAddManagerOpen(false); + if (response.ok) { + // for debugging + console.log("response OK", response); + refetchManagers(); + } else { + await showErrorPopup("Error", "Please Try Again!"); + } + + reset(); + }; + if (!isInternetAvailable) { //show no internet connection component if (!isInternetAvailable) { @@ -84,10 +148,7 @@ const ManagerProfile = () => { > <form className={styles.addManagerForm} - onSubmit={(e) => { - e.preventDefault(); - setAddManagerOpen(false); - }} + onSubmit={handleSubmit(onSubmit)} > {/* <label htmlFor="manager_name">Manager Name</label> <input @@ -104,19 +165,30 @@ const ManagerProfile = () => { </span> */} </label> <input + {...register("email", { required: true })} type="email" name="email" id="email" placeholder="johndoe@gmail.com" /> - <Btn type="submit" Icon={FaPlus}> + <Btn disabled={isSubmitting} type="submit" Icon={FaPlus}> Invite Manager </Btn> </form> </DialogModal> </div> <div className={styles.managersTableContainer}> - <ManagersTable /> + {isLoading || managerProfileData === undefined ? ( + <div className={styles.spinnerContainer}> + <Spinner /> + </div> + ) : ( + <ManagersTable + managerProfileData={managerProfileData} + handleAction={refetchManagers} + key={managerProfileData.length} + /> + )} </div> </div> </div> diff --git a/code/client/impax/src/components/Profile/ManagerProfile/ManagersTable.tsx b/code/client/impax/src/components/Profile/ManagerProfile/ManagersTable.tsx index 5fc36b90..e6c2876f 100644 --- a/code/client/impax/src/components/Profile/ManagerProfile/ManagersTable.tsx +++ b/code/client/impax/src/components/Profile/ManagerProfile/ManagersTable.tsx @@ -15,56 +15,61 @@ import { FaSort } from "react-icons/fa"; import { Manager } from "../../../types"; import { Verification } from "../../PlayerManagement/PlayersTable/Verification/Verification"; import ManagerActions from "./ManagerActions"; -import { managers } from "./managersData"; -const columns: ColumnDef<Manager>[] = [ - { - //TODO:Until verified manager will not have a name - accessorKey: "name", - size: 100, - header: ({ column }) => { - return ( - <button - onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} - > - Name <FaSort className={styles.icon} /> - </button> - ); +const ManagersTable: React.FC<{ + managerProfileData: Manager[]; + handleAction: () => void; +}> = ({ managerProfileData, handleAction }) => { + const columns: ColumnDef<Manager>[] = [ + { + //TODO:Until verified manager will not have a name + accessorKey: "name", + size: 100, + header: ({ column }) => { + return ( + <button + onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} + > + Name <FaSort className={styles.icon} /> + </button> + ); + }, + cell: ({ row }) => { + return ( + row.getValue("name") ?? <span className={styles.noName}>---</span> + ); + }, }, - cell: ({ row }) => { - return row.getValue("name") ?? <span className={styles.noName}>---</span>; - }, - }, - { - accessorKey: "email", - header: "Email", - id: "email", - size: 100, - }, + { + accessorKey: "email", + header: "Email", + id: "email", + size: 100, + }, - { - accessorKey: "verification", - header: "Verification", - id: "verification", - size: 10, - cell: ({ row }) => <Verification status={row.getValue("verification")} />, - }, - { - accessorKey: "edit", - header: "", - id: "edit", - size: 3, - cell: ({ row }) => ( - <ManagerActions - name={row.getValue("name")} - email={row.getValue("email")} - /> - ), - }, -]; -const ManagersTable = () => { - const [data] = React.useState(() => [...managers]); + { + accessorKey: "verification", + header: "Verification", + id: "verification", + size: 10, + cell: ({ row }) => <Verification status={row.getValue("verification")} />, + }, + { + accessorKey: "edit", + header: "", + id: "edit", + size: 3, + cell: ({ row }) => ( + <ManagerActions + name={row.getValue("name")} + email={row.getValue("email")} + handleAction={handleAction} + /> + ), + }, + ]; + const [data] = React.useState(() => [...managerProfileData]); const [sorting, setSorting] = React.useState<SortingState>([]); const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( [] diff --git a/code/client/impax/src/components/Profile/PlayerProfile/MyTeamsTable.tsx b/code/client/impax/src/components/Profile/PlayerProfile/MyTeamsTable.tsx index 4787c46e..10ad7d72 100644 --- a/code/client/impax/src/components/Profile/PlayerProfile/MyTeamsTable.tsx +++ b/code/client/impax/src/components/Profile/PlayerProfile/MyTeamsTable.tsx @@ -13,47 +13,52 @@ import { } from "@tanstack/react-table"; import { MyTeam } from "../../../types"; import { Verification } from "../../PlayerManagement/PlayersTable/Verification/Verification"; -import { myTeams } from "./myTeams"; import TeamActions from "./TeamsActions"; -const columns: ColumnDef<MyTeam>[] = [ - { - accessorKey: "team_id", - size: 60, - id: "team_id", - header: "Team ID", - }, +const MyTeamsTable: React.FC<{ + playerProfileTable: MyTeam[]; + handleActions: () => void; +}> = ({ playerProfileTable, handleActions }) => { + const columns: ColumnDef<MyTeam>[] = [ + { + accessorKey: "team_id", + size: 60, + id: "team_id", + header: "Team ID", + }, - { - accessorKey: "team_name", - header: "Team Name", - id: "team_name", - size: 80, - }, + { + accessorKey: "team_name", + header: "Team Name", + id: "team_name", + size: 80, + }, - { - accessorKey: "verification", - header: "Verification", - id: "verification", - size: 10, - cell: ({ row }) => <Verification status={row.getValue("verification")} />, - }, - { - accessorKey: "edit", - header: "", - id: "edit", - size: 20, - cell: ({ row }) => ( - <TeamActions - team_id={row.getValue("team_id")} - team_name={row.getValue("team_name")} - verification={row.getValue("verification")} - /> - ), - }, -]; -const MyTeamsTable = () => { - const [data] = React.useState(() => [...myTeams]); + { + accessorKey: "verification", + header: "Verification", + id: "verification", + size: 10, + cell: ({ row }) => <Verification status={row.getValue("verification")} />, + }, + { + accessorKey: "edit", + header: "", + id: "edit", + size: 20, + cell: ({ row }) => ( + <TeamActions + myTeam={{ + team_id: row.getValue("team_id"), + team_name: row.getValue("team_name"), + verification: row.getValue("verification"), + }} + handleActions={handleActions} + /> + ), + }, + ]; + const [data] = React.useState(() => [...playerProfileTable]); const [sorting, setSorting] = React.useState<SortingState>([]); const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( [] @@ -71,6 +76,7 @@ const MyTeamsTable = () => { columnFilters, }, }); + return ( <table className={styles.managersTable}> <thead> diff --git a/code/client/impax/src/components/Profile/PlayerProfile/PlayerProfile.module.scss b/code/client/impax/src/components/Profile/PlayerProfile/PlayerProfile.module.scss index 6670e2e6..fe7c691a 100644 --- a/code/client/impax/src/components/Profile/PlayerProfile/PlayerProfile.module.scss +++ b/code/client/impax/src/components/Profile/PlayerProfile/PlayerProfile.module.scss @@ -36,3 +36,11 @@ margin-left: 1em; } } + +.spinnerContainer { + width: 100%; + height: 60dvh; + display: flex; + justify-content: center; + align-items: center; +} diff --git a/code/client/impax/src/components/Profile/PlayerProfile/PlayerProfile.tsx b/code/client/impax/src/components/Profile/PlayerProfile/PlayerProfile.tsx index f8fe7f65..c69ba41b 100644 --- a/code/client/impax/src/components/Profile/PlayerProfile/PlayerProfile.tsx +++ b/code/client/impax/src/components/Profile/PlayerProfile/PlayerProfile.tsx @@ -9,6 +9,11 @@ import MyTeamsTable from "./MyTeamsTable"; import { useNavigate } from "react-router-dom"; import { useAppState } from "../../../states/appState"; import NoInternetConnection from "../../StatusScreens/NoInternetConnection"; +import { useQuery } from "@tanstack/react-query"; +import { renewAccessToken } from "../../../services/authService"; +import { BASE_URL } from "../../../config/config"; +import { MyTeam } from "../../../types"; +import Spinner from "../../StatusScreens/Spinner"; const PlayerProfile = () => { // Get team-id @@ -21,6 +26,29 @@ const PlayerProfile = () => { const setLoginInfo = useLoginState((state) => state.setLoginInfo); const navigate = useNavigate(); + const { + data: myTeamsData, + isLoading, + refetch: refetchPlayers, + } = useQuery({ + queryFn: () => fetchplayerProfileTableData(), + queryKey: ["data"], + }); + async function fetchplayerProfileTableData(): Promise<MyTeam[]> { + console.log("Fetching Profile Table..."); + // Renew access Token + await renewAccessToken(); + const response = await fetch(`${BASE_URL}/player/myTeams`, { + // Use the constructed URL with query params + method: "GET", // Change the method to GET + headers: { + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, + "Content-Type": "application/json", // Keep the Content-Type header for consistency + }, + }); + const responseData = await response.json(); + return responseData.teams; + } const isInternetAvailable = useAppState((state) => state.isInternetAvailable); if (!isInternetAvailable) { //show no internet connection component @@ -65,7 +93,17 @@ const PlayerProfile = () => { <div className={styles.myTeamsContainer}> <h2>My Teams</h2> <div className={styles.tableContainer}> - <MyTeamsTable /> + {isLoading || myTeamsData === undefined ? ( + <div className={styles.spinnerContainer}> + <Spinner /> + </div> + ) : ( + <MyTeamsTable + playerProfileTable={myTeamsData} + handleActions={refetchPlayers} + key={Date.now()} + /> + )} </div> </div> </div> diff --git a/code/client/impax/src/components/Profile/PlayerProfile/TeamsActions.tsx b/code/client/impax/src/components/Profile/PlayerProfile/TeamsActions.tsx index 22de9c28..6c297fb2 100644 --- a/code/client/impax/src/components/Profile/PlayerProfile/TeamsActions.tsx +++ b/code/client/impax/src/components/Profile/PlayerProfile/TeamsActions.tsx @@ -5,12 +5,63 @@ import styles from "./TeamsActions.module.scss"; import AlertModal from "../../Modal/AlertModal"; import { FaCheck } from "react-icons/fa6"; import { MyTeam } from "../../../types"; +import { renewAccessToken } from "../../../services/authService"; +import { BASE_URL } from "../../../config/config"; -const TeamActions: React.FC<MyTeam> = ({ - // team_id, - team_name, - verification, +const TeamActions: React.FC<{ myTeam: MyTeam; handleActions: () => void }> = ({ + myTeam, + handleActions, }) => { + const { team_id, team_name, verification } = myTeam; + const denyTeam = async () => { + // renew access Token + console.log("I am here"); + renewAccessToken(); + + const token = localStorage.getItem("accessToken"); + + const response = await fetch( + `${BASE_URL}/player/accept-invite/${team_id}/${0}`, + { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ); + const responseData = await response.json(); + if (response.ok) { + // for debugging + handleActions(); + console.log("response OK", responseData); + } + console.log("Hello", response); + return response; + }; + + const acceptTeam = async () => { + console.log("Sending request to accept team"); + // renew access Token + renewAccessToken(); + + const token = localStorage.getItem("accessToken"); + + const response = await fetch( + `${BASE_URL}/player/accept-invite/${team_id}/${1}`, + { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ); + // const responseData = await response.json(); + handleActions(); + console.log("Hello", response); + }; + return ( <div className={styles.actions}> <AlertModal @@ -22,6 +73,7 @@ const TeamActions: React.FC<MyTeam> = ({ Icon={FaCheck} iconSizeEm={1.2} disabled={verification === "verified"} + onClick={() => acceptTeam()} > Accept </Btn> @@ -51,6 +103,7 @@ const TeamActions: React.FC<MyTeam> = ({ bgColor="rgba(255,255,255,0)" Icon={FaTrash} iconSizeEm={1.2} + disabled={verification === "verified"} > Deny </Btn> @@ -74,6 +127,7 @@ const TeamActions: React.FC<MyTeam> = ({ buttonStyle="secondary" Icon={FaTrash} iconSizeEm={1} + onClick={() => denyTeam()} > Deny </Btn> diff --git a/code/client/impax/src/components/Profile/SignupManager.tsx b/code/client/impax/src/components/Profile/SignupManager.tsx index 7480331e..f64692e3 100644 --- a/code/client/impax/src/components/Profile/SignupManager.tsx +++ b/code/client/impax/src/components/Profile/SignupManager.tsx @@ -14,7 +14,6 @@ const SignupManager = () => { handleSubmit, formState: { errors, isSubmitting }, reset, - setError, } = useForm(); const onSubmit = async (data: FieldValues) => { const { teamId, email } = data; diff --git a/code/client/impax/src/components/StatusScreens/TeamExists.tsx b/code/client/impax/src/components/StatusScreens/TeamExists.tsx index 697e45e7..d298555c 100644 --- a/code/client/impax/src/components/StatusScreens/TeamExists.tsx +++ b/code/client/impax/src/components/StatusScreens/TeamExists.tsx @@ -4,6 +4,8 @@ import styles from "./status.module.scss"; import Hero from "../Profile/Hero"; import { useSignupState } from "../../states/formState"; import { useNavigate } from "react-router-dom"; +import Btn from "../Buttons/Btn"; +import { FaRegCircleUser } from "react-icons/fa6"; const teamExists = () => { const navigate = useNavigate(); @@ -15,16 +17,16 @@ const teamExists = () => { <FaCheckCircle className={styles.icon} /> You already have an account! </h2> - <button + <Btn + Icon={FaRegCircleUser} onClick={() => { setIsSignup(false); - navigate("/profile"); + navigate("login/manager"); }} type="submit" - className={styles.nextBtn} > Login - </button> + </Btn> </div> <Hero /> </main> diff --git a/code/client/impax/src/services/httpClient.ts b/code/client/impax/src/services/httpClient.ts index 097e842d..e0770dca 100644 --- a/code/client/impax/src/services/httpClient.ts +++ b/code/client/impax/src/services/httpClient.ts @@ -70,9 +70,12 @@ export const getPlayers = async () => { Authorization: `Bearer ${token}`, }, }); - const playersData: Players = await playersResponse.json(); - console.log("players data fetched: ", playersData); - updatePlayersDetails(playersData); + if (!playersResponse.ok) return; + else { + const playersData: Players = await playersResponse.json(); + console.log("players data fetched: ", playersData); + updatePlayersDetails(playersData); + } } catch (error) { console.log(error); } diff --git a/code/client/impax/src/services/mqttClient.ts b/code/client/impax/src/services/mqttClient.ts index a7a31aa9..d1d2b00c 100644 --- a/code/client/impax/src/services/mqttClient.ts +++ b/code/client/impax/src/services/mqttClient.ts @@ -6,7 +6,7 @@ import { setPlayerMap, setSessionDetails, updatePlayersImpactHistory, - checkBuddiesAvailability, + // checkBuddiesAvailability, flushStates, validateTimestampAndSetPlayerDetails, } from "../states/updateAppStates"; @@ -19,7 +19,7 @@ class MqttClient { private topics: string[]; private constructor() { - this.client = mqtt.connect("ws://127.0.0.1:8080/", { + this.client = mqtt.connect("ws://192.168.4.1:8080/", { clientId: `impax-dashboard-${Date.now()}`, reconnectPeriod: 2000, keepalive: 60, @@ -71,6 +71,9 @@ class MqttClient { private handleMessage = (topic: string, message: Buffer) => { console.log(`Received message on topic ${topic}: ${message}`); + + // Zero Payload Ignore + if (message.toString().length === 0) return; switch (true) { case /^player_map$/.test(topic): setPlayerMap(message.toString()); diff --git a/code/client/impax/src/states/appState.ts b/code/client/impax/src/states/appState.ts index 98f3a415..18cfccb5 100644 --- a/code/client/impax/src/states/appState.ts +++ b/code/client/impax/src/states/appState.ts @@ -105,7 +105,8 @@ export const useAppState = create<AppState>()((set) => ({ playersImpactHistory: {} as PlayerImpactHistory, //TODO: Clashing of players with other dashbaords - playerDetails: localStorage.getItem("players") as Players | {} as Players, + playerDetails: JSON.parse(localStorage.getItem("players") || '{"player": {}}') + .players as Players, setPlayerDetails: (players: Players) => { set({ playerDetails: players }); const timestamp = new Date().getTime(); diff --git a/code/client/impax/src/states/formState.ts b/code/client/impax/src/states/formState.ts index 407c26d2..d4d22c5e 100644 --- a/code/client/impax/src/states/formState.ts +++ b/code/client/impax/src/states/formState.ts @@ -52,7 +52,7 @@ export const useSignupState = create<SignupState>()((set) => ({ }, isLoggedInManager: localStorage.getItem("isLoggedInManager") === "true", - // isLoggedInManager: false, + // isLoggedInManager: true, setIsLoggedInManager: (isLoggedInManager) => { set({ isLoggedInManager: isLoggedInManager }), diff --git a/code/client/impax/src/states/updateAppStates.ts b/code/client/impax/src/states/updateAppStates.ts index 4b27d7f2..8aa2e17d 100644 --- a/code/client/impax/src/states/updateAppStates.ts +++ b/code/client/impax/src/states/updateAppStates.ts @@ -26,14 +26,18 @@ export const updateBuddy = (buddy_id: number, battery: number) => { //updateSet useAppState.setState((prevState) => { const buddiesStatus = { ...prevState.buddiesStatus }; + const playerMap = { ...prevState.playerMap }; if (battery === 0) { delete buddiesStatus[buddy_id]; + console.log("Deleted buddy", buddy_id); + delete playerMap[buddy_id]; + MqttClient.getInstance().publishPlayerMap(playerMap); } else { buddiesStatus[buddy_id] = buddyStatus; } - return { buddiesStatus }; + return { buddiesStatus, playerMap }; }); }; @@ -84,6 +88,7 @@ export const setSessionDetails = (sessionString: string) => { //Parse sessionString and set sessionDetails const session: Session = JSON.parse(sessionString); if (session.active === false) { + console.log("Flushing States..."); useAppState.setState({ playersImpact: {} as PlayersImpact }); useAppState.setState({ playersImpactHistory: {} as PlayerImpactHistory }); useAppState.setState({ monitoringBuddies: new Set() as Set<number> }); diff --git a/code/client/impax/src/types/index.d.ts b/code/client/impax/src/types/index.d.ts index 5f3ccad7..95e0ef50 100644 --- a/code/client/impax/src/types/index.d.ts +++ b/code/client/impax/src/types/index.d.ts @@ -16,7 +16,7 @@ export type Role = "player" | "manager"; export type Impact = { magnitude: number; - direction: "left" | "right" | "front" | "back"; + direction: "Left" | "Right" | "Front" | "Back"; timestamp: number; isConcussion?: boolean; }; @@ -74,7 +74,7 @@ export type SessionToBeUploaded = { export type Metric = { title: string; value: string | number; - trend?: number | Impact.direction; + trend?: number | Impact.direction | "--"; metaUnits?: string; }; @@ -82,10 +82,10 @@ export type TimeSpan = "Last Week" | "Last Month" | "All Time"; //for player analytics export type HistogramData = { - left: number[]; - right: number[]; - front: number[]; - back: number[]; + Left: number[]; + Right: number[]; + Front: number[]; + Back: number[]; }; //for player critical sessions @@ -109,10 +109,10 @@ export type TeamAnalyticsColumns = { concussions: number; }; -export type TeamAnalyticsSummary ={ +export type TeamAnalyticsSummary = { summaryData: Metric[]; tableData: TeamAnalyticsColumns[]; -} +}; export type PlayerAnalyticsSummary = { summaryData: Metric[]; @@ -120,8 +120,6 @@ export type PlayerAnalyticsSummary = { criticalSessions: CriticalSessionType[]; }; - - //Profile Managers export type Manager = | { @@ -151,5 +149,4 @@ export type LoginInfo = { teamId?: string; teamName?: string; email: string; - -} +}; diff --git a/code/client/impax/src/utils/utils.ts b/code/client/impax/src/utils/utils.ts index d8ae8c54..b120ac9f 100644 --- a/code/client/impax/src/utils/utils.ts +++ b/code/client/impax/src/utils/utils.ts @@ -12,6 +12,7 @@ export function generateStringId(input: string): string { const words = input.split(" "); const id = words.map((word) => word[0]).join(""); const currentDate = new Date().toISOString().split("T")[0]; - const stringId = `${id}-${currentDate}`; + const timestamp = Date.now().toString().split("").slice(0, 5).join(""); + const stringId = `${id}-${currentDate}-${timestamp}`; return stringId; } diff --git a/code/client/impax/tests/dashboard_load_test/mqtt_load.py b/code/client/impax/tests/dashboard_load_test/mqtt_load.py index 2beea598..acfb2982 100644 --- a/code/client/impax/tests/dashboard_load_test/mqtt_load.py +++ b/code/client/impax/tests/dashboard_load_test/mqtt_load.py @@ -3,11 +3,11 @@ import random # MQTT broker settings -broker_address = "localhost" +broker_address = "192.168.4.1" broker_port = 1883 # Number of buddy devices -num_buddies = 5 +num_buddies = 30 # Impact frequency (chance for a buddy to publish an impact each cycle) impact_frequency = 10 @@ -58,7 +58,7 @@ def on_connect(client, userdata, flags, rc): if random.random() < impact_frequency: magnitude = random.randint(14, 100) direction = random.choice( - ["left", "right", "front", "back", "top", "bottom"]) + ["left", "right", "front", "back", "top", "bottom"]).capitalize() topic = f"buddy/{client_id_str.split('_')[1]}/impact" client.publish( topic, f"{magnitude} {direction}", retain=True) diff --git a/code/hub-firmware/hub-client/old_client.py b/code/hub-firmware/hub-client/old_client.py index e5334bb1..3ce19a09 100644 --- a/code/hub-firmware/hub-client/old_client.py +++ b/code/hub-firmware/hub-client/old_client.py @@ -128,6 +128,8 @@ def on_message(client, userdata, msg): impact["isConcussion"] = True break + client.publish(f'player/{player_id}/impact_history', + json.dumps(data_buffer[int(player_id)]), retain=True) except Exception as e: print(f"Error in handling concussion data: {str(e)}") @@ -158,6 +160,13 @@ def end_session(): global session_started, start_time, data_buffer, player_device_mapping session_started = False + # zero payload messages to clear retained messages + for player_id in data_buffer: + impact_topic = f'player/{player_id}/impact_with_timestamp' + impact_history_topic = f'player/{player_id}/impact_history' + client.publish(impact_topic, "", retain=True) + client.publish(impact_history_topic, "", retain=True) + # for entry in data_buffer: # client.publish(session_data, json.dumps(entry), retain=True) diff --git a/docs/assets/css/style.css b/docs/assets/css/style.css index 5a77bfa3..73e1c0d2 100644 --- a/docs/assets/css/style.css +++ b/docs/assets/css/style.css @@ -1455,4 +1455,4 @@ section { #footer .footer-top .footer-info { margin: -20px 0 30px 0; } -} \ No newline at end of file +} diff --git a/docs/assets/img/3D-buddy-img.jpg b/docs/assets/img/3D-buddy-img.jpg new file mode 100644 index 00000000..907edf4a Binary files /dev/null and b/docs/assets/img/3D-buddy-img.jpg differ diff --git a/docs/assets/img/3D-hub-img.jpg b/docs/assets/img/3D-hub-img.jpg new file mode 100644 index 00000000..3d73c76e Binary files /dev/null and b/docs/assets/img/3D-hub-img.jpg differ diff --git a/docs/assets/img/3d-buddy.mp4 b/docs/assets/img/3d-buddy.mp4 new file mode 100644 index 00000000..7c0c696b Binary files /dev/null and b/docs/assets/img/3d-buddy.mp4 differ diff --git a/docs/assets/img/3d-hub.mp4 b/docs/assets/img/3d-hub.mp4 new file mode 100644 index 00000000..ef4f89ce Binary files /dev/null and b/docs/assets/img/3d-hub.mp4 differ diff --git a/docs/assets/img/Coach-feedback.jpg b/docs/assets/img/Coach-feedback.jpg new file mode 100644 index 00000000..e4a87247 Binary files /dev/null and b/docs/assets/img/Coach-feedback.jpg differ diff --git a/docs/assets/img/Demo.jpg b/docs/assets/img/Demo.jpg new file mode 100644 index 00000000..2f2fb534 Binary files /dev/null and b/docs/assets/img/Demo.jpg differ diff --git a/docs/assets/img/PXL_20240126_094654363.mp4 b/docs/assets/img/PXL_20240126_094654363.mp4 new file mode 100644 index 00000000..bf784324 Binary files /dev/null and b/docs/assets/img/PXL_20240126_094654363.mp4 differ diff --git a/docs/assets/img/User-feedback.jpg b/docs/assets/img/User-feedback.jpg new file mode 100644 index 00000000..cf81bb06 Binary files /dev/null and b/docs/assets/img/User-feedback.jpg differ diff --git a/docs/assets/img/buck-booster.webp b/docs/assets/img/buck-booster.webp new file mode 100644 index 00000000..c5399608 Binary files /dev/null and b/docs/assets/img/buck-booster.webp differ diff --git a/docs/assets/img/buddy v1-1.avi b/docs/assets/img/buddy v1-1.avi new file mode 100644 index 00000000..2f3b563a Binary files /dev/null and b/docs/assets/img/buddy v1-1.avi differ diff --git a/docs/assets/img/buddy-labled.jpg b/docs/assets/img/buddy-labled.jpg new file mode 100644 index 00000000..6296f83b Binary files /dev/null and b/docs/assets/img/buddy-labled.jpg differ diff --git a/docs/assets/img/dashboard-screenshot.png b/docs/assets/img/dashboard-screenshot.png new file mode 100644 index 00000000..ab75b250 Binary files /dev/null and b/docs/assets/img/dashboard-screenshot.png differ diff --git a/docs/assets/img/hub-labled.jpg b/docs/assets/img/hub-labled.jpg new file mode 100644 index 00000000..f33fd28a Binary files /dev/null and b/docs/assets/img/hub-labled.jpg differ diff --git a/docs/download.css b/docs/download.css new file mode 100644 index 00000000..5c47b598 --- /dev/null +++ b/docs/download.css @@ -0,0 +1,117 @@ +body { + font-family: "Raleway", sans-serif; + margin: 0; + padding: 0; +} + +.container { + width: 100%; + margin: 0 auto; + padding: 120px; + padding-top: 10em; +} + +.container .mockup-container { + overflow: hidden; + height: auto; + display: flex; + justify-content: center; + align-items: center; +} + +.container .mockup-container .mockup-img { + height: 60dvh; + margin-top: -2em; + object-fit: contain; +} +.logo { + display: block; + margin: 0 auto; + width: 200px; +} + +.title { + text-align: center; + font-size: 24px; + font-weight: bold; + margin-top: 20px; +} + +.subtitle { + text-align: center; + font-size: 18px; + margin-bottom: 20px; +} + +.requirements { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2em; + margin-bottom: 20px; +} + +.requirement { + padding: 16px; + border: 1px solid #eee; + background-color: #fbfbfb; + border-radius: 10px; + display: flex; + flex-direction: column; + justify-content: space-between; + /* box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1); */ +} + +.requirement h3 { + text-align: left; + font-size: 20px; + font-weight: 600; + margin-top: 0; + display: flex; + align-items: center; + gap: 10px; +} + +.requirement h3 img { + width: 20px; +} + +.requirement ul { + list-style: none; + padding: 0; + margin: 0; +} + +.requirement li { + margin: 10px 0; + font-weight: 500; + font-size: 1em; +} + +.requirement li span { + display: block; + margin-top: 0.2em; + font-weight: 400; + font-size: 0.8em; +} + +.download-link { + width: 100%; +} +.button { + width: 100%; + margin-top: 2em; + border: none; + border-radius: 8px; + padding: 1em 1em; + background-color: #2a2a2a; + color: white; + font-size: 16px; + font-weight: 400; + font-family: "Raleway", sans-serif; + cursor: pointer; + transition: all 150ms ease-in-out; +} + +.button:hover { + background-color: #882626; +} diff --git a/docs/download.html b/docs/download.html new file mode 100644 index 00000000..34dd8b51 --- /dev/null +++ b/docs/download.html @@ -0,0 +1,178 @@ + +<!DOCTYPE html> +<html> + <head> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet"> + <link rel="stylesheet" href="styles.css" /> + <link rel="stylesheet" href="download.css" /> + <script src="./func.js"></script> + </head> + <body> + <!-- ============================================ --> + <!-- Navigation --> + <!-- ============================================ --> + + <header id="cs-navigation"> + <div class="cs-container"> + <!--Nav Logo--> + <a href="./index.html" class="cs-logo" aria-label="back to home"> + <img + src="assets/img/Logo-Impax.webp" + alt="logo" + width="210" + height="29" + aria-hidden="true" + decoding="async" + /> + </a> + <!--Navigation List--> + <nav class="cs-nav" role="navigation"> + <!--Mobile Nav Toggle--> + <button class="cs-toggle" aria-label="mobile menu toggle"> + <div class="cs-box" aria-hidden="true"> + <span class="cs-line cs-line1" aria-hidden="true"></span> + <span class="cs-line cs-line2" aria-hidden="true"></span> + <span class="cs-line cs-line3" aria-hidden="true"></span> + </div> + </button> + <!-- We need a wrapper div so we can set a fixed height on the cs-ul in case the nav list gets too long from too many dropdowns being opened and needs to have an overflow scroll. This wrapper acts as the background so it can go the full height of the screen and not cut off any overflowing nav items while the cs-ul stops short of the bottom of the screen, which keeps all nav items in view no matter how mnay there are--> + <div class="cs-ul-wrapper"> + <ul id="cs-expanded" class="cs-ul" aria-expanded="false"> + <li class="cs-li"> + <a href="index.html" class="cs-li-link cs-active"> Home </a> + </li> + <li class="cs-li"> + <a href="index.html#Problem" class="cs-li-link"> Problem </a> + </li> + <!--Copy and paste this cs-dropdown list item and replace any .cs-li with this cs-dropdown group to make a new dropdown and it will work--> + <li class="cs-li cs-dropdown" tabindex="0"> + <span class="cs-li-link"> + Solution + <img + class="cs-drop-icon" + src="https://csimg.nyc3.cdn.digitaloceanspaces.com/Icons/down-red.svg" + alt="dropdown icon" + width="15" + height="15" + decoding="async" + aria-hidden="true" + /> + </span> + <ul class="cs-drop-ul"> + <li class="cs-drop-li"> + <a href="index.html#Solution" class="cs-li-link cs-drop-link" + >Our Solution</a + > + </li> + <li class="cs-drop-li"> + <a + href="index.html#SolutionArchitecture" + class="cs-li-link cs-drop-link" + >Solution Architecture</a + > + </li> + </ul> + </li> + <li class="cs-li cs-dropdown" tabindex="0"> + <span class="cs-li-link"> + Hardware + <img + class="cs-drop-icon" + src="https://csimg.nyc3.cdn.digitaloceanspaces.com/Icons/down-red.svg" + alt="dropdown icon" + width="15" + height="15" + decoding="async" + aria-hidden="true" + /> + </span> + <ul class="cs-drop-ul"> + <li class="cs-drop-li"> + <a href="index.html#Hardware" class="cs-li-link cs-drop-link" + >Infrastructure</a + > + </li> + <li class="cs-drop-li"> + <a + href="index.html#video-gallery" + class="cs-li-link cs-drop-link" + >3D Designs</a + > + </li> + </ul> + </li> + <li class="cs-li"> + <a href="" class="cs-li-link"> Documentation </a> + </li> + + <li class="cs-li"> + <a href="index.html#meet-us-1021" class="cs-li-link"> About Us </a> + </li> + </ul> + </div> + </nav> + <a href="download.html" class="cs-button-solid cs-nav-button" + >Download Now</a + > + <!--Dark Mode toggle, uncomment button code if you want to enable a dark mode toggle--> + <!--Dark Mode toggle--> + <!-- Place at the bottom of your container that wraps around your <nav> navigation list --> + <!-- If there is no container wrapping your <nav>, add one. This should NOT be placed inside the navigation list --> + <!-- <button id="dark-mode-toggle"> + Moon is an inline SVG so you can edit the color if needed + <svg class="cs-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 480" style="enable-background:new 0 0 480 480" xml:space="preserve"><path d="M459.782 347.328c-4.288-5.28-11.488-7.232-17.824-4.96-17.76 6.368-37.024 9.632-57.312 9.632-97.056 0-176-78.976-176-176 0-58.4 28.832-112.768 77.12-145.472 5.472-3.712 8.096-10.4 6.624-16.832S285.638 2.4 279.078 1.44C271.59.352 264.134 0 256.646 0c-132.352 0-240 107.648-240 240s107.648 240 240 240c84 0 160.416-42.688 204.352-114.176 3.552-5.792 3.04-13.184-1.216-18.496z"/></svg> + <img class="cs-sun" aria-hidden="true" src="https://csimg.nyc3.digitaloceanspaces.com/Contact-Page/sun.svg" decoding="async" alt="sun" width="15" height="15"> + </button> --> + </div> + </header> + + <div class="container"> + <div class="mockup-container"> + <img class="mockup-img" src="./images/Mockup.png" alt=""> + </div> + + <div class="requirements"> + <div class="requirement"> + <ul> + <h3><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/5f/Windows_logo_-_2012.svg/2048px-Windows_logo_-_2012.svg.png" alt="">Windows</h3> + <li>Supported Operating Systems: <span>Windows 7 and later (x86 and amd64)</span></li> + <li>Minimum RAM: <span>512 MB</span></li> + <li>Processor: <span>Pentium 4 or later with SSE2 support</span></li> + </ul> + <a class="download-link" href="https://drive.google.com/file/d/1QrtFUyMJjdxp3R9_-hngfu2TinYQd0iN/view?usp=drive_link"><button class="button" >Download for Windows</button></a> + </div> + <div class="requirement"> + <ul> + <h3><img src="https://upload.wikimedia.org/wikipedia/commons/1/1b/Apple_logo_grey.svg" alt="">macOS</h3> + <li>Supported Operating Systems: <span>macOS 10.10 and later (64-bit)</span></li> + <li>Minimum RAM: <span>512 MB</span></li> + <li>Processor: <span>x64, Apple Silicon</span></li> + </ul> + <a class="download-link" href="https://drive.google.com/file/d/1duFbu2PqN75zXSo-03Cd9Wg-ixk4wtNx/view?usp=drive_link"><button class="button" onclick="download('macos')">Download for macOS</button></a> + </div> + <div class="requirement"> + <ul> + <h3><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/ab/Linux_Logo_in_Linux_Libertine_Font.svg/1200px-Linux_Logo_in_Linux_Libertine_Font.svg.png" alt="">Linux</h3> + <li>Supported Distributions: <span>Ubuntu 12.04 and later, Fedora 21, Debian 8 (ia32/i686 and x64/amd64)</span></li> + <li>Minimum RAM: <span>512 MB</span></li> + <li>Processor: <span>Intel Pentium 4 or later with SSE2 support</span></li> + </ul> + <button class="button" onclick="download('linux')">Download for Linux</button> + </div> + </div> + + </div> + <script> + // Add your custom JavaScript here + function download(os) { + // Replace the alert with your download logic + alert('Coming soon on ' + os); + } + </script> + </body> + </body> +</html> + +​ diff --git a/docs/func.js b/docs/func.js index 8638f725..f9ec2e82 100644 --- a/docs/func.js +++ b/docs/func.js @@ -73,4 +73,15 @@ document.getElementById('dark-mode-toggle').addEventListener('click', () => { localStorage.getItem('theme') === 'light' ? enableDarkMode() : disableDarkMode(); }); - \ No newline at end of file +function openModal(imgSrc) { + var modal = document.getElementById("myModal"); + var modalImg = document.getElementById("modalImg"); + modal.style.display = "block"; + modalImg.src = imgSrc; +} + +function closeModal() { + var modal = document.getElementById("myModal"); + modal.style.display = "none"; +} + \ No newline at end of file diff --git a/docs/images/Mockup.png b/docs/images/Mockup.png new file mode 100644 index 00000000..e76d320e Binary files /dev/null and b/docs/images/Mockup.png differ diff --git a/docs/index.html b/docs/index.html index 696283a2..b9dbfe0a 100644 --- a/docs/index.html +++ b/docs/index.html @@ -5,6 +5,10 @@ <html> <head> <link rel="stylesheet" href="styles.css"> + <link rel="stylesheet" href="video-gallery.css"> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet"> <script src="./func.js"> </script> </head> @@ -55,12 +59,24 @@ <li class="cs-drop-li"> <a href="#SolutionArchitecture" class="cs-li-link cs-drop-link">Solution Architecture</a> </li> + <li class="cs-drop-li"> + <a href="#end-points" class="cs-li-link cs-drop-link">End Points</a> + </li> </ul> </li> - <li class="cs-li"> - <a href="#Hardware" class="cs-li-link"> + <li class="cs-li cs-dropdown" tabindex="0"> + <span class="cs-li-link"> Hardware - </a> + <img class="cs-drop-icon" src="https://csimg.nyc3.cdn.digitaloceanspaces.com/Icons/down-red.svg" alt="dropdown icon" width="15" height="15" decoding="async" aria-hidden="true"> + </span> + <ul class="cs-drop-ul"> + <li class="cs-drop-li"> + <a href="#Hardware" class="cs-li-link cs-drop-link">Hardware Infrastructure</a> + </li> + <li class="cs-drop-li"> + <a href="#video-gallery" class="cs-li-link cs-drop-link">3D Designs</a> + </li> + </ul> </li> <li class="cs-li"> <a href="" class="cs-li-link"> @@ -74,7 +90,7 @@ </ul> </div> </nav> - <a href="" class="cs-button-solid cs-nav-button">Download Now</a> + <a href="download.html" class="cs-button-solid cs-nav-button">Download Now</a> <!--Dark Mode toggle, uncomment button code if you want to enable a dark mode toggle--> <!--Dark Mode toggle--> <!-- Place at the bottom of your container that wraps around your <nav> navigation list --> @@ -193,13 +209,13 @@ <h2 class="cs-title" style="text-align: center;">Problem Domain</h2> <picture class="cs-left-image"> <source media="(max-width: 600px)" srcset="assets/img/dashboard.jpg"> <source media="(min-width: 601px)" srcset="assets/img/dashboard.jpg"> - <img loading="lazy" decoding="async" src="assets/img/dashboard.jpg" alt="cleaning" width="211" height="211" aria-hidden="true"> + <img loading="lazy" decoding="async" src="assets/img/dashboard.jpg" alt="dashboard" width="211" height="211" aria-hidden="true"> </picture> <!--Bottom Circle Image--> <picture class="cs-bottom-image"> - <source media="(max-width: 600px)" srcset="assets/img/Hardware-hub.png"> - <source media="(min-width: 601px)" srcset="assets/img/Hardware-hub.png"> - <img loading="lazy" decoding="async" src="assets/img/Hardware-hub.png" alt="cleaning" width="180" height="180" aria-hidden="true"> + <source media="(max-width: 600px)" srcset="assets/img/3D-buddy-img.jpg"> + <source media="(min-width: 601px)" srcset="assets/img/3D-buddy-img.jpg"> + <img loading="lazy" decoding="async" src="assets/img/3D-buddy-img.jpg" alt="hub" width="180" height="180" aria-hidden="true"> </picture> <!--Main Person Image--> <!--To make your own crops of people, use https://www.remove.bg/ to remove the background of any image--> @@ -238,16 +254,209 @@ <h2 class="cs-title" style="text-align: center;">Our Solution</h2> <section id="SolutionArchitecture"> <div class="cs-container"> <h2 class="cs-title">High Level Solution Architecture</h2> + <div class="grid-picture"> <picture class="cs-picture"> <source media="(max-width: 767px)" srcset="assets/img/solution architecture.png"> <source media="(min-width: 768px)" srcset="assets/img/solution architecture.png"> <img aria-hidden="true" loading="lazy" decoding="async" src="assets/img/solution architecture.png" alt="Solution Architecture"> </picture> + <div class="picture-label"> + <p> + During sports sessions, wearable devices and dashboards establish a local area network connected to the hub, + utilizing MQTT for real-time impact data transmission. Managers monitor impacts in real-time on their dashboards + and can mark potential concussions based on their expertise. + </p> + <p> + Post-session, session details, including impact data + and concussion markers, are sent to the backend and stored in a secure database. Players access their analytics + dashboard to retrieve personalized impact histories, while managers utilize a separate analytics dashboard for + reviewing their players' impact histories. + </p> + <p> + This architecture ensures seamless information flow during sessions, + real-time monitoring, expert intervention, and comprehensive analytics for informed decision-making by both players + and managers. + </p> + </div> + </div> + </section> + + <!--===========================================--> + <!-- End Points --> + <!--===========================================--> + + <section id="end-points"> + + <!-- Force doctype to use empty attributes--> + <!-- variables--> + <!-- <div class="tools"> + <select id="tools"> + <option value="">Choose a modifier</option> + <option value="card-grid">Card Grid</option> + <option value="card-grid media-bottom">Card Grid media align bottom</option> + <option value="card-grid media-top">Card Grid media align top</option> + <option value="card-list">Card List</option> + <option value="card-list media-alternate">Card List medias position alternates</option> + <option value="l-masonry">Masonry</option> + </select> + <select id="theme"> + <option value="">Choose a theme</option> + <option value="">Un-Styled</option> + <option value="t-light-shadows">Light Shadows</option> + </select> + </div> --> + <div class="t-light-shadows" id="page"> + <h2 class="cs-title">System End Points</h2> + <div class="card-grid" id="main"> + <div class="card"> + <div class="card-media"> + <img src="assets/img/dashboard-screenshot.png"> + </div> + <div class="card-content fixie"> + <div class="title"> + <h2>Desktop Dashboard</h2> + </div> + <div class="description"> + <p> + <ul> + <li> + User-friendly cross-platform desktop application to view the real-time effects and further + analyze the data from Impax buddies. + </li> + <li> + Seperate dashboards for players and team managers. + </li> + <li> + Players can get the analysis of their impact history. + </li> + + </ul> + </p> + </div> + <!-- <a class="btn" onclick="openModal('assets/img/dashboard-screenshot.png')">Inspect</a> --> + </div> + </div> + + <div class="card"> + <div class="card-media"> + <img src="assets/img/hub-labled.jpg"> + </div> + <div class="card-content fixie"> + <div class="title"> + <h2>ImpaX Hub</h2> + </div> + <div class="description"> + <p> + <ul> + <li> + Middleware for the communication between buddy devices and the desktop dashboards during a session. + </li> + <li> + With optimized single range and dual LED indicators, it ensures reliable communication between devices and dashboards. + </li> + </ul> + </p> + </div> + <!-- <a class="btn" onclick="openModal('assets/img/hub-labled.jpg')">Inspect</a> --> + </div> + </div> + + <div class="card"> + <div class="card-media"> + <img src="assets/img/buddy-labled.jpg"> + </div> + <div class="card-content fixie"> + <div class="title"> + <h2>ImpaX Buddy</h2> + </div> + <div class="description"> + <p> + <ul> + <li> + Wearable device worn on the head of the athlete, which sends real-time information on impacts. + </li> + <li> + It consists of gyroscope sensor and accelerometer to measure the impact. + </li> + </ul> + </p> + </div> + <!-- <a class="btn" onclick="openModal('assets/img/buddy-labled.jpg')">Inspect</a> --> + </div> + </div> + + + </div> + </div> + + <div id="myModal" class="modal"> + <span class="close" onclick="closeModal()">×</span> + <img class="modal-content" id="modalImg"> + </div> + + + + <!-- <div class="card"> + <div class="card-media"></div> + <div class="card-content fixie"> + <div class="title"> + <h2></h2> + </div> + <div class="description"> + <p></p> + </div><a class="btn"></a> + </div> + </div> + <div class="card"> + <div class="card-media"></div> + <div class="card-content fixie"> + <div class="title"> + <h2></h2> + </div> + <div class="description"> + <p></p> + </div><a class="btn"></a> + </div> + </div> + <div class="card"> + <div class="card-media"></div> + <div class="card-content fixie"> + <div class="title"> + <h2></h2> + </div> + <div class="description"> + <p></p> + </div><a class="btn"></a> + </div> + </div> + <div class="card"> + <div class="card-media"></div> + <div class="card-content fixie"> + <div class="title"> + <h2></h2> + </div> + <div class="description"> + <p></p> + </div><a class="btn"></a> + </div> + </div> + <div class="card"> + <div class="card-media"></div> + <div class="card-content fixie"> + <div class="title"> + <h2></h2> + </div> + <div class="description"> + <p></p> + </div><a class="btn"></a> + </div> + </div> + </div> --> </div> </section> <!-- ============================================ --> - <!-- Hardware --> + <!-- Hardware --> <!-- ============================================ --> <section id="Hardware"> @@ -256,7 +465,7 @@ <h2 class="cs-title">High Level Solution Architecture</h2> <!-- <picture class="cs-wrapper" aria-hidden="true"> <img class="cs-decal" src="https://csimg.nyc3.cdn.digitaloceanspaces.com/Food-Menu/decal1.svg" decoding="async" alt="decal" width="181" height="36"> </picture> --> - <h2 class="cs-title">Hardware Components</h2> + <h2 class="cs-title">Hardware Infrastructure</h2> </div> <ul class="cs-card-group"> <li class="cs-item"> @@ -271,7 +480,8 @@ <h3 class="cs-h3"> </h3> <p class="cs-item-p"> - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. + Compact single-board computer with wireless connectivity, ideal for low-power projects and IoT applications. + </p> </div> </li> @@ -287,7 +497,8 @@ <h3 class="cs-h3"> </h3> <p class="cs-item-p"> - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. + Wireless microcontroller with integrated Wi-Fi and Bluetooth capabilities, suitable for IoT projects and sensor interfacing. + </p> </div> </li> @@ -302,7 +513,8 @@ <h3 class="cs-h3"> <span class="cs-name">3.7V 1000mah 30C Lipo Battery</span> </h3> <p class="cs-item-p"> - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. + High-capacity, high-discharge, Smaller in size lithium-polymer battery for portable power applications. + </p> </div> </li> @@ -317,7 +529,8 @@ <h3 class="cs-h3"> <span class="cs-name">Accelerometer MPU6050 Module</span> </h3> <p class="cs-item-p"> - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. + Triple-axis accelerometer and gyroscope sensor module for motion sensing and orientation tracking. + </p> </div> </li> @@ -332,7 +545,8 @@ <h3 class="cs-h3"> <span class="cs-name">Triple Axis Accelerometer</span> </h3> <p class="cs-item-p"> - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. + Sensor for measuring acceleration in three dimensions, commonly used in motion and tilt-sensing applications. + </p> </div> </li> @@ -347,22 +561,23 @@ <h3 class="cs-h3"> <span class="cs-name">3.7V 18650 Rechargable Battery</span> </h3> <p class="cs-item-p"> - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. - </p> + Rechargable and smaller in size. Used to power the Impax hub. + </p> + </div> </li> <li class="cs-item"> <picture class="cs-picture" aria-hidden="true"> - <source media="(min-width: 601px)" srcset="assets/img/accelerometer.jpg"> - <source media="(max-width: 600px)" srcset="assets/img/accelerometer.jpg"> - <img loading="lazy" decoding="async" src="assets/img/accelerometer.jpg" alt="food" width="160" height="160"> + <source media="(min-width: 601px)" srcset="assets/img/buck-booster.webp"> + <source media="(max-width: 600px)" srcset="assets/img/buck-booster.webp"> + <img loading="lazy" decoding="async" src="assets/img/buck-booster.webp" alt="food" width="160" height="160"> </picture> <div class="cs-info"> <h3 class="cs-h3"> - <span class="cs-name">Triple Axis Accelerometer</span> + <span class="cs-name">Buck booster Converter</span> </h3> <p class="cs-item-p"> - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. + Used to step up the voltage from 3.7V to 5V for the operation of the Raspberry pi Zero W module. </p> </div> </li> @@ -377,7 +592,8 @@ <h3 class="cs-h3"> <span class="cs-name">TP 4056 Lithium Battery Charger Module</span> </h3> <p class="cs-item-p"> - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. + Provide overcharge protection for the 18650 rechargable battery. + </p> </div> </li> @@ -385,7 +601,142 @@ <h3 class="cs-h3"> </div> <!--Static background is in CSS as a tiled pattern--> </section> - + + <!-- ============================================ --> + <!-- Video Gallery --> + <!-- ============================================ --> + <section id="video-gallery"> + <div class="content"> + + <div class="section-header"> + <h2 class="cs-title">3D Designs</h1> + </div> + + <div class="video-gallery"> + + <div class="gallery-item "> + <img src="assets/img/3D-hub-img.jpg" alt="3d model-hub"/> + <div class="gallery-item-caption"> + <!--<div>--> + <h2>Impax-Hub</h2> + <!-- <p>14410 feet of adventure</p> --> + <!--</div>--> + <a class="vimeo-popup" href="assets/img/3d-hub.mp4"></a> + </div> + </div> + + <div class="gallery-item"> + <img src="assets\img\3D-buddy-img.jpg" alt="3d model-buddy"/> + <div class="gallery-item-caption"> + <!--<div>--> + <h2>Impax-Buddy</h2> + <!-- <p>Mountains, rain forests, wild coastlines</p> --> + <!--</div>--> + <a class="vimeo-popup" href="assets/img/3d-buddy.mp4"></a> + </div> + </div> + + <!-- <div class="gallery-item"> + <img src="https://assets.codepen.io/156905/northcascadespark.jpg" class="north-cascades-img" alt="North Cascades National Park"/> + <div class="gallery-item-caption"> --> + <!--<div>--> + <!-- <h2>North Cascades</h2> + <p>The mountains are calling</p> --> + <!--</div>--> + <!-- <a class="vimeo-popup" href="https://vimeo.com/3653567"></a> + </div> + </div> + --> + <!-- <div class="gallery-item"> + <img src="https://assets.codepen.io/156905/mountsthelens.jpg" alt="Mount St Helens"/> + <div class="gallery-item-caption"> --> + <!--<div>--> + <!-- <h2>Mount St. Helens</h2> + <p>The one and only</p> --> + <!--</div>--> + <!-- <a class="vimeo-popup" href="https://vimeo.com/171540296"></a> + </div> + </div> + --> + + + </div> + </div> + </section> + <section id="video-gallery"> + <div class="content"> + + <div class="section-header"> + <h2 class="cs-title">Video Demonstration</h1> + </div> + + <div class="video-gallery"> + + <div class="gallery-item "> + <img src="assets/img/Demo.jpg" alt="3d model-hub"/> + <div class="gallery-item-caption"> + <div> + <h2>Video Demonstration</h2> + <!-- <p>14410 feet of adventure</p> --> + </div> + <a class="vimeo-popup" href="https://drive.google.com/file/d/10Y2OGkNmfh4zBngDk4XLzLXRg2bCRZhA/view?t=2"></a> + <!-- <iframe src="https://drive.google.com/file/d/10Y2OGkNmfh4zBngDk4XLzLXRg2bCRZhA/preview" width="640" height="480" allow="autoplay"></iframe> --> + </div> + </div> + </div> + </div></section> + <!-- ============================================ --> + <!-- User Feedbacks--> + <!----------------------------------> + + <section id="video-gallery"> + <div class="content"> + + <div class="section-header"> + <h2 class="cs-title">User Feedbacks</h1> + </div> + + <div class="video-gallery"> + + <div class="gallery-item"> + <img src="assets\img\User-feedback.jpg" alt="3d model-buddy"/> + <div class="gallery-item-caption"> + <div> + <h2>Uthsara and Kavindu</h2> + <p>Boxing players of UOP team</p> + </div> + <a class="vimeo-popup" href="https://drive.google.com/file/d/1iKZmcOc2lcuGWbpmopv_ffbPdR8j_A0z/view?t=2"></a> + </div> + </div> + + <div class="gallery-item"> + <img src="assets/img/Coach-feedback.jpg" class="north-cascades-img" alt="North Cascades National Park"/> + <div class="gallery-item-caption"> + <div> + <h2>Mr. Anusha Mallawaarachchi</h2> + <p>Boxing Coach of UOP</p> + </div> + <a class="vimeo-popup" href="https://drive.google.com/file/d/1eB81XjcQ57mBLuWyad84G_S9hK8-FHv2/view?t=2"></a> + </div> + </div> + + + <!-- <div class="gallery-item"> + <img src="https://assets.codepen.io/156905/mountsthelens.jpg" alt="Mount St Helens"/> + <div class="gallery-item-caption"> --> + <!--<div>--> + <!-- <h2>Mount St. Helens</h2> + <p>The one and only</p> --> + <!--</div>--> + <!-- <a class="vimeo-popup" href="https://vimeo.com/171540296"></a> + </div> + </div> + --> + + + </div> + </div> + </section> @@ -400,11 +751,12 @@ <h2 class="cs-title">Meet Our Team</h2> <ul class="cs-card-group"> <li class="cs-item"> + <a href="#Problem"> <picture class="cs-picture"> <source media="(max-width: 767px)" srcset="assets/img/team/e19094.jpg"> <source media="(min-width: 768px)" srcset="assets/img/team/e19094.jpg"> <img aria-hidden="true" loading="lazy" decoding="async" src="assets/img/team/e19094.jpg" alt="Mansitha" width="305" height="335"> - </picture> + </picture></a> <div class="cs-info"> <span class="cs-name">Mansitha Eashwara</span> <span class="cs-job">E/19/094</span> diff --git a/docs/styles.css b/docs/styles.css index 8b88942a..a5490fce 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -23,19 +23,23 @@ body { margin: 0; padding: 0; + box-sizing: border-box; + font-family: "Raleway", sans-serif; } -*, *:before, *:after { +*, +*:before, +*:after { /* prevents padding from affecting height and width */ box-sizing: border-box; } .cs-topper { - font-family: 'Courier New', Courier, monospace; + font-family: "Courier New", Courier, monospace; font-size: var(--topperFontSize); line-height: 1.2em; text-transform: uppercase; text-align: inherit; - letter-spacing: .1em; + letter-spacing: 0.1em; font-weight: 700; color: var(--primary); margin-bottom: 0.25rem; @@ -44,18 +48,18 @@ body { .cs-title { font-size: var(--headerFontSize); - font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif; + font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif; font-weight: 900; line-height: 1.2em; text-align: inherit; - max-width: 43.75rem; + max-width: fit-content; margin: 0 0 1rem 0; color: var(--headerColor); position: relative; } .cs-text { - font-family:Cambria, Cochin, Georgia, Times, 'Times New Roman', serif; + font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif; font-size: var(--bodyFontSize); line-height: 1.5em; text-align: inherit; @@ -66,268 +70,268 @@ body { } @keyframes slideUp { from { - transform: translateY(80%); + transform: translateY(80%); } to { - transform: translateY(0); + transform: translateY(0); } } @keyframes slideDown { from { - transform: translateY(20%); + transform: translateY(20%); } to { - transform: translateY(100%); + transform: translateY(100%); } } - + @media only screen and (min-width: 0rem) { #hero-229 { - /* Centers button */ - text-align: center; - /* changes on tablet */ - padding: 0 1rem; - position: relative; - z-index: 1; - /* prevents overflow from the lines extending past the screen width */ - overflow: hidden; + /* Centers button */ + text-align: center; + /* changes on tablet */ + padding: 0 1rem; + position: relative; + z-index: 1; + /* prevents overflow from the lines extending past the screen width */ + overflow: hidden; } #hero-229 .cs-picture { - /* Background Image */ - width: 100%; - height: 100%; - display: block; - position: absolute; - top: 0; - left: 0; - z-index: -2; + /* Background Image */ + width: 100%; + height: 100%; + display: block; + position: absolute; + top: 0; + left: 0; + z-index: -2; } #hero-229 .cs-picture:before { - /* Black Color Overlay */ - content: ""; - width: 100%; - height: 100%; - background: #000; - opacity: 0.6; - position: absolute; - display: block; - top: 0; - left: 0; - z-index: 1; - /* prevents the cursor from interacting with it */ - pointer-events: none; + /* Black Color Overlay */ + content: ""; + width: 100%; + height: 100%; + background: #000; + opacity: 0.6; + position: absolute; + display: block; + top: 0; + left: 0; + z-index: 1; + /* prevents the cursor from interacting with it */ + pointer-events: none; } #hero-229 .cs-picture img { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - /* Makes image act like a background-image */ - object-fit: cover; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + /* Makes image act like a background-image */ + object-fit: cover; } #hero-229 .cs-container { - width: 100%; - max-width: 80rem; - /* we put the padding top and bottom on the container instead of #Hero so the pseudo element lines go to the top and bottom of the section */ - /* 144px - 280px - leaving extra space for the navigation */ - /* changes on tablet */ - padding: clamp(9rem, 25.95vw, 17.5rem) 0; - margin: auto; - position: relative; + width: 100%; + max-width: 80rem; + /* we put the padding top and bottom on the container instead of #Hero so the pseudo element lines go to the top and bottom of the section */ + /* 144px - 280px - leaving extra space for the navigation */ + /* changes on tablet */ + padding: clamp(9rem, 25.95vw, 17.5rem) 0; + margin: auto; + position: relative; } #hero-229 .cs-container:before { - /* Left Line */ - content: ""; - width: 1px; - height: 100%; - background: -moz-linear-gradient( - top, - rgba(250, 251, 252, 0.5) 0%, - rgba(250, 251, 252, 0) 100% - ); /* FF3.6-15 */ - background: -webkit-linear-gradient( - top, - rgba(250, 251, 252, 0.5) 0%, - rgba(250, 251, 252, 0) 100% - ); /* Chrome10-25,Safari5.1-6 */ - opacity: 1; - position: absolute; - display: block; - top: 0; - left: 0; + /* Left Line */ + content: ""; + width: 1px; + height: 100%; + background: -moz-linear-gradient( + top, + rgba(250, 251, 252, 0.5) 0%, + rgba(250, 251, 252, 0) 100% + ); /* FF3.6-15 */ + background: -webkit-linear-gradient( + top, + rgba(250, 251, 252, 0.5) 0%, + rgba(250, 251, 252, 0) 100% + ); /* Chrome10-25,Safari5.1-6 */ + opacity: 1; + position: absolute; + display: block; + top: 0; + left: 0; } #hero-229 .cs-flex-group { - /* 60px - 220px */ - margin-bottom: clamp(3.75rem, 15.5vw, 13.75rem); - margin: auto; - width: 80vw; - /* 464px - 562px */ - max-width: clamp(29rem, 60vw, 35.125rem); - display: flex; - align-items: flex-start; - justify-content: center; - flex-wrap: wrap; - column-gap: 1.25rem; - box-sizing: border-box; + /* 60px - 220px */ + margin-bottom: clamp(3.75rem, 15.5vw, 13.75rem); + margin: auto; + width: 80vw; + /* 464px - 562px */ + max-width: clamp(29rem, 60vw, 35.125rem); + display: flex; + align-items: flex-start; + justify-content: center; + flex-wrap: wrap; + column-gap: 1.25rem; + box-sizing: border-box; } #hero-229 .cs-topper { - /* 13px - 16px */ - font-size: clamp(0.8125rem, 1.6vw, 1rem); - line-height: 1.2em; - text-transform: uppercase; - text-align: center; - letter-spacing: 0.1rem; - font-weight: 700; - color: var(--primaryLight); - margin-bottom: 1rem; - display: block; + /* 13px - 16px */ + font-size: clamp(0.8125rem, 1.6vw, 1rem); + line-height: 1.2em; + text-transform: uppercase; + text-align: center; + letter-spacing: 0.1rem; + font-weight: 700; + color: var(--primaryLight); + margin-bottom: 1rem; + display: block; } #hero-229 .cs-title { - /* 39px - 61px */ - font-size: clamp(2.4375rem, 6.4vw, 3.8125rem); - font-weight: 900; - line-height: 1.2em; - text-align: center; - width: 100%; - /* 32px - 40px */ - margin: 0 auto clamp(2rem, 4vw, 2.5rem) 0; - color: var(--bodyTextColorWhite); - position: relative; + /* 39px - 61px */ + font-size: clamp(2.4375rem, 6.4vw, 3.8125rem); + font-weight: 900; + line-height: 1.2em; + text-align: center; + width: 100%; + /* 32px - 40px */ + margin: 0 auto clamp(2rem, 4vw, 2.5rem) 0; + color: var(--bodyTextColorWhite); + position: relative; } #hero-229 .cs-text { - /* 16px - 20px */ - font-size: clamp(1rem, 1.95vw, 1.25rem); - line-height: 1.5em; - text-align: center; - width: 100%; - /* 32px - 40px */ - margin: 0 auto clamp(2rem, 4vw, 2.5rem) 0; - /* 40px - 48px */ - margin-bottom: clamp(2.5rem, 4vw, 3rem); - color: var(--bodyTextColorWhite); + /* 16px - 20px */ + font-size: clamp(1rem, 1.95vw, 1.25rem); + line-height: 1.5em; + text-align: center; + width: 100%; + /* 32px - 40px */ + margin: 0 auto clamp(2rem, 4vw, 2.5rem) 0; + /* 40px - 48px */ + margin-bottom: clamp(2.5rem, 4vw, 3rem); + color: var(--bodyTextColorWhite); } #hero-229 .cs-button-solid { - font-size: 1rem; - /* 46px - 56px */ - line-height: clamp(2.875rem, 5.5vw, 3.5rem); - width: 11.25rem; - text-decoration: none; - font-weight: 700; - margin: 0 0 1rem 0; - color: #fff; - padding: 0; - background-color: var(--primary); - display: inline-block; - position: relative; - z-index: 1; + font-size: 1rem; + /* 46px - 56px */ + line-height: clamp(2.875rem, 5.5vw, 3.5rem); + width: 11.25rem; + text-decoration: none; + font-weight: 700; + margin: 0 0 1rem 0; + color: #fff; + padding: 0; + background-color: var(--primary); + display: inline-block; + position: relative; + z-index: 1; } #hero-229 .cs-button-solid:before { - content: ""; - position: absolute; - display: block; - height: 100%; - width: 0%; - background: #000; - opacity: 1; - top: 0; - left: 0; - z-index: -1; - transition: width 0.3s; + content: ""; + position: absolute; + display: block; + height: 100%; + width: 0%; + background: #000; + opacity: 1; + top: 0; + left: 0; + z-index: -1; + transition: width 0.3s; } #hero-229 .cs-button-solid:hover:before { - width: 100%; + width: 100%; } #hero-229 .cs-button-transparent { - font-size: 1rem; - /* 46px - 56px */ - line-height: clamp(2.875rem, 5.5vw, 3.5rem); - width: 11.25rem; - /* 46px - 56px */ - height: clamp(2.875rem, 5.5vw, 3.5rem); - text-decoration: none; - font-weight: 700; - margin: 0; - color: #fff; - padding: 0; - background-color: transparent; - border: 1px solid var(--bodyTextColorWhite); - box-sizing: border-box; - display: inline-flex; - justify-content: center; - align-items: center; - position: relative; - z-index: 1; + font-size: 1rem; + /* 46px - 56px */ + line-height: clamp(2.875rem, 5.5vw, 3.5rem); + width: 11.25rem; + /* 46px - 56px */ + height: clamp(2.875rem, 5.5vw, 3.5rem); + text-decoration: none; + font-weight: 700; + margin: 0; + color: #fff; + padding: 0; + background-color: transparent; + border: 1px solid var(--bodyTextColorWhite); + box-sizing: border-box; + display: inline-flex; + justify-content: center; + align-items: center; + position: relative; + z-index: 1; } #hero-229 .cs-button-transparent:before { - content: ""; - position: absolute; - display: block; - background: #000; - opacity: 1; - /* so it sits on top of the border */ - top: -1px; - left: -1px; - right: -1px; - bottom: -1px; - z-index: -1; - transform-origin: left; - /* this is what creates the grow affect on hover */ - transform: scaleX(0); - transition: transform 0.3s; + content: ""; + position: absolute; + display: block; + background: #000; + opacity: 1; + /* so it sits on top of the border */ + top: -1px; + left: -1px; + right: -1px; + bottom: -1px; + z-index: -1; + transform-origin: left; + /* this is what creates the grow affect on hover */ + transform: scaleX(0); + transition: transform 0.3s; } #hero-229 .cs-button-transparent:hover:before { - transform: scaleX(1); + transform: scaleX(1); } #hero-229 .cs-button-transparent .cs-img { - display: block; - margin-right: 0.75rem; + display: block; + margin-right: 0.75rem; } } /* Tablet - 768px */ @media only screen and (min-width: 48rem) { #hero-229 { - /* 32px - 40px */ - padding: 0 clamp(2rem, 5vw, 2.5rem); + /* 32px - 40px */ + padding: 0 clamp(2rem, 5vw, 2.5rem); } #hero-229 .cs-container:after { - /* Right Line */ - content: ""; - width: 1px; - height: 100%; - background: -moz-linear-gradient( - top, - rgba(250, 251, 252, 0) 0%, - rgba(250, 251, 252, 0.5) 100% - ); /* FF3.6-15 */ - background: -webkit-linear-gradient( - top, - rgba(250, 251, 252, 0) 0%, - rgba(250, 251, 252, 0.5) 100% - ); /* Chrome10-25,Safari5.1-6 */ - opacity: 1; - position: absolute; - display: block; - top: 0; - right: 0; + /* Right Line */ + content: ""; + width: 1px; + height: 100%; + background: -moz-linear-gradient( + top, + rgba(250, 251, 252, 0) 0%, + rgba(250, 251, 252, 0.5) 100% + ); /* FF3.6-15 */ + background: -webkit-linear-gradient( + top, + rgba(250, 251, 252, 0) 0%, + rgba(250, 251, 252, 0.5) 100% + ); /* Chrome10-25,Safari5.1-6 */ + opacity: 1; + position: absolute; + display: block; + top: 0; + right: 0; } #hero-229 .cs-button-solid { - margin-bottom: 0; + margin-bottom: 0; } } /* Desktop Parallax Effect - 1300px */ @media only screen and (min-width: 81.25rem) { #hero-229 { - background: url("assets/img/background.jpg"); - background-size: cover; - background-position: center; - background-repeat: no-repeat; - /* creates parallax effect on background image */ - background-attachment: fixed; - /* remove img tag so we can make parallax work */ + background: url("assets/img/background.jpg"); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + /* creates parallax effect on background image */ + background-attachment: fixed; + /* remove img tag so we can make parallax work */ } #hero-229 .cs-picture img { - display: none; + display: none; } } @@ -669,7 +673,8 @@ body { align-items: center; gap: 0.75rem; overflow: hidden; - transition: padding 0.3s, margin 0.3s, height 0.3s, opacity 0.3s, visibility 0.3s; + transition: padding 0.3s, margin 0.3s, height 0.3s, opacity 0.3s, + visibility 0.3s; } #cs-navigation .cs-drop-li { list-style: none; @@ -1142,7 +1147,7 @@ body { z-index: 100; } #cs-navigation .cs-logo img { - width: 100%; + width: 60%; height: 100%; /* ensures the image never overflows the container. It stays contained within it's width and height and expands to fill it then stops once it reaches an edge */ object-fit: contain; @@ -1249,890 +1254,890 @@ body { <--- Side By Side Reverse --> <--- -------------------------- -*/ - /* Mobile - 360px */ - @media only screen and (min-width: 0rem) { - #Problem { - padding: var(--sectionPadding); - overflow: hidden; - position: relative; - z-index: 1; - } - @keyframes floatAnimation { - 0% { - transform: translateY(0); - } - 50% { - transform: translateY(-2em); - } - 100% { - transform: translateY(0); - } - } - @keyframes floatAnimation2 { - 0% { - transform: translateY(0); - } - 50% { - transform: translateY(-1em); - } - 100% { - transform: translateY(0); - } - } - #Problem .cs-container { - width: 100%; - /* changes to 1280px at desktop */ - max-width: 34.375rem; - margin: auto; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - /* 48px - 56px */ - gap: clamp(3rem, 3vw, 3.5rem); - } - #Problem:before { - /* static tiled pattern */ - content: ""; - height: 100%; - width: 100%; - opacity: 0.08; - background: url("https://csimg.nyc3.cdn.digitaloceanspaces.com/Food-Menu/static-pattern.png"); - background-size: auto; - background-position: center; - background-repeat: repeat; - display: block; - position: absolute; - top: 0; - left: 0; - z-index: -1; +/* Mobile - 360px */ +@media only screen and (min-width: 0rem) { + #Problem { + padding: var(--sectionPadding); + overflow: hidden; + position: relative; + z-index: 1; + } + @keyframes floatAnimation { + 0% { + transform: translateY(0); + } + 50% { + transform: translateY(-2em); + } + 100% { + transform: translateY(0); + } + } + @keyframes floatAnimation2 { + 0% { + transform: translateY(0); } - #Problem .cs-content { - /* set text align to left if content needs to be left aligned */ - text-align: justify; - width: 100%; - max-width: 33.875rem; - display: flex; - flex-direction: column; - /* centers content horizontally, set to flex-start to left align */ - align-items: flex-start; - } + 50% { + transform: translateY(-1em); + } + 100% { + transform: translateY(0); + } + } + #Problem .cs-container { + width: 100%; + /* changes to 1280px at desktop */ + max-width: 34.375rem; + margin: auto; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + /* 48px - 56px */ + gap: clamp(3rem, 3vw, 3.5rem); + } + #Problem:before { + /* static tiled pattern */ + content: ""; + height: 100%; + width: 100%; + opacity: 0.08; + background: url("https://csimg.nyc3.cdn.digitaloceanspaces.com/Food-Menu/static-pattern.png"); + background-size: auto; + background-position: center; + background-repeat: repeat; + display: block; + position: absolute; + top: 0; + left: 0; + z-index: -1; + } + #Problem .cs-content { + /* set text align to left if content needs to be left aligned */ + text-align: justify; + width: 100%; + max-width: 33.875rem; + display: flex; + flex-direction: column; + /* centers content horizontally, set to flex-start to left align */ + align-items: flex-start; + } - #Problem .cs-text { - margin-bottom: 1rem; - } - #Problem .cs-text:last-of-type { - margin-bottom: 2rem; - } - #Problem .cs-button-solid { - font-size: 1rem; - /* 46px - 56px */ - line-height: clamp(2.875rem, 5.5vw, 3.5rem); - text-decoration: none; - font-weight: 700; - text-align: center; - margin: 0; - color: #fff; - min-width: 9.375rem; - padding: 0 1.5rem; - background-color: var(--primary); - border-radius: 0.25rem; - display: inline-block; - position: relative; - z-index: 1; - /* prevents padding from adding to the width */ - box-sizing: border-box; - } - #Problem .cs-button-solid:before { - content: ""; - position: absolute; - height: 100%; - width: 0%; - background: #000; - opacity: 1; - top: 0; - left: 0; - z-index: -1; - border-radius: 0.25rem; - transition: width 0.3s; - } - #Problem .cs-button-solid:hover:before { - width: 100%; - } - #Problem .cs-image-group { - /* everything in the group is in ems so we can scale them down with this min/max font size tied to the view width */ - font-size: min(2.3vw, 1em); - width: 36.1875em; - height: 36.75em; - position: relative; - /* sends it to the top above the content */ - order: -1; - } - #Problem .cs-splash { - width: 35.625em; - height: 36.625em; - opacity: 0.1; - position: absolute; - left: 0.3125em; - top: 0; - } - #Problem .cs-blob { - width: 24.5em; - height: 31.0625em; - position: absolute; - right: 0em; - top: 0.625em; - } - #Problem .cs-lightning { - width: 4em; - height: 6.4375em; - position: absolute; - right: 2.5em; - top: 0.5em; - transform: rotate(23deg); - } - #Problem .cs-left-image { - width: 13.1875em; - height: 13.1875em; - border-radius: 50%; - /* cover the 1px gap between border and image */ - background-color: #f7f7f7; - border: 12px solid #610404; - /* clips image to the circle */ - overflow: hidden; - position: absolute; - left: -0.75em; - top: 8.1875em; - z-index: 10; - animation-name: floatAnimation2; - animation-duration: 6s; - animation-timing-function: ease-in-out; - animation-fill-mode: forwards; - animation-iteration-count: infinite; - } - #Problem .cs-left-image img { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - /* makes it act like a background image */ - object-fit: cover; - } - #Problem .cs-bottom-image { - width: 11.25em; - height: 11.25em; - border-radius: 50%; - /* cover the 1px gap between border and image */ - background-color: #fafafa; - border: 12px solid #610404; - /* clips image to the circle */ - overflow: hidden; - position: absolute; - right: 11.25em; - bottom: -0.75em; - z-index: 10; - animation-name: floatAnimation; - animation-duration: 10s; - animation-delay: 1s; - animation-timing-function: ease-in-out; - animation-fill-mode: forwards; - animation-iteration-count: infinite; - } - #Problem .cs-bottom-image img { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - /* makes it act like a background image */ - object-fit: cover; - } - #Problem .cs-person { - width: 24.125em; - height: auto; - position: absolute; - right: 2.875em; - bottom: 4.0625em; - } - #Problem .cs-person img { - width: 100%; - height: auto; - } - #Problem .cs-mask { - width: 23.9375em; - height: 10.8125em; - position: absolute; - right: -2.0625em; - bottom: 2.6875em; - z-index: 2; - } - /* Style for the bullet list */ - #Problem ul { - list-style: none; - padding: 0; - margin: 0; - } + #Problem .cs-text { + margin-bottom: 1rem; + } + #Problem .cs-text:last-of-type { + margin-bottom: 2rem; + } + #Problem .cs-button-solid { + font-size: 1rem; + /* 46px - 56px */ + line-height: clamp(2.875rem, 5.5vw, 3.5rem); + text-decoration: none; + font-weight: 700; + text-align: center; + margin: 0; + color: #fff; + min-width: 9.375rem; + padding: 0 1.5rem; + background-color: var(--primary); + border-radius: 0.25rem; + display: inline-block; + position: relative; + z-index: 1; + /* prevents padding from adding to the width */ + box-sizing: border-box; + } + #Problem .cs-button-solid:before { + content: ""; + position: absolute; + height: 100%; + width: 0%; + background: #000; + opacity: 1; + top: 0; + left: 0; + z-index: -1; + border-radius: 0.25rem; + transition: width 0.3s; + } + #Problem .cs-button-solid:hover:before { + width: 100%; + } + #Problem .cs-image-group { + /* everything in the group is in ems so we can scale them down with this min/max font size tied to the view width */ + font-size: min(2.3vw, 1em); + width: 36.1875em; + height: 36.75em; + position: relative; + /* sends it to the top above the content */ + order: -1; + } + #Problem .cs-splash { + width: 35.625em; + height: 36.625em; + opacity: 0.1; + position: absolute; + left: 0.3125em; + top: 0; + } + #Problem .cs-blob { + width: 24.5em; + height: 31.0625em; + position: absolute; + right: 0em; + top: 0.625em; + } + #Problem .cs-lightning { + width: 4em; + height: 6.4375em; + position: absolute; + right: 2.5em; + top: 0.5em; + transform: rotate(23deg); + } + #Problem .cs-left-image { + width: 13.1875em; + height: 13.1875em; + border-radius: 50%; + /* cover the 1px gap between border and image */ + background-color: #f7f7f7; + border: 12px solid #610404; + /* clips image to the circle */ + overflow: hidden; + position: absolute; + left: -0.75em; + top: 8.1875em; + z-index: 10; + animation-name: floatAnimation2; + animation-duration: 6s; + animation-timing-function: ease-in-out; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + } + #Problem .cs-left-image img { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + /* makes it act like a background image */ + object-fit: cover; + } + #Problem .cs-bottom-image { + width: 11.25em; + height: 11.25em; + border-radius: 50%; + /* cover the 1px gap between border and image */ + background-color: #fafafa; + border: 12px solid #610404; + /* clips image to the circle */ + overflow: hidden; + position: absolute; + right: 11.25em; + bottom: -0.75em; + z-index: 10; + animation-name: floatAnimation; + animation-duration: 10s; + animation-delay: 1s; + animation-timing-function: ease-in-out; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + } + #Problem .cs-bottom-image img { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + /* makes it act like a background image */ + object-fit: cover; + } + #Problem .cs-person { + width: 24.125em; + height: auto; + position: absolute; + right: 2.875em; + bottom: 4.0625em; + } + #Problem .cs-person img { + width: 100%; + height: auto; + } + #Problem .cs-mask { + width: 23.9375em; + height: 10.8125em; + position: absolute; + right: -2.0625em; + bottom: 2.6875em; + z-index: 2; + } + /* Style for the bullet list */ + #Problem ul { + list-style: none; + padding: 0; + margin: 0; + } - /* Style for the list items */ - #Problem li { - margin-bottom: 10px; - padding-left: 20px; - position: relative; - font-size: 16px; - line-height: 1.6; - color: #333; - } + /* Style for the list items */ + #Problem li { + margin-bottom: 10px; + padding-left: 20px; + position: relative; + font-size: 16px; + line-height: 1.6; + color: #333; + } - /* Style for the custom bullet point */ - #Problem li::before { - content: "\2713"; /* Unicode character for a checkmark */ - color: #610404; /* Change the color as needed */ - font-size: 20px; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - } - } - /* Small Desktop - 1024px */ - @media only screen and (min-width: 64rem) { - #Problem .cs-container { - max-width: 80rem; - flex-direction: row; - justify-content: space-between; - align-items: flex-start; - } - #Problem .cs-ul { - margin-top: 1rem; - } - #Problem .cs-image-group { - font-size: min(1.1vw, 1em); - /* prevents flexbox from squishing it */ - flex: none; - } + /* Style for the custom bullet point */ + #Problem li::before { + content: "\2713"; /* Unicode character for a checkmark */ + color: #610404; /* Change the color as needed */ + font-size: 20px; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + } +} +/* Small Desktop - 1024px */ +@media only screen and (min-width: 64rem) { + #Problem .cs-container { + max-width: 80rem; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + } + #Problem .cs-ul { + margin-top: 1rem; } + #Problem .cs-image-group { + font-size: min(1.1vw, 1em); + /* prevents flexbox from squishing it */ + flex: none; + } +} - /*-- -------------------------- --> +/*-- -------------------------- --> <--- Side By Side Solution --> <--- -------------------------- -*/ - /* Mobile - 360px */ - @media only screen and (min-width: 0rem) { - #Solution{ - padding: var(--sectionPadding); - overflow: hidden; - position: relative; - z-index: 1; - } - @keyframes floatAnimation { - 0% { - transform: translateY(0); - } - 50% { - transform: translateY(-2em); - } - 100% { - transform: translateY(0); - } - } - @keyframes floatAnimation2 { - 0% { - transform: translateY(0); - } - 50% { - transform: translateY(-1em); - } - 100% { - transform: translateY(0); - } - } - #Solution .cs-container { - width: 100%; - /* changes to 1280px at desktop */ - max-width: 34.375rem; - margin: auto; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - /* 48px - 56px */ - gap: clamp(3rem, 3vw, 3.5rem); - } - #Solution .cs-content { - /* set text align to left if content needs to be left aligned */ - text-align: justify; - width: 100%; - max-width: 33.875rem; - display: flex; - flex-direction: column; - /* centers content horizontally, set to flex-start to left align */ - align-items: flex-start; - } - #Solution:before { - /* static tiled pattern */ - content: ""; - height: 100%; - width: 100%; - opacity: 0.08; - background: url("https://csimg.nyc3.cdn.digitaloceanspaces.com/Food-Menu/static-pattern.png"); - background-size: auto; - background-position: center; - background-repeat: repeat; - display: block; - position: absolute; - top: 0; - left: 0; - z-index: -1; +/* Mobile - 360px */ +@media only screen and (min-width: 0rem) { + #Solution { + padding: var(--sectionPadding); + overflow: hidden; + position: relative; + z-index: 1; + } + @keyframes floatAnimation { + 0% { + transform: translateY(0); } + 50% { + transform: translateY(-2em); + } + 100% { + transform: translateY(0); + } + } + @keyframes floatAnimation2 { + 0% { + transform: translateY(0); + } + 50% { + transform: translateY(-1em); + } + 100% { + transform: translateY(0); + } + } + #Solution .cs-container { + width: 100%; + /* changes to 1280px at desktop */ + max-width: 34.375rem; + margin: auto; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + /* 48px - 56px */ + gap: clamp(3rem, 3vw, 3.5rem); + } + #Solution .cs-content { + /* set text align to left if content needs to be left aligned */ + text-align: justify; + width: 100%; + max-width: 33.875rem; + display: flex; + flex-direction: column; + /* centers content horizontally, set to flex-start to left align */ + align-items: flex-start; + } + #Solution:before { + /* static tiled pattern */ + content: ""; + height: 100%; + width: 100%; + opacity: 0.08; + background: url("https://csimg.nyc3.cdn.digitaloceanspaces.com/Food-Menu/static-pattern.png"); + background-size: auto; + background-position: center; + background-repeat: repeat; + display: block; + position: absolute; + top: 0; + left: 0; + z-index: -1; + } - #Solution .cs-text { - margin-bottom: 1rem; - } - #Solution .cs-text:last-of-type { - margin-bottom: 2rem; - } - #Solution .cs-button-solid { - font-size: 1rem; - /* 46px - 56px */ - line-height: clamp(2.875rem, 5.5vw, 3.5rem); - text-decoration: none; - font-weight: 700; - text-align: center; - margin: 0; - color: #fff; - min-width: 9.375rem; - padding: 0 1.5rem; - background-color: var(--primary); - border-radius: 0.25rem; - display: inline-block; - position: relative; - z-index: 1; - /* prevents padding from adding to the width */ - box-sizing: border-box; - } - #Solution .cs-button-solid:before { - content: ""; - position: absolute; - height: 100%; - width: 0%; - background: #000; - opacity: 1; - top: 0; - left: 0; - z-index: -1; - border-radius: 0.25rem; - transition: width 0.3s; - } - #Solution .cs-button-solid:hover:before { - width: 100%; - } - #Solution .cs-image-group { - /* everything in the group is in ems so we can scale them down with this min/max font size tied to the view width */ - font-size: min(2.3vw, 1em); - width: 36.1875em; - height: 36.75em; - position: relative; - /* sends it to the top above the content */ - order: -1; - } - #Solution .cs-splash { - width: 35.625em; - height: 36.625em; - opacity: 0.1; - position: absolute; - left: 0.3125em; - top: 0; - } - #Solution .cs-blob { - width: 24.5em; - height: 31.0625em; - position: absolute; - right: 0em; - top: 0.625em; - } - #Solution .cs-lightning { - width: 4em; - height: 6.4375em; - position: absolute; - right: 2.5em; - top: 0.5em; - transform: rotate(23deg); - } - #Solution .cs-left-image { - width: 13.1875em; - height: 13.1875em; - border-radius: 50%; - /* cover the 1px gap between border and image */ - background-color: #f7f7f7; - border: 12px solid #610404; - /* clips image to the circle */ - overflow: hidden; - position: absolute; - left: -0.75em; - top: 8.1875em; - z-index: 10; - animation-name: floatAnimation2; - animation-duration: 6s; - animation-timing-function: ease-in-out; - animation-fill-mode: forwards; - animation-iteration-count: infinite; - } - #Solution .cs-left-image img { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - /* makes it act like a background image */ - object-fit: cover; - } - #Solution .cs-bottom-image { - width: 11.25em; - height: 11.25em; - border-radius: 50%; - /* cover the 1px gap between border and image */ - background-color: #fafafa; - border: 12px solid #610404; - /* clips image to the circle */ - overflow: hidden; - position: absolute; - right: 11.25em; - bottom: -0.75em; - z-index: 10; - animation-name: floatAnimation; - animation-duration: 10s; - animation-delay: 1s; - animation-timing-function: ease-in-out; - animation-fill-mode: forwards; - animation-iteration-count: infinite; - } - #Solution .cs-bottom-image img { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - /* makes it act like a background image */ - object-fit: cover; - } - #Solution .cs-person { - width: 24.125em; - height: auto; - position: absolute; - right: 2.875em; - bottom: 4.0625em; - } - #Solution .cs-person img { - width: 100%; - height: auto; - } - #Solution .cs-mask { - width: 23.9375em; - height: 10.8125em; - position: absolute; - right: -2.0625em; - bottom: 2.6875em; - z-index: 2; - } - /* Style for the bullet list */ - #Solution ul { - list-style: none; - padding: 0; - margin: 0; - } - - /* Style for the list items */ - #Solution li { - margin-bottom: 10px; - padding-left: 20px; - position: relative; - font-size: 16px; - line-height: 1.6; - color: #333; - } + #Solution .cs-text { + margin-bottom: 1rem; + } + #Solution .cs-text:last-of-type { + margin-bottom: 2rem; + } + #Solution .cs-button-solid { + font-size: 1rem; + /* 46px - 56px */ + line-height: clamp(2.875rem, 5.5vw, 3.5rem); + text-decoration: none; + font-weight: 700; + text-align: center; + margin: 0; + color: #fff; + min-width: 9.375rem; + padding: 0 1.5rem; + background-color: var(--primary); + border-radius: 0.25rem; + display: inline-block; + position: relative; + z-index: 1; + /* prevents padding from adding to the width */ + box-sizing: border-box; + } + #Solution .cs-button-solid:before { + content: ""; + position: absolute; + height: 100%; + width: 0%; + background: #000; + opacity: 1; + top: 0; + left: 0; + z-index: -1; + border-radius: 0.25rem; + transition: width 0.3s; + } + #Solution .cs-button-solid:hover:before { + width: 100%; + } + #Solution .cs-image-group { + /* everything in the group is in ems so we can scale them down with this min/max font size tied to the view width */ + font-size: min(2.3vw, 1em); + width: 36.1875em; + height: 36.75em; + position: relative; + /* sends it to the top above the content */ + order: -1; + } + #Solution .cs-splash { + width: 35.625em; + height: 36.625em; + opacity: 0.1; + position: absolute; + left: 0.3125em; + top: 0; + } + #Solution .cs-blob { + width: 24.5em; + height: 31.0625em; + position: absolute; + right: 0em; + top: 0.625em; + } + #Solution .cs-lightning { + width: 4em; + height: 6.4375em; + position: absolute; + right: 2.5em; + top: 0.5em; + transform: rotate(23deg); + } + #Solution .cs-left-image { + width: 13.1875em; + height: 13.1875em; + border-radius: 50%; + /* cover the 1px gap between border and image */ + background-color: #f7f7f7; + border: 12px solid #610404; + /* clips image to the circle */ + overflow: hidden; + position: absolute; + left: -0.75em; + top: 8.1875em; + z-index: 10; + animation-name: floatAnimation2; + animation-duration: 6s; + animation-timing-function: ease-in-out; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + } + #Solution .cs-left-image img { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + /* makes it act like a background image */ + object-fit: cover; + } + #Solution .cs-bottom-image { + width: 11.25em; + height: 11.25em; + border-radius: 50%; + /* cover the 1px gap between border and image */ + background-color: #fafafa; + border: 12px solid #610404; + /* clips image to the circle */ + overflow: hidden; + position: absolute; + right: 11.25em; + bottom: -0.75em; + z-index: 10; + animation-name: floatAnimation; + animation-duration: 10s; + animation-delay: 1s; + animation-timing-function: ease-in-out; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + } + #Solution .cs-bottom-image img { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + /* makes it act like a background image */ + object-fit: cover; + } + #Solution .cs-person { + width: 24.125em; + height: auto; + position: absolute; + right: 2.875em; + bottom: 4.0625em; + } + #Solution .cs-person img { + width: 100%; + height: auto; + } + #Solution .cs-mask { + width: 23.9375em; + height: 10.8125em; + position: absolute; + right: -2.0625em; + bottom: 2.6875em; + z-index: 2; + } + /* Style for the bullet list */ + #Solution ul { + list-style: none; + padding: 0; + margin: 0; + } - /* Style for the custom bullet point */ - #Solution li::before { - content: "\2713"; /* Unicode character for a checkmark */ - color: #610404; /* Change the color as needed */ - font-size: 20px; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - } - } - /* Small Desktop - 1024px */ - @media only screen and (min-width: 64rem) { - #Solution .cs-container { - max-width: 80rem; - flex-direction: row; - justify-content: space-between; - align-items: flex-start; - } - #Solution .cs-ul { - margin-top: 1rem; - } - #Solution .cs-image-group { - font-size: min(1.1vw, 1em); - /* prevents flexbox from squishing it */ - flex: none; - } + /* Style for the list items */ + #Solution li { + margin-bottom: 10px; + padding-left: 20px; + position: relative; + font-size: 16px; + line-height: 1.6; + color: #333; + } + + /* Style for the custom bullet point */ + #Solution li::before { + content: "\2713"; /* Unicode character for a checkmark */ + color: #610404; /* Change the color as needed */ + font-size: 20px; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + } +} +/* Small Desktop - 1024px */ +@media only screen and (min-width: 64rem) { + #Solution .cs-container { + max-width: 80rem; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + } + #Solution .cs-ul { + margin-top: 1rem; + } + #Solution .cs-image-group { + font-size: min(1.1vw, 1em); + /* prevents flexbox from squishing it */ + flex: none; + } +} + +/*-- -------------------------- --> + <--- Meet The Team --> + <--- -------------------------- -*/ + +/* Mobile - 360px */ +@media only screen and (min-width: 0rem) { + #meet-us-1021 { + padding: var(--sectionPadding); + background-color: #1a1a1a; + } + #meet-us-1021 .cs-container { + width: 100%; + max-width: 80rem; + margin: auto; + position: relative; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + /* 48px - 64px */ + gap: clamp(3rem, 6vw, 4rem); + } + #meet-us-1021 .cs-container { + width: 100%; + max-width: 80rem; + margin: auto; + display: flex; + flex-direction: column; + align-items: center; + /* 48px - 64px */ + gap: clamp(3rem, 6vw, 4rem); + } + #meet-us-1021 .cs-content { + /* set text align to left if content needs to be left aligned */ + text-align: center; + width: 100%; + display: flex; + flex-direction: column; + /* centers content horizontally, set to flex-start to left align */ + align-items: center; + } + #meet-us-1021 .cs-graphic { + width: 100%; + max-width: 26.82352941rem; + margin: 0 0 1rem 0; + } + #meet-us-1021 .cs-title { + font-size: var(--headerFontSize); + font-weight: 900; + line-height: 1.2em; + text-align: inherit; + max-width: 43.75rem; + margin: 0 0 1rem 0; + color: var(--headerColor); + position: relative; + } + #meet-us-1021 .cs-text { + font-size: var(--bodyFontSize); + line-height: 1.5em; + text-align: inherit; + width: 100%; + max-width: 40.625rem; + margin: 0; + color: var(--bodyTextColor); + } + #meet-us-1021 .cs-title, + #meet-us-1021 .cs-text { + color: var(--bodyTextColorWhite); + } + #meet-us-1021 .cs-text { + opacity: 0.8; + } + #meet-us-1021 .cs-card-group { + width: 100%; + padding: 0; + margin: 0; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + flex-wrap: wrap; + /* 16px - 20px */ + gap: clamp(1rem, 2vw, 1.25rem); + } + #meet-us-1021 .cs-item { + list-style: none; + width: 100%; + max-width: 25.8125rem; + position: relative; + z-index: 1; + } + #meet-us-1021 .cs-item:hover .cs-picture { + background-color: #000; + } + #meet-us-1021 .cs-item:hover .cs-picture img { + transform: scale(1.1); + opacity: 0.6; + } + #meet-us-1021 .cs-info { + width: 85.5%; + /* 16px - 24px */ + padding: clamp(1rem, 2vw, 1.5rem); + margin: -3.75rem auto 0; + box-sizing: border-box; + background-color: #1a1a1a; + border-top: 4px solid var(--primary); + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + z-index: 10; + } + #meet-us-1021 .cs-name { + /* 16px - 25px */ + font-size: clamp(1rem, 2vw, 1.5625rem); + font-weight: 700; + line-height: 1.2em; + /* 4px - 8px */ + margin: 0 0 clamp(0.25rem, 1vw, 0.5rem); + color: var(--bodyTextColorWhite); + display: block; + transition: color 0.3s; + } + #meet-us-1021 .cs-job { + /* 14px - 16px */ + font-size: clamp(0.875rem, 1.5vw, 1rem); + line-height: 1.5em; + color: var(--bodyTextColorWhite); + opacity: 0.8; + display: block; + } + #meet-us-1021 .cs-picture { + width: 100%; + /* 246px - 500px */ + height: clamp(15rem, 20vw, 20rem); + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + position: relative; + transition: background-color 0.3s; + z-index: -1; + } + #meet-us-1021 .cs-picture img { + position: relative; + top: 0; + left: 0; + height: 100%; + width: 60%; + /* makes it behave like a background imahe */ + object-fit: contain; + /* makes the image position itself at the top of the parent */ + object-position: top; + transition: transform 0.6s, opacity 0.3s; + } +} +/* Tablet - 768px */ +@media only screen and (min-width: 48rem) { + #meet-us-1021 .cs-card-group { + display: grid; + grid-template-columns: repeat(12, 1fr); + width: 100%; + } + #meet-us-1021 .cs-item { + grid-column: span 4; + } +} + +/*-- -------------------------- --> + <--- Menu --> + <--- -------------------------- -*/ + +/* Mobile - 360px */ +@media only screen and (min-width: 0rem) { + #Hardware { + padding: var(--sectionPadding); + position: relative; + z-index: 1; + } + #Hardware:before { + /* static tiled pattern */ + content: ""; + height: 100%; + width: 100%; + opacity: 0.08; + background: url("https://csimg.nyc3.cdn.digitaloceanspaces.com/Food-Menu/static-pattern.png"); + background-size: auto; + background-position: center; + background-repeat: repeat; + display: block; + position: absolute; + top: 0; + left: 0; + z-index: -1; + } + #Hardware .cs-container { + width: 100%; + max-width: 80em; + margin: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + /* 48px - 64px */ + gap: clamp(3rem, 7vw, 4rem); + } + #Hardware .cs-content { + /* set text align to left if content needs to be left aligned */ + text-align: center; + width: 100%; + display: flex; + flex-direction: column; + /* centers content horizontally, set to flex-start to left align */ + align-items: center; + } + #Hardware .cs-wrapper { + /* 120px - 181px */ + width: clamp(7.5rem, 9vw, 11.3125rem); + margin-bottom: 0.75rem; + display: inline-flex; + justify-content: center; + align-items: center; + position: relative; + } + #Hardware .cs-wrapper:before { + /* left line */ + content: ""; + /* 90px - 155px */ + width: clamp(5.625rem, 4vw, 9.6875rem); + height: 1px; + /* 12px - 24px */ + margin-right: clamp(0.75rem, 2vw, 1.5rem); + background: #b4b2c7; + opacity: 1; + position: absolute; + display: block; + top: 50%; + right: 100%; + transform: translateY(-50%); + } + #Hardware .cs-wrapper:after { + /* right line */ + content: ""; + /* 90px - 155px */ + width: clamp(5.625rem, 4vw, 9.6875rem); + height: 1px; + /* 12px - 24px */ + margin-left: clamp(0.75rem, 2vw, 1.5rem); + background: #b4b2c7; + opacity: 1; + position: absolute; + display: block; + top: 50%; + left: 100%; + transform: translateY(-50%); + } + #Hardware .cs-wrapper img { + width: 100%; + } + + #Hardware .cs-button-solid { + font-size: 1rem; + /* 46px - 56px */ + line-height: clamp(2.875em, 5.5vw, 3.5em); + text-decoration: none; + font-weight: 700; + text-align: center; + margin: auto; + color: #fff; + min-width: 9.375rem; + padding: 0 2rem; + background-color: var(--primary); + display: inline-block; + position: relative; + z-index: 1; + /* prevents padding from adding to the width */ + box-sizing: border-box; + transition: color 0.3s; + } + #Hardware .cs-button-solid:before { + content: ""; + position: absolute; + height: 100%; + width: 0%; + background: #000; + opacity: 1; + top: 0; + left: 0; + z-index: -1; + transition: width 0.3s; + } + #Hardware .cs-button-solid:hover { + color: #fff; + } + #Hardware .cs-button-solid:hover:before { + width: 100%; + } + #Hardware .cs-card-group { + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + justify-content: center; + column-gap: 1.25rem; + /* 32px - 60px */ + row-gap: clamp(2rem, 6vw, 3.75rem); + } + #Hardware .cs-item { + list-style: none; + width: 100%; + max-width: 36.25rem; + margin: 0; + display: flex; + justify-content: space-between; + align-items: center; + /* 16px - 36px */ + gap: clamp(1rem, 3vw, 2.25rem); + } + #Hardware .cs-picture { + /* 76px - 160px */ + width: clamp(4.75rem, 14vw, 10rem); + height: clamp(4.75rem, 14vw, 10rem); + overflow: hidden; + display: block; + flex: none; + position: relative; + } + #Hardware .cs-picture img { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + /* makes it act like a background image */ + object-fit: scale-down; + } + #Hardware .cs-h3 { + /* 16px - 24px */ + font-size: clamp(1rem, 2vw, 1.5rem); + line-height: 1.2em; + font-weight: 700; + text-align: left; + /* 8px - 16px */ + margin: 0 0 clamp(0.5rem, 1.5vw, 1rem); + color: var(--headerColor); + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + gap: 0.75rem; } - - /*-- -------------------------- --> - <--- Meet The Team --> - <--- -------------------------- -*/ - - /* Mobile - 360px */ - @media only screen and (min-width: 0rem) { - #meet-us-1021 { - padding: var(--sectionPadding); - background-color: #1a1a1a; - } - #meet-us-1021 .cs-container { - width: 100%; - max-width: 80rem; - margin: auto; - position: relative; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - /* 48px - 64px */ - gap: clamp(3rem, 6vw, 4rem); - } - #meet-us-1021 .cs-container { - width: 100%; - max-width: 80rem; - margin: auto; - display: flex; - flex-direction: column; - align-items: center; - /* 48px - 64px */ - gap: clamp(3rem, 6vw, 4rem); - } - #meet-us-1021 .cs-content { - /* set text align to left if content needs to be left aligned */ - text-align: center; - width: 100%; - display: flex; - flex-direction: column; - /* centers content horizontally, set to flex-start to left align */ - align-items: center; - } - #meet-us-1021 .cs-graphic { - width: 100%; - max-width: 26.82352941rem; - margin: 0 0 1rem 0; - } - #meet-us-1021 .cs-title { - font-size: var(--headerFontSize); - font-weight: 900; - line-height: 1.2em; - text-align: inherit; - max-width: 43.75rem; - margin: 0 0 1rem 0; - color: var(--headerColor); - position: relative; - } - #meet-us-1021 .cs-text { - font-size: var(--bodyFontSize); - line-height: 1.5em; - text-align: inherit; - width: 100%; - max-width: 40.625rem; - margin: 0; - color: var(--bodyTextColor); - } - #meet-us-1021 .cs-title, - #meet-us-1021 .cs-text { - color: var(--bodyTextColorWhite); - } - #meet-us-1021 .cs-text { - opacity: 0.8; - } - #meet-us-1021 .cs-card-group { - width: 100%; - padding: 0; - margin: 0; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - flex-wrap: wrap; - /* 16px - 20px */ - gap: clamp(1rem, 2vw, 1.25rem); - } - #meet-us-1021 .cs-item { - list-style: none; - width: 100%; - max-width: 25.8125rem; - position: relative; - z-index: 1; - } - #meet-us-1021 .cs-item:hover .cs-picture { - background-color: #000; - } - #meet-us-1021 .cs-item:hover .cs-picture img { - transform: scale(1.1); - opacity: 0.6; - } - #meet-us-1021 .cs-info { - width: 85.5%; - /* 16px - 24px */ - padding: clamp(1rem, 2vw, 1.5rem); - margin: -3.75rem auto 0; - box-sizing: border-box; - background-color: #1a1a1a; - border-top: 4px solid var(--primary); - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - z-index: 10; - } - #meet-us-1021 .cs-name { - /* 16px - 25px */ - font-size: clamp(1rem, 2vw, 1.5625rem); - font-weight: 700; - line-height: 1.2em; - /* 4px - 8px */ - margin: 0 0 clamp(0.25rem, 1vw, 0.5rem); - color: var(--bodyTextColorWhite); - display: block; - transition: color 0.3s; - } - #meet-us-1021 .cs-job { - /* 14px - 16px */ - font-size: clamp(0.875rem, 1.5vw, 1rem); - line-height: 1.5em; - color: var(--bodyTextColorWhite); - opacity: 0.8; - display: block; - } - #meet-us-1021 .cs-picture { - width: 100%; - /* 246px - 500px */ - height: clamp(15.375rem, 40vw, 32.25rem); - overflow: hidden; - display: block; - position: relative; - transition: background-color 0.3s; - z-index: -1; - } - #meet-us-1021 .cs-picture img { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - /* makes it behave like a background imahe */ - object-fit: cover; - /* makes the image position itself at the top of the parent */ - object-position: top; - transition: - transform 0.6s, - opacity 0.3s; - } - } - /* Tablet - 768px */ - @media only screen and (min-width: 48rem) { - #meet-us-1021 .cs-card-group { - display: grid; - grid-template-columns: repeat(12, 1fr); - width: 100%; - } - #meet-us-1021 .cs-item { - grid-column: span 4; - } - } - - /*-- -------------------------- --> - <--- Menu --> - <--- -------------------------- -*/ - - /* Mobile - 360px */ - @media only screen and (min-width: 0rem) { - #Hardware { - padding: var(--sectionPadding); - position: relative; - z-index: 1; - } - #Hardware:before { - /* static tiled pattern */ - content: ""; - height: 100%; - width: 100%; - opacity: 0.08; - background: url("https://csimg.nyc3.cdn.digitaloceanspaces.com/Food-Menu/static-pattern.png"); - background-size: auto; - background-position: center; - background-repeat: repeat; - display: block; - position: absolute; - top: 0; - left: 0; - z-index: -1; - } - #Hardware .cs-container { - width: 100%; - max-width: 80em; - margin: auto; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - /* 48px - 64px */ - gap: clamp(3rem, 7vw, 4rem); - } - #Hardware .cs-content { - /* set text align to left if content needs to be left aligned */ - text-align: center; - width: 100%; - display: flex; - flex-direction: column; - /* centers content horizontally, set to flex-start to left align */ - align-items: center; - } - #Hardware .cs-wrapper { - /* 120px - 181px */ - width: clamp(7.5rem, 9vw, 11.3125rem); - margin-bottom: 0.75rem; - display: inline-flex; - justify-content: center; - align-items: center; - position: relative; - } - #Hardware .cs-wrapper:before { - /* left line */ - content: ""; - /* 90px - 155px */ - width: clamp(5.625rem, 4vw, 9.6875rem); - height: 1px; - /* 12px - 24px */ - margin-right: clamp(0.75rem, 2vw, 1.5rem); - background: #b4b2c7; - opacity: 1; - position: absolute; - display: block; - top: 50%; - right: 100%; - transform: translateY(-50%); - } - #Hardware .cs-wrapper:after { - /* right line */ - content: ""; - /* 90px - 155px */ - width: clamp(5.625rem, 4vw, 9.6875rem); - height: 1px; - /* 12px - 24px */ - margin-left: clamp(0.75rem, 2vw, 1.5rem); - background: #b4b2c7; - opacity: 1; - position: absolute; - display: block; - top: 50%; - left: 100%; - transform: translateY(-50%); - } - #Hardware .cs-wrapper img { - width: 100%; - } - - #Hardware .cs-button-solid { - font-size: 1rem; - /* 46px - 56px */ - line-height: clamp(2.875em, 5.5vw, 3.5em); - text-decoration: none; - font-weight: 700; - text-align: center; - margin: auto; - color: #fff; - min-width: 9.375rem; - padding: 0 2rem; - background-color: var(--primary); - display: inline-block; - position: relative; - z-index: 1; - /* prevents padding from adding to the width */ - box-sizing: border-box; - transition: color 0.3s; - } - #Hardware .cs-button-solid:before { - content: ""; - position: absolute; - height: 100%; - width: 0%; - background: #000; - opacity: 1; - top: 0; - left: 0; - z-index: -1; - transition: width 0.3s; - } - #Hardware .cs-button-solid:hover { - color: #fff; - } - #Hardware .cs-button-solid:hover:before { - width: 100%; - } - #Hardware .cs-card-group { - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - justify-content: center; - column-gap: 1.25rem; - /* 32px - 60px */ - row-gap: clamp(2rem, 6vw, 3.75rem); - } - #Hardware .cs-item { - list-style: none; - width: 100%; - max-width: 36.25rem; - margin: 0; - display: flex; - justify-content: space-between; - align-items: center; - /* 16px - 36px */ - gap: clamp(1rem, 3vw, 2.25rem); - } - #Hardware .cs-picture { - /* 76px - 160px */ - width: clamp(4.75rem, 14vw, 10rem); - height: clamp(4.75rem, 14vw, 10rem); - overflow: hidden; - display: block; - flex: none; - position: relative; - } - #Hardware .cs-picture img { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - /* makes it act like a background image */ - object-fit:scale-down; - } - #Hardware .cs-h3 { - /* 16px - 24px */ - font-size: clamp(1rem, 2vw, 1.5rem); - line-height: 1.2em; - font-weight: 700; - text-align: left; - /* 8px - 16px */ - margin: 0 0 clamp(0.5rem, 1.5vw, 1rem); - color: var(--headerColor); - display: flex; - justify-content: space-between; - align-items: center; - position: relative; - gap: 0.75rem; - } - /* #Hardware .cs-h3:after { + /* #Hardware .cs-h3:after { content: ""; width: 50%; height: 1px; @@ -2142,269 +2147,463 @@ body { display: block; order: 1; } */ - #Hardware .cs-name { - max-width: 13.75rem; - flex: none; - } - #Hardware .cs-price { - /* 16px - 25px */ - font-size: clamp(1rem, 2vw, 1.5625rem); - /* 28px - 46px */ - line-height: clamp(1.75rem, 4vw, 2.875rem); - font-weight: 700; - /* 8px - 12px */ - padding: 0 clamp(0.5rem, 1vw, 0.75rem); - color: var(--primary); - background-color: #fff; - border-radius: 5rem; - border: 1px solid #b4b2c7; - display: flex; - align-items: center; - order: 3; - } - #Hardware .cs-item-p { - /* 14px - 16px */ - font-size: clamp(0.875rem, 2vw, 1rem); - line-height: 1.5em; - text-align: left; - margin: 0; - color: var(--bodyTextColor); - } - } - /* Tablet - 768px */ - @media only screen and (min-width: 48rem) { - #Hardware .cs-card-group { - flex-direction: row; - justify-content: space-between; - flex-wrap: wrap; - column-gap: 1.25rem; - } - #Hardware .cs-item { - width: 48.5%; - } - } - /*-- -------------------------- --> + #Hardware .cs-name { + max-width: 75%; + flex: none; + } + #Hardware .cs-price { + /* 16px - 25px */ + font-size: clamp(1rem, 2vw, 1.5625rem); + /* 28px - 46px */ + line-height: clamp(1.75rem, 4vw, 2.875rem); + font-weight: 700; + /* 8px - 12px */ + padding: 0 clamp(0.5rem, 1vw, 0.75rem); + color: var(--primary); + background-color: #fff; + border-radius: 5rem; + border: 1px solid #b4b2c7; + display: flex; + align-items: center; + order: 3; + } + #Hardware .cs-item-p { + /* 14px - 16px */ + font-size: clamp(0.875rem, 2vw, 1rem); + line-height: 1.5em; + text-align: left; + margin: 0; + color: var(--bodyTextColor); + } +} +/* Tablet - 768px */ +@media only screen and (min-width: 48rem) { + #Hardware .cs-card-group { + flex-direction: row; + justify-content: space-between; + flex-wrap: wrap; + column-gap: 1.25rem; + } + #Hardware .cs-item { + width: 48.5%; + } +} +/*-- -------------------------- --> <--- Footer --> <--- -------------------------- -*/ - /* Mobile - 360px */ - @media only screen and (min-width: 0rem) { - #cs-footer-274 { - padding: var(--sectionPadding); - /* Navigation Links */ - } - #cs-footer-274 .cs-container { - width: 100%; - /* reset on tablet */ - max-width: 34.375rem; - margin: auto; - display: flex; - justify-content: flex-start; - align-items: flex-start; - flex-wrap: wrap; - column-gap: 5.5rem; - row-gap: 2rem; - } - #cs-footer-274 .cs-logo-group { - /* takes up all the space, lets the other ul's wrap below it */ - width: 100%; - position: relative; - } - #cs-footer-274 .cs-logo-group { - /* 44px - 52px */ - margin-bottom: clamp(2.75rem, 6.8vw, 3.25rem); - } - #cs-footer-274 .cs-logo { - /* 210px - 240px */ - width: clamp(13.125rem, 8vw, 15rem); - height: auto; - display: block; - } - #cs-footer-274 .cs-logo-img { - width: 100%; - height: auto; - } - #cs-footer-274 .cs-social { - display: inline-flex; - flex-direction: column; - justify-content: flex-start; - gap: 0.75rem; - position: absolute; - top: 0; - right: 0; - } - #cs-footer-274 .cs-social-link { - width: 1.5rem; - height: 1.5rem; - background-color: #4e4b66; - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - position: relative; - z-index: 1; - transition: - transform 0.3s, - background-color 0.3s; - } - #cs-footer-274 .cs-social-link:hover { - background-color: var(--primary); - transform: translateY(-0.1875rem); - } - #cs-footer-274 .cs-social-img { - height: 0.8125rem; - width: auto; - display: block; - } - #cs-footer-274 .cs-nav { - padding: 0; - margin: 0; - } - #cs-footer-274 .cs-nav-li { - list-style: none; - margin: 0; - color: var(--bodyTextColor); - } - #cs-footer-274 .cs-header { - font-size: 1rem; - line-height: 1.5em; - font-weight: 700; - /* 16px - 20px */ - margin-bottom: clamp(1rem, 2.7vw, 1.25rem); - color: var(--bodyTextColor); - position: relative; - display: block; - } - #cs-footer-274 .cs-nav-link { - font-size: 1rem; - text-decoration: none; - line-height: 1.5em; - color: var(--bodyTextColor); - position: relative; - } - #cs-footer-274 .cs-nav-link:before { - /* underline */ - content: ""; - width: 0%; - height: 0.125rem; - background: var(--bodyTextColor); - opacity: 1; - position: absolute; - display: block; - bottom: -0.125rem; - left: 0; - transition: width 0.3s; - } - #cs-footer-274 .cs-nav-link:hover:before { - width: 100%; - } - } - /* Tablet - 768px */ - @media only screen and (min-width: 48rem) { - #cs-footer-274 .cs-container { - max-width: 80rem; - row-gap: 0; - /* 44px - 88px */ - column-gap: clamp(2.75rem, calc(6%), 5.5rem); - } - #cs-footer-274 .cs-logo-group { - display: flex; - justify-content: space-between; - align-items: flex-start; - } - #cs-footer-274 .cs-social { - flex-direction: row; - position: relative; - top: auto; - right: auto; - } - } - /* Small Desktop - 1024px */ - @media only screen and (min-width: 64rem) { - #cs-footer-274 .cs-container { - justify-content: flex-end; - } - #cs-footer-274 .cs-logo-group { - width: auto; - margin: 0; - /* pushes the rest of the content to the right in a flexbox */ - margin-right: auto; - flex-direction: column; - } - #cs-footer-274 .cs-logo-img { - margin-bottom: 2.75rem; - } - #cs-footer-274 .cs-nav { - margin-top: 0.75rem; - } +/* Mobile - 360px */ +@media only screen and (min-width: 0rem) { + #cs-footer-274 { + padding: var(--sectionPadding); + /* Navigation Links */ + } + #cs-footer-274 .cs-container { + width: 100%; + /* reset on tablet */ + max-width: 34.375rem; + margin: auto; + display: flex; + justify-content: flex-start; + align-items: flex-start; + flex-wrap: wrap; + column-gap: 5.5rem; + row-gap: 2rem; + } + #cs-footer-274 .cs-logo-group { + /* takes up all the space, lets the other ul's wrap below it */ + width: 100%; + position: relative; + } + #cs-footer-274 .cs-logo-group { + /* 44px - 52px */ + margin-bottom: clamp(2.75rem, 6.8vw, 3.25rem); + } + #cs-footer-274 .cs-logo { + /* 210px - 240px */ + width: clamp(13.125rem, 8vw, 15rem); + height: auto; + display: block; + } + #cs-footer-274 .cs-logo-img { + width: 100%; + height: auto; + } + #cs-footer-274 .cs-social { + display: inline-flex; + flex-direction: column; + justify-content: flex-start; + gap: 0.75rem; + position: absolute; + top: 0; + right: 0; + } + #cs-footer-274 .cs-social-link { + width: 1.5rem; + height: 1.5rem; + background-color: #4e4b66; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + position: relative; + z-index: 1; + transition: transform 0.3s, background-color 0.3s; + } + #cs-footer-274 .cs-social-link:hover { + background-color: var(--primary); + transform: translateY(-0.1875rem); + } + #cs-footer-274 .cs-social-img { + height: 0.8125rem; + width: auto; + display: block; + } + #cs-footer-274 .cs-nav { + padding: 0; + margin: 0; + } + #cs-footer-274 .cs-nav-li { + list-style: none; + margin: 0; + color: var(--bodyTextColor); + } + #cs-footer-274 .cs-header { + font-size: 1rem; + line-height: 1.5em; + font-weight: 700; + /* 16px - 20px */ + margin-bottom: clamp(1rem, 2.7vw, 1.25rem); + color: var(--bodyTextColor); + position: relative; + display: block; + } + #cs-footer-274 .cs-nav-link { + font-size: 1rem; + text-decoration: none; + line-height: 1.5em; + color: var(--bodyTextColor); + position: relative; + } + #cs-footer-274 .cs-nav-link:before { + /* underline */ + content: ""; + width: 0%; + height: 0.125rem; + background: var(--bodyTextColor); + opacity: 1; + position: absolute; + display: block; + bottom: -0.125rem; + left: 0; + transition: width 0.3s; + } + #cs-footer-274 .cs-nav-link:hover:before { + width: 100%; + } +} +/* Tablet - 768px */ +@media only screen and (min-width: 48rem) { + #cs-footer-274 .cs-container { + max-width: 80rem; + row-gap: 0; + /* 44px - 88px */ + column-gap: clamp(2.75rem, calc(6%), 5.5rem); + } + #cs-footer-274 .cs-logo-group { + display: flex; + justify-content: space-between; + align-items: flex-start; } + #cs-footer-274 .cs-social { + flex-direction: row; + position: relative; + top: auto; + right: auto; + } +} +/* Small Desktop - 1024px */ +@media only screen and (min-width: 64rem) { + #cs-footer-274 .cs-container { + justify-content: flex-end; + } + #cs-footer-274 .cs-logo-group { + width: auto; + margin: 0; + /* pushes the rest of the content to the right in a flexbox */ + margin-right: auto; + flex-direction: column; + } + #cs-footer-274 .cs-logo-img { + margin-bottom: 2.75rem; + } + #cs-footer-274 .cs-nav { + margin-top: 0.75rem; + } +} - /*-- -------------------------- --> +/*-- -------------------------- --> <--- Solution Architecture --> <--- -------------------------- -*/ - @media only screen and (min-width: 0rem) { - #SolutionArchitecture { - padding: var(--sectionPadding); - overflow: hidden; - position: relative; - z-index: 1; - } - #SolutionArchitecture:before { - /* static tiled pattern */ - content: ""; - height: 100%; - width: 100%; - opacity: 0.08; - background: url("https://csimg.nyc3.cdn.digitaloceanspaces.com/Food-Menu/static-pattern.png"); - background-size: auto; - background-position: center; - background-repeat: repeat; - display: block; - position: absolute; - top: 0; - left: 0; - z-index: -1; - } - #SolutionArchitecture .cs-container { - width: 100%; - max-width: 80em; - margin: auto; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - /* 48px - 64px */ - gap: clamp(3rem, 7vw, 4rem); - } +@media only screen and (min-width: 0rem) { + #SolutionArchitecture { + padding: var(--sectionPadding); + overflow: hidden; + position: relative; + z-index: 1; + } + #SolutionArchitecture:before { + /* static tiled pattern */ + content: ""; + height: 100%; + width: 100%; + opacity: 0.08; + background: url("https://csimg.nyc3.cdn.digitaloceanspaces.com/Food-Menu/static-pattern.png"); + background-size: auto; + background-position: center; + background-repeat: repeat; + display: block; + position: absolute; + top: 0; + left: 0; + z-index: -1; + } + #SolutionArchitecture .cs-container { + width: 100%; + max-width: 80em; + margin: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + /* 48px - 64px */ + gap: clamp(3rem, 7vw, 4rem); + } + + /* Style for cs-picture in Solution Architecture section */ + #SolutionArchitecture .cs-picture { + width: 100%; + max-width: 80%; + height: auto; + margin: 0 auto; + display: block; + } + #SolutionArchitecture .cs-picture img { + /* position: absolute; */ + top: 0; + left: 0; + height: 100%; + width: 100%; + /* makes it act like a background image */ + object-fit: scale-down; + animation: slideUp 800ms ease-out; + } +} +/* ================================================================= + Video Gallery +================================================================= */ - /* Style for cs-picture in Solution Architecture section */ - #SolutionArchitecture .cs-picture { - width: 100%; - max-width:80%; - height: auto; - margin: 0 auto; - display: block; - } - #SolutionArchitecture .cs-picture img { - /* position: absolute; */ - top: 0; - left: 0; - height: 100%; - width: 100%; - /* makes it act like a background image */ - object-fit:scale-down; - animation: slideUp 800ms ease-out; - } +.section-header { + display: flex; + justify-content: center; + align-items: center; + padding: 10px; +} + +.section-header h1 { + font: 400 32px "Montserrat", sans-serif; + text-transform: uppercase; +} +#video-gallery { + padding: var(--sectionPadding); + position: relative; + z-index: 1; +} +#video-gallery:before { + /* static tiled pattern */ + content: ""; + height: 100%; + width: 100%; + opacity: 0.08; + background: url("https://csimg.nyc3.cdn.digitaloceanspaces.com/Food-Menu/static-pattern.png"); + background-size: auto; + background-position: center; + background-repeat: repeat; + display: block; + position: absolute; + top: 0; + left: 0; + z-index: -1; +} + +.video-gallery { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(370px, 1fr)); + grid-gap: 15px; + max-width: 1100px; + padding: 15px; + margin: 0 auto; + box-sizing: border-box; + align-items: center; +} + +.video-gallery .gallery-item { + position: relative; + width:fit-content; + align-items: center; + height: 300px; + background: #000; + cursor: pointer; + overflow: hidden; +} + +.video-gallery .gallery-item img { + position: relative; + display: block; + width: 115%; + height: 300px; + object-fit: cover; + opacity: 0.5; + /*transition: opacity .35s, transform .35s;*/ + transition: all 350ms ease-in-out; + transform: translate3d(-23px, 0, 0); + /*backface-visibility: hidden;*/ +} + +.north-cascades-img { + object-position: 50% 30%; +} + +.video-gallery .gallery-item .gallery-item-caption { + padding: 32px; + font-size: 1em; + color: #fff; + text-transform: uppercase; +} + +.video-gallery .gallery-item .gallery-item-caption, +.video-gallery .gallery-item .gallery-item-caption > a { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.video-gallery .gallery-item h2 { + font-weight: 300; + overflow: hidden; + padding: 12px 0; +} + +.video-gallery .gallery-item h2, +.video-gallery .gallery-item p { + position: relative; + margin: 0; + z-index: 1; + pointer-events: none; +} + +.video-gallery .gallery-item p { + letter-spacing: 1px; + font-size: 12px; + padding: 12px 0; + opacity: 0; + transition: opacity 0.35s, transform 0.35s; + transform: translate3d(10%, 0, 0); +} + +.video-gallery .gallery-item:hover img { + opacity: 0.3; + transform: translate3d(0, 0, 0); +} + +.video-gallery .gallery-item .gallery-item-caption { + text-align: left; +} + +.video-gallery .gallery-item h2::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + width: 15%; + height: 1px; + background: #fff; + transition: transform 0.3s; + transform: translate3d(-100%, 0, 0); +} + +.video-gallery .gallery-item:hover h2::after { + transform: translate3d(0, 0, 0); +} + +.video-gallery .gallery-item:hover p { + opacity: 1; + transform: translate3d(0, 0, 0); +} + +@media screen and (max-width: 784px) { + .video-gallery { + width: 100%; + padding: 15px; } - - + .video-gallery .gallery-item { + width: 95%; + margin: 0 auto; + width: 100%; + } +} + +/* Add modal styles as needed */ +.modal { + display: none; + position: fixed; + z-index: 1; + padding-top: 50px; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgb(0, 0, 0); + background-color: rgba(0, 0, 0, 0.9); +} +.modal-content { + margin: auto; + display: block; + max-width: 80%; + max-height: 80%; +} - +.close { + position: absolute; + top: 15px; + right: 35px; + color: #f1f1f1; + font-size: 40px; + font-weight: bold; + cursor: pointer; +} +.close:hover, +.close:focus { + color: #bbb; + text-decoration: none; + cursor: pointer; +} - - +.grid-picture { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 4em; +} - \ No newline at end of file +@media (max-width: 768px) { + .grid-picture { + grid-template-columns: 1fr; + grid-auto-rows: 1fr; + } +} \ No newline at end of file diff --git a/docs/styles2.css b/docs/styles2.css new file mode 100644 index 00000000..3191e655 --- /dev/null +++ b/docs/styles2.css @@ -0,0 +1,535 @@ +/*-- -------------------------- --> +<--- Hero --> +<--- -------------------------- -*/ +/* Mobile - 360px */ +:root { + /* Add these styles to your global stylesheet, which is used across all site pages. You only need to do this once. All elements in the library derive their variables and base styles from this central sheet, simplifying site-wide edits. For instance, if you want to modify how your h2's appear across the site, you just update it once in the global styles, and the changes apply everywhere. */ + --primary: #980c0c; + --primaryLight: #ffba43; + --secondary: #ffba43; + --secondaryLight: #ffba43; + --headerColor: #1a1a1a; + --bodyTextColor: #4e4b66; + --bodyTextColorWhite: #fafbfc; + /* 13px - 16px */ + --topperFontSize: clamp(0.8125rem, 1.6vw, 1rem); + /* 31px - 49px */ + --headerFontSize: clamp(1.9375rem, 3.9vw, 3.0625rem); + --bodyFontSize: 1rem; + /* 60px - 100px top and bottom */ + --sectionPadding: clamp(3.75rem, 7.82vw, 6.25rem) 1rem; + } + + body { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + *, *:before, *:after { + /* prevents padding from affecting height and width */ + box-sizing: border-box; + } + .cs-topper { + font-family: 'Courier New', Courier, monospace; + font-size: var(--topperFontSize); + line-height: 1.2em; + text-transform: uppercase; + text-align: inherit; + letter-spacing: .1em; + font-weight: 700; + color: var(--primary); + margin-bottom: 0.25rem; + display: block; + } + + .cs-title { + font-size: var(--headerFontSize); + font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif; + font-weight: 900; + line-height: 1.2em; + text-align: inherit; + max-width: fit-content; + margin: 0 0 1rem 0; + color: var(--headerColor); + position: relative; + } + + .cs-text { + font-family:Cambria, Cochin, Georgia, Times, 'Times New Roman', serif; + font-size: var(--bodyFontSize); + line-height: 1.5em; + text-align: inherit; + width: 100%; + max-width: 40.625rem; + margin: 0; + color: var(--bodyTextColor); + } + @keyframes slideUp { + from { + transform: translateY(80%); + } + to { + transform: translateY(0); + } + } + @keyframes slideDown { + from { + transform: translateY(20%); + } + to { + transform: translateY(100%); + } + } + /*-- -------------------------- --> +<--- Mobile Navigation --> +<--- -------------------------- -*/ +/* Mobile - 1023px */ +@media only screen and (max-width: 63.9375rem) { + body.cs-open { + overflow: hidden; + } + body.scroll #cs-navigation { + width: 100%; + max-width: 100%; + top: 0; + } + body.scroll #cs-navigation:before { + border-radius: 0; + } + body.scroll #cs-navigation .cs-ul-wrapper { + top: 100%; + } + #cs-navigation { + width: 94%; + max-width: 80rem; + /* prevents padding from affecting height and width */ + box-sizing: border-box; + /* 12px - 24px */ + padding: clamp(0.75rem, 2vw, 1.5rem); + /* 12px - 24px */ + border-radius: clamp(0.75rem, 2vw, 1.5rem); + position: fixed; + top: 2rem; + left: 50%; + z-index: 10000; + transform: translateX(-50%); + transition: top 0.3s, border-radius 0.3s, width 0.3s, max-width 0.3s; + } + #cs-navigation:before { + /* background color */ + content: ""; + width: 100%; + height: 100%; + background: #302d2d; + box-shadow: rgba(248, 246, 246, 0.2) 0px 8px 24px; + opacity: 1; + /* 12px - 24px */ + border-radius: clamp(0.75rem, 2vw, 1.5rem); + display: block; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + transition: transform 0.2s, border-radius 0.3s ease-in-out; + } + #cs-navigation.cs-active:before { + transform: translateX(-50%) scale(1.03); + } + #cs-navigation.cs-active .cs-toggle { + transform: rotate(180deg); + } + #cs-navigation.cs-active .cs-ul-wrapper { + transform: scaleY(1); + transition-delay: 0.15s; + } + #cs-navigation.cs-active .cs-li { + opacity: 1; + transform: translateY(0); + } + #cs-navigation .cs-container { + width: 100%; + display: flex; + justify-content: flex-end; + align-items: center; + gap: 1.5rem; + } + #cs-navigation .cs-logo { + width: auto; + max-width: 12.5rem; + height: 100%; + margin: 0 auto 0 0; + /* prevents padding from affecting height and width */ + box-sizing: border-box; + padding: 0; + display: flex; + justify-content: flex-start; + align-items: center; + z-index: 10; + } + #cs-navigation .cs-logo img { + width: 100%; + height: 100%; + /* ensures the image never overflows the container. It stays contained within it's width and height and expands to fill it then stops once it reaches an edge */ + object-fit: contain; + } + #cs-navigation .cs-toggle { + width: 3.5rem; + height: 3.5rem; + margin: 0 0 0 auto; + background-color: #1a1a1a; + border: none; + border-radius: 0.25rem; + display: flex; + justify-content: center; + align-items: center; + position: relative; + z-index: 10; + transition: transform 0.6s; + } + #cs-navigation .cs-nav { + /* sends it to the right in the 3rd position */ + order: 3; + } + #cs-navigation .cs-contact-group { + display: none; + position: relative; + z-index: 10; + } + #cs-navigation .cs-phone { + font-size: 1rem; + line-height: 1.5em; + text-decoration: none; + margin: 0; + color: var(--headerColor); + display: flex; + justify-content: flex-start; + align-items: center; + gap: 0.5rem; + transition: opacity 0.3s, color 0.3s; + } + #cs-navigation .cs-phone-icon { + width: 1.5rem; + height: auto; + display: block; + } + #cs-navigation .cs-social { + display: none; + } + #cs-navigation .cs-active .cs-line1 { + top: 50%; + transform: translate(-50%, -50%) rotate(225deg); + } + #cs-navigation .cs-active .cs-line2 { + top: 50%; + transform: translate(-50%, -50%) translateY(0) rotate(-225deg); + transform-origin: center; + } + #cs-navigation .cs-active .cs-line3 { + opacity: 0; + bottom: 100%; + } + #cs-navigation .cs-box { + /* 24px - 28px */ + width: clamp(1.5rem, 2vw, 1.75rem); + height: 1rem; + position: relative; + } + #cs-navigation .cs-line { + width: 100%; + height: 2px; + background-color: #fafbfc; + border-radius: 2px; + position: absolute; + left: 50%; + transform: translateX(-50%); + } + #cs-navigation .cs-line1 { + top: 0; + transition: transform 0.5s, top 0.3s, left 0.3s; + animation-duration: 0.7s; + animation-timing-function: ease; + animation-direction: normal; + animation-fill-mode: forwards; + transform-origin: center; + } + #cs-navigation .cs-line2 { + top: 50%; + transform: translateX(-50%) translateY(-50%); + transition: top 0.3s, left 0.3s, transform 0.5s; + animation-duration: 0.7s; + animation-timing-function: ease; + animation-direction: normal; + animation-fill-mode: forwards; + } + #cs-navigation .cs-line3 { + bottom: 0; + transition: bottom 0.3s, opacity 0.3s; + } + #cs-navigation .cs-ul-wrapper { + width: 100%; + height: auto; + padding-bottom: 2.4em; + background-color: #fff; + border-radius: 0 0 1.5rem 1.5rem; + position: absolute; + top: 85%; + left: 0; + z-index: -1; + overflow: hidden; + transform: scaleY(0); + transition: transform 0.4s; + transform-origin: top; + } + #cs-navigation .cs-ul { + width: 100%; + height: auto; + max-height: 65vh; + margin: 0; + padding: 4rem 0 0 0; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + gap: 1.25rem; + overflow: auto; + } + #cs-navigation .cs-li { + text-align: center; + list-style: none; + width: 100%; + margin-right: 0; + opacity: 0; + /* transition from these values */ + transform: translateY(-70/16rem); + transition: transform 0.6s, opacity 0.9s; + } + #cs-navigation .cs-li:nth-of-type(1) { + transition-delay: 0.05s; + } + #cs-navigation .cs-li:nth-of-type(2) { + transition-delay: 0.1s; + } + #cs-navigation .cs-li:nth-of-type(3) { + transition-delay: 0.15s; + } + #cs-navigation .cs-li:nth-of-type(4) { + transition-delay: 0.2s; + } + #cs-navigation .cs-li:nth-of-type(5) { + transition-delay: 0.25s; + } + #cs-navigation .cs-li:nth-of-type(6) { + transition-delay: 0.3s; + } + #cs-navigation .cs-li:nth-of-type(7) { + transition-delay: 0.35s; + } + #cs-navigation .cs-li:nth-of-type(8) { + transition-delay: 0.4s; + } + #cs-navigation .cs-li:nth-of-type(9) { + transition-delay: 0.45s; + } + #cs-navigation .cs-li:nth-of-type(10) { + transition-delay: 0.5s; + } + #cs-navigation .cs-li:nth-of-type(11) { + transition-delay: 0.55s; + } + #cs-navigation .cs-li:nth-of-type(12) { + transition-delay: 0.6s; + } + #cs-navigation .cs-li:nth-of-type(13) { + transition-delay: 0.65s; + } + #cs-navigation .cs-li-link { + /* 16px - 24px */ + font-size: clamp(1rem, 2.5vw, 1.5rem); + line-height: 1.2em; + text-decoration: none; + margin: 0; + color: var(--headerColor); + display: inline-block; + position: relative; + } + #cs-navigation .cs-li-link.cs-active { + color: var(--primary); + } + #cs-navigation .cs-li-link:hover { + color: var(--primary); + } + #cs-navigation .cs-button-solid { + display: none; + } +} +/* Tablet - 768px */ +@media only screen and (min-width: 48rem) { + #cs-navigation .cs-contact-group { + display: block; + } +} +/*-- -------------------------- --> +<--- Navigation Dropdown --> +<--- -------------------------- -*/ +/* Mobile - 1023px */ +@media only screen and (max-width: 63.9375rem) { + #cs-navigation .cs-li { + text-align: center; + width: 100%; + display: block; + } + #cs-navigation .cs-dropdown { + color: var(--bodyTextColorWhite); + position: relative; + } + #cs-navigation .cs-dropdown.cs-active .cs-drop-ul { + height: auto; + margin: 0.75rem 0 0 0; + padding: 0.75rem 0; + opacity: 1; + visibility: visible; + } + #cs-navigation .cs-dropdown.cs-active .cs-drop-link { + opacity: 1; + } + #cs-navigation .cs-dropdown .cs-li-link { + position: relative; + transition: opacity 0.3s; + } + #cs-navigation .cs-drop-icon { + width: 0.9375rem; + height: auto; + position: absolute; + top: 50%; + right: -1.25rem; + transform: translateY(-50%); + } + #cs-navigation .cs-drop-ul { + width: 100%; + height: 0; + margin: 0; + padding: 0; + background-color: var(--primary); + opacity: 0; + display: flex; + visibility: hidden; + flex-direction: column; + justify-content: flex-start; + align-items: center; + gap: 0.75rem; + overflow: hidden; + transition: padding 0.3s, margin 0.3s, height 0.3s, opacity 0.3s, visibility 0.3s; + } + #cs-navigation .cs-drop-li { + list-style: none; + } + #cs-navigation .cs-li-link.cs-drop-link { + /* 14px - 16px */ + font-size: clamp(0.875rem, 2vw, 1.25rem); + color: #fff; + } +} +/* Desktop - 1024px */ +@media only screen and (min-width: 64rem) { + #cs-navigation .cs-dropdown { + position: relative; + } + #cs-navigation .cs-dropdown:hover { + cursor: pointer; + } + #cs-navigation .cs-dropdown:hover .cs-drop-ul { + opacity: 1; + visibility: visible; + transform: scaleY(1); + } + #cs-navigation .cs-dropdown:hover .cs-drop-li { + opacity: 1; + transform: translateY(0); + } + #cs-navigation .cs-drop-icon { + width: 0.9375rem; + height: auto; + display: inline-block; + } + #cs-navigation .cs-drop-ul { + min-width: 12.5rem; + margin: 0; + padding: 0; + background-color: #fff; + box-shadow: inset rgba(149, 157, 165, 0.1) 0px 8px 10px; + opacity: 0; + border-bottom: 5px solid var(--primary); + border-radius: 0 0 1.5rem 1.5rem; + visibility: hidden; + /* if you have 8 or more links in your dropdown nav, uncomment the columns property to make the list into 2 even columns. Change it to 3 or 4 if you need extra columns. Then remove the transition delays on the cs-drop-li so they don't have weird scattered animations */ + position: absolute; + top: 100%; + z-index: -100; + overflow: hidden; + transform: scaleY(0); + transition: transform 0.3s, visibility 0.3s, opacity 0.3s; + transform-origin: top; + } + #cs-navigation .cs-drop-li { + font-size: 1rem; + text-decoration: none; + list-style: none; + width: 100%; + height: auto; + opacity: 0; + display: block; + transform: translateY(-0.625rem); + transition: opacity 0.6s, transform 0.6s; + } + #cs-navigation .cs-drop-li:nth-of-type(1) { + transition-delay: 0.05s; + } + #cs-navigation .cs-drop-li:nth-of-type(2) { + transition-delay: 0.1s; + } + #cs-navigation .cs-drop-li:nth-of-type(3) { + transition-delay: 0.15s; + } + #cs-navigation .cs-drop-li:nth-of-type(4) { + transition-delay: 0.2s; + } + #cs-navigation .cs-drop-li:nth-of-type(5) { + transition-delay: 0.25s; + } + #cs-navigation .cs-drop-li:nth-of-type(6) { + transition-delay: 0.3s; + } + #cs-navigation .cs-drop-li:nth-of-type(7) { + transition-delay: 0.35s; + } + #cs-navigation .cs-drop-li:nth-of-type(8) { + transition-delay: 0.4s; + } + #cs-navigation .cs-drop-li:nth-of-type(9) { + transition-delay: 0.45s; + } + #cs-navigation .cs-li-link.cs-drop-link { + font-size: 1rem; + line-height: 1.5em; + text-transform: capitalize; + text-decoration: none; + white-space: nowrap; + width: 100%; + /* prevents padding from affecting height and width */ + box-sizing: border-box; + padding: 0.75rem; + color: var(--headerColor); + display: block; + transition: color 0.3s, background-color 0.3s; + } + #cs-navigation .cs-li-link.cs-drop-link:hover { + color: var(--bodyTextColorWhite); + background-color: var(--primary); + } + #cs-navigation .cs-li-link.cs-drop-link:before { + display: none; + } +} + + diff --git a/docs/video-gallery.css b/docs/video-gallery.css new file mode 100644 index 00000000..8140c235 --- /dev/null +++ b/docs/video-gallery.css @@ -0,0 +1,833 @@ +/*---------------------------------------------------------------- + End points + --------------------------------------------------------------- */ + @import url("https://fonts.googleapis.com/css?family=Roboto:100|Ovo"); + + #end-points { + padding: var(--sectionPadding); + overflow: hidden; + position: relative; + z-index: 1; + } + @keyframes floatAnimation { + 0% { + transform: translateY(0); + } + 50% { + transform: translateY(-2em); + } + 100% { + transform: translateY(0); + } + } + @keyframes floatAnimation2 { + 0% { + transform: translateY(0); + } + 50% { + transform: translateY(-1em); + } + 100% { + transform: translateY(0); + } + } + #end-points:before { + /* static tiled pattern */ + content: ""; + height: 100%; + width: 100%; + opacity: 0.08; + background: url("https://csimg.nyc3.cdn.digitaloceanspaces.com/Food-Menu/static-pattern.png"); + background-size: auto; + background-position: center; + background-repeat: repeat; + display: block; + position: absolute; + top: 0; + left: 0; + z-index: -1; + } + #end-points img { + width: 100%; + height: auto; + } + + #end-points p:first-of-type { + margin-top: 0; + } + + #end-points h2.cs-title { + margin-top: 0; + text-align: center; + justify-content: center; + max-width: 100%; + } + + .clearfix::after { + clear: both; + content: ""; + display: block; + } + + .wrapper { + max-width: 960px; + margin-left: auto; + margin-right: auto; + } + .wrapper:after { + content: " "; + display: block; + clear: both; + } + + .btn, + button.btn, + [type="button"].btn, + [type="reset"].btn, + [type="submit"].btn { + background-color: white; + border: 1px solid silver; + color: silver; + } + .btn:hover, + .btn:focus { + background-color: black; + border: 1px solid white; + } + + .btn, + button.btn, + [type="button"].btn, + [type="reset"].btn, + [type="submit"].btn { + padding: 9px 35px; + display: inline-block; + letter-spacing: 0.1em; + text-transform: uppercase; + text-decoration: none; + text-align: center; + white-space: nowrap; + cursor: pointer; + transition: background-color 1.2s; + } + .btn > span, + [type="button"].btn > span, + [type="reset"].btn > span, + [type="submit"].btn > span { + line-height: 1.8; + } + + .btn--inline { + vertical-align: baseline; + font-size: inherit; + line-height: inherit; + height: auto; + padding: 0.1em 0.5em; + } + + .btn--small { + font-size: 1em; + } + + .btn--large { + font-size: 2rem; + } + + .btn--huge { + font-size: 3rem; + } + + .btn--full { + width: 100%; + } + + .btn--rounded { + border-radius: 5rem; + } + + /** + medium + xx-small + x-small|small + large + x-large + xx-large + smaller + larger + + */ + .text-center { + text-align: center; + } + + .text-left { + text-align: left; + } + + .text-right { + text-align: right; + } + + /** # Dummy images + + Random images from https://picsum.photos/, classified by tags + + Usage: + ``` + .dummy-background{ + @include dummy-image('800/450', sea); + } + ``` + + $dummy-img-provider-url {string} - image provider url + $dummy-images {map} - list images id's by categories + + @link - https://picsum.photos/ + + Styleguide tools.dummy.images + */ + .img1 { + background-image: url(https://unsplash.it/800?image=1070); + } + + /* .img2 { + background-image: url(https://unsplash.it/800?image=1060); + } */ + + .img3 { + background-image: url(https://unsplash.it/800?image=1059); + } + + .img4 { + background-image: url(https://unsplash.it/800?image=1025); + } + + .img5 { + background-image: url(https://unsplash.it/800?image=998); + } + + .bg1 { + background: url(http://subtlepatterns2015.subtlepatterns.netdna-cdn.com/patterns/low_contrast_linen.png) + repeat; + } + + /** # Cards + + https://www.nngroup.com/articles/cards-component/ + + Definition: + A card is container for a few short, related pieces of information. + It roughly resembles a playing card in size and shape, and is intended as a linked, + short representation of a conceptual unit. + + + Card actions : https://material.io/guidelines/components/cards.html#cards-actions + + + ## Cards Bootstrap V4 revisited + + + Markup: + <div class="card" style="width:300px;"> + <img class="card-thumbnail top" width="300" /> + <div class="card-content"> + <h4 class="title">Card title</h4> + <p class="subtitle"></p> + <p class="description">Some quick example text to build on the card title and make up the bulk of the card's content.</p> + <a href="#" class="btn btn-primary">Button</a> + </div> + </div> + <h3>Card with Caps : Header & Footer</h3> + <div class="card" style="width:300px;"> + <div class="card-header"> + Featured + </div> + <div class="card-content"> + <h4 class="title">Card title</h4> + <p class="subtitle"></p> + <p class="description">Some quick example text to build on the card title and make up the bulk of the card's content.</p> + <a href="#" class="btn btn-primary">Button</a> + </div> + <div class="card-footer"> + <a href="#" class="action">Button</a> + Card footer + </div> + </div> + <h3>Card with Images Caps</h3> + <div class="card" style="width:300px;"> + <img class="card-thumbnail top" width="300" /> + <div class="card-content"> + <h4 class="title">Card title</h4> + <p class="description">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p> + <p class="description"><small class="text-muted">Last updated 3 mins ago</small></p> + </div> + </div> + <div class="card" style="width:300px;"> + <div class="card-content"> + <h4 class="title">Card title</h4> + <p class="description">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p> + <p class="description"><small class="text-muted">Last updated 3 mins ago</small></p> + </div> + <img class="card-thumbnail top" width="300" /> + </div> + + @see - <http://v4-alpha.getbootstrap.com/components/card/> + @see - https://vitalcss.com/layouts/ Exemple de structure et de declinaisons pour le card component + @see - http://zurb.com/article/1456/5-common-mistakes-designers-make-when-usi + @see - https://www.nngroup.com/articles/cards-component/ + @see - http://line25.com/inspiration/card-grid-layouts-websites + @see - https://designshack.net/articles/layouts/the-complete-guide-to-an-effective-card-style-interface-design/ + @see - https://codepen.io/sdthornton/pen/wBZdXq - MaterialDesigns shadow boxs + @bug - Sur l'élément title, le text overflow flex-childs https://css-tricks.com/flexbox-truncated-text/ + @param $card-border-radius (3px !default) - + @param $card-spacer-x (1em !default) - + @param $card-spacer-y (1em !default) - + @param $card-bg (white !default) - + @param $card-border-width (1px !default) - + @param $card-border-color (silver !default) - + @param $card-cap-bg (lighten($black, 80)!default) - + @param $card-cap-color (white !default) - + @param $unstyled-components (false !default) - + + Styleguide addons.uikit.cards + */ + * { + box-sizing: border-box; + } + + #page { + max-width: 960px; + margin-left: auto; + margin-right: auto; + padding: 0 0.375em; + } + #page:after { + content: " "; + display: block; + clear: both; + } + head { + display: block; + position: fixed; + left: 10px; + top: 10px; + z-index: 999; + color: #333; + background: rgba(255, 255, 255, 0.25); + } + + head:before { + content: "|||"; + display: block; + padding: 5px 10px; + font-family: sans-serif; + font-size: 16px; + font-weight: bold; + } + + head:hover { + background: rgba(255, 255, 255, 0.5); + color: red; + } + + head:hover ~ #page, + head:hover ~ body #page { + position: relative; + } + head:hover ~ #page:before, + head:hover ~ body #page:before { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + content: " "; + z-index: 998; + background-image: linear-gradient( + to right, + rgba(102, 102, 255, 0.25), + rgba(179, 179, 255, 0.25) 80%, + transparent 0% + ), + linear-gradient(to bottom, #000 1px, transparent 1px); + background-size: 12.8205128205%, 100% 1.5em; + background-origin: content-box; + background-clip: content-box; + background-position: left top; + } + @media (min-width: 481px) { + #page { + padding: 0; + } + } + + .card { + position: relative; + max-width: 100%; + display: block; + margin-bottom: 2.5641025641%; + background-color: #ffffff; + } + .card-media { + position: relative; + display: inline-block; + width: 100%; + overflow: hidden; + background-size: cover; + background-position: center; + } + .card-media img { + min-width: 100%; + height: 100%; + } + .card-content { + display: block; + padding: 1em; + } + .card-content .title, + .card-content .subtitle, + .card-content .description { + display: inline-block; + width: 100%; + } + .card-content .title { + padding-bottom: 0.75em; + min-width: 0; + display: flex; + align-items: center; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + } + .card-content .title > * { + flex: 1; + margin: 0; + padding: 0; + } + .card-content .subtitle { + position: relative; + margin-top: -15px; + margin-bottom: 0.75em; + } + .card-content .description:last-child { + margin-bottom: 0; + } + .card-content .btn { + max-width: 100%; + word-break: break-word; + white-space: initial; + } + .card-header { + padding: 0.75em; + } + .card-footer { + padding: 0.75em; + } + + .card-list-legacy { + max-width: 960px; + margin-left: auto; + margin-right: auto; + } + .card-list-legacy:after { + content: " "; + display: block; + clear: both; + } + head:hover ~ .card-list-legacy, + head:hover ~ body .card-list-legacy { + position: relative; + } + head:hover ~ .card-list-legacy:before, + head:hover ~ body .card-list-legacy:before { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + content: " "; + z-index: 998; + /* background-image: linear-gradient(to right, rgba(102, 102, 255, 0.25), rgba(179, 179, 255, 0.25) 80%, transparent 80%), linear-gradient(to bottom, #000 1px, transparent 1px); + background-size: 12.8205128205%, 100% 1.5em; + background-origin: content-box; + background-clip: content-box; + background-position: left top; */ + } + .card-list-legacy > .card { + width: 100%; + float: left; + margin-left: 0; + margin-right: 0; + } + .card-list-legacy > .card .card-media { + width: 37.5%; + float: left; + padding-left: 1.25%; + padding-right: 1.25%; + } + .card-list-legacy > .card .card-content { + width: 62.5%; + float: left; + padding-left: 1.25%; + padding-right: 1.25%; + } + + /** # Cards layouts + + .card-list - vue de l'objet card en mode liste + .card-list.media-right - le visuel est à droite + .card-list.media-left - le visuel est à gauche + .card-list.media-alternate - alterne les visuels gauche/droite + .card-grid - vue de l'objet card en grille + .card-grid.media-top - visuels en haut + .card-grid.media-bottom - visuel en bas + + Markup + <div class="$modifierClass"> + <div class="card"> + <div class="card-media"><img src="https://images.unsplash.com/photo-1465414829459-d228b58caf6e"/></div> + <div class="card-content"> + <div class="title"> + <h2>Card title</h2> + </div> + <div class="subtitle">My card subtitle</div> + <div class="description">Card description , la description peut être un texte très long et parfois peut intéressant</div> + </div> + <div class="card-footer"></div> + </div> + <div class="card"> + <div class="card-media"><img src="https://images.unsplash.com/photo-1465414829459-d228b58caf6e"/></div> + <div class="card-content"> + <div class="title"> + <h2>Card title</h2> + </div> + <div class="subtitle">My card subtitle</div> + <div class="description">Card description , la description peut être un texte très long et parfois peut intéressant</div> + </div> + <div class="card-footer"></div> + </div> + <div class="card"> + <div class="card-media"><img src="https://images.unsplash.com/photo-1465414829459-d228b58caf6e"/></div> + <div class="card-content"> + <div class="title"> + <h2>Card title</h2> + </div> + <div class="subtitle">My card subtitle</div> + <div class="description">Card description , la description peut être un texte très long et parfois peut intéressant</div> + </div> + <div class="card-footer"></div> + </div> + <div class="card"> + <div class="card-media"> + <img src="https://images.unsplash.com/photo-1485871811272-aa71f1f55124"/> + </div> + <div class="card-content"> + <div class="title"> + <h2>Card title, le titre sur plusieures lignes</h2> + </div> + <div class="subtitle">My card subtitle, plus court</div> + <div class="description">Card description , la description peut être un texte très long et parfois peut intéressant</div> + </div> + <div class="card-footer"></div> + </div> + </div> + + Styleguide cards.layouts + */ + .card-list { + max-width: 960px; + margin-left: auto; + margin-right: auto; + } + .card-list:after { + content: " "; + display: block; + clear: both; + } + head:hover ~ .card-list, + head:hover ~ body .card-list { + position: relative; + } + head:hover ~ .card-list:before, + head:hover ~ body .card-list:before { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + content: " "; + z-index: 998; + background-image: linear-gradient( + to right, + rgba(102, 102, 255, 0.25), + rgba(179, 179, 255, 0.25) 80%, + transparent 80% + ), + linear-gradient(to bottom, #000 1px, transparent 1px); + background-size: 12.8205128205%, 100% 1.5em; + background-origin: content-box; + background-clip: content-box; + background-position: left top; + } + .card-list > .card { + display: flex; + flex-flow: row; + flex-wrap: wrap; + align-self: center; + margin-left: 0.75em; + margin-right: 0.75em; + } + .card-list > .card .card-header, + .card-list > .card .card-footer { + width: 100%; + float: left; + padding-left: 1.25%; + padding-right: 1.25%; + } + .card-list > .card .card-media { + padding-bottom: 0%; + } + @media (min-width: 780px) { + .card-list > .card { + flex-wrap: nowrap; + } + .card-list > .card .card-media { + padding-right: 45%; + padding-left: 0; + padding-bottom: 40%; + } + .card-list > .card .card-content { + padding: 1.5em; + } + } + @media (min-width: 780px) { + .card-list.media-right > .card .card-content, + .card-list > .card.media-right .card-content, + .card-list.media-alternate > .card:nth-child(2n) .card-content { + order: 3; + } + .card-list.media-right > .card .card-media, + .card-list > .card.media-right .card-media, + .card-list.media-alternate > .card:nth-child(2n) .card-media { + padding-left: 45%; + padding-right: 0; + order: 4; + } + .card-list.media-right > .card .card-footer, + .card-list > .card.media-right .card-footer, + .card-list.media-alternate > .card:nth-child(2n) .card-footer { + order: 5; + } + } + .card-list > .card.media-left { + flex-flow: row; + } + + .card-grid { + max-width: 960px; + margin-left: auto; + margin-right: auto; + padding-top: 1.5em; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + } + .card-grid:after { + content: " "; + display: block; + clear: both; + } + head:hover ~ .card-grid, + head:hover ~ body .card-grid { + position: relative; + } + head:hover ~ .card-grid:before, + head:hover ~ body .card-grid:before { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + content: " "; + z-index: 998; + background-image: linear-gradient( + to right, + rgba(102, 102, 255, 0.25), + rgba(179, 179, 255, 0.25) 80%, + transparent 80% + ), + linear-gradient(to bottom, #000 1px, transparent 1px); + background-size: 12.8205128205%, 100% 1.5em; + background-origin: content-box; + background-clip: content-box; + background-position: left top; + } + .card-grid:after { + content: ""; + flex: auto; + } + .card-grid > .card { + display: flex; + flex-direction: column; + margin-left: 0.75em; + margin-right: 0.75em; + } + .card-grid > .card .card-media { + padding-bottom: 0%; + } + @media (min-width: 481px) and (max-width: 780px) { + .card-grid > .card { + width: 46.6666666667%; + float: left; + margin-left: 1.6666666667%; + margin-right: 1.6666666667%; + } + .card-grid > .card .card-media { + padding-bottom: 0%; + } + } + @media (min-width: 781px) { + .card-grid > .card { + width: 31.1111111111%; + float: left; + margin-left: 1.1111111111%; + margin-right: 1.1111111111%; + } + .card-grid > .card .card-media { + padding-bottom: 0%; + } + } + .card-grid.media-bottom > .card, + .card-grid .card.media-bottom { + flex-direction: column-reverse; + } + .card-grid.media-top > .card, + .card-grid .card.media-top { + flex-direction: column; + } + + .l-masonry { + display: flex; + flex-flow: column wrap; + align-items: center; + max-width: 960px; + counter-reset: element; + margin: 0 auto; + } + .l-masonry .card { + margin: 0 0 1rem 0; + flex: 0 0 auto; + overflow: hidden; + width: 100%; + } + @media (min-width: 481px) and (max-width: 767px) { + .l-masonry .card { + width: calc(50% - 1rem); + } + } + @media (min-width: 768px) { + .l-masonry .card { + width: calc(33.3333333333% - 1rem); + } + } + .l-masonry .card-media { + padding-bottom: 80%; + } + + .t-light-shadows .card-content .title, + h1 { + font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif; + font-weight: 400; + text-align: left; + color: #222; + } + + .t-light-shadows .card-content p, + .t-light-shadows .card-content .btn { + font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif; + font-weight: 100; + } + + h1 { + text-align: center; + padding: 0.75em 0; + margin: 0.75em 15%; + border-top: 1px solid silver; + border-bottom: 1px solid silver; + } + @media (min-width: 481px) { + h1 { + margin: 1.5em 30%; + } + } + h1 small { + display: inline-block; + width: 100%; + font-size: 0.5em; + } + + .t-light-shadows .card { + box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.15); + } + .t-light-shadows .card-content { + transition: all 0.5s ease-out; + } + .t-light-shadows .card-content .description:before { + content: ""; + display: block; + height: 0.25rem; + width: 4rem; + background-color: #888888; + margin-bottom: 0.75em; + } + .t-light-shadows .angle-bottom .card .card-media, + .t-light-shadows .card-media.angle-bottom { + -webkit-clip-path: polygon(0 0, 100% 10%, 100% 100%, 0 90%); + clip-path: polygon(0 0, 100% 10%, 100% 100%, 0 90%); + transition: all 0.5s ease; + } + .t-light-shadows .angle-bottom .card:hover .card-media, + .t-light-shadows .angle-bottom .card.hover .card-media { + -webkit-clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); + clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); + } + .t-light-shadows .angle-bottom .card:hover .card-content, + .t-light-shadows .angle-bottom .card.hover .card-content { + transform: translateY(-30%); + background-color: rgba(255, 255, 255, 0.75); + } + .t-light-shadows .angle-bottom .card:hover .title, + .t-light-shadows .angle-bottom .card.hover .title { + margin: 15% 0; + } + /* Style for the bullet list */ + #end-points ul { + list-style: none; + padding: 0; + margin: 0; + } + + /* Style for the list items */ + #end-points li { + margin-bottom: 10px; + padding-left: 20px; + position: relative; + font-size: 16px; + font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif; + justify-content: center; + align-items: justify-content; + line-height: 1.5; + color: #333; + } + + /* Style for the custom bullet point */ + #end-points li::before { + content: "\2713"; /* Unicode character for a checkmark */ + color: #610404; /* Change the color as needed */ + font-size: 20px; + font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif; + position: relative; + left: 0; + top: 50%; + transform: translateY(-50%); + } \ No newline at end of file