From 9427d9ec324d439ca815c7557313e8c6934e48fe Mon Sep 17 00:00:00 2001 From: Ryan Taheri Date: Mon, 10 Nov 2025 06:07:36 -0800 Subject: [PATCH 1/6] womp womp --- front-end/components/SearchandCompare.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/front-end/components/SearchandCompare.js b/front-end/components/SearchandCompare.js index 73cf063..891fb0a 100644 --- a/front-end/components/SearchandCompare.js +++ b/front-end/components/SearchandCompare.js @@ -114,12 +114,20 @@ const SearchAndCompare = ({ player1Stats, onPlayer2Found }) => { )} {/* Stats Display - Identical to Social */} -
+
+
+
+
+
+
+

Loading...

+

Please wait...

+
+

{!friendResult && "Search for a player"} {friendResult && `${friendResult.player_id}`}

-
{stats.map(stat => (
From 389e382b94e0f3d5d2b54db47f9ca27ddf1efbc3 Mon Sep 17 00:00:00 2001 From: qhgill Date: Mon, 10 Nov 2025 06:12:54 -0800 Subject: [PATCH 2/6] social almost done --- front-end/components/SearchandCompare.js | 30 ++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/front-end/components/SearchandCompare.js b/front-end/components/SearchandCompare.js index 73cf063..db8dcd5 100644 --- a/front-end/components/SearchandCompare.js +++ b/front-end/components/SearchandCompare.js @@ -3,6 +3,8 @@ import { useFriendContext } from "@/context/FriendContext"; const SearchAndCompare = ({ player1Stats, onPlayer2Found }) => { const [searchQuery, setSearchQuery] = useState(""); + const [username, setUsername] = useState(""); + const [tagline, setTagline] = useState(""); const [player2Stats, setPlayer2Stats] = useState({ "avg_kda": 0, "avg_cs_per_min": 0, @@ -27,14 +29,14 @@ const SearchAndCompare = ({ player1Stats, onPlayer2Found }) => { } const handleSearch = async () => { - if (!searchQuery.trim()) return; + if (!username.trim() || !tagline.trim()) return; // TODO: Replace with actual API call - const endpoint = "https://v4ft9564pb.execute-api.us-west-2.amazonaws.com/player/process"; + const endpoint = "https://v4ft9564pb.execute-api.us-west-2.amazonaws.com/player/compare"; const body = { - game_name: "ShadowLeaf", - tagline: "8005", + game_name: username, + tagline: tagline, num_games: 1 }; @@ -72,8 +74,8 @@ const SearchAndCompare = ({ player1Stats, onPlayer2Found }) => { if (!stat2 || stat2 === "") return "text-gray-300"; if (isNaN(val1) || isNaN(val2)) return "text-gray-300"; - if (val1 > val2) return "text-green-400"; - if (val1 < val2) return "text-red-400"; + if (val1 < val2) return "text-green-400"; + if (val1 > val2) return "text-red-400"; return "text-gray-300"; }; @@ -84,16 +86,20 @@ const SearchAndCompare = ({ player1Stats, onPlayer2Found }) => {
setSearchQuery(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && handleSearch()} + value={username} + onChange={(e) => setUsername(e.target.value)} + onKeyPress={(e) => e.key === "Enter" && handleSearch()} className="flex-1 px-4 py-3 bg-gray-800 bg-opacity-70 border border-gray-700 rounded text-white focus:outline-none focus:border-gray-500" - placeholder="Find opponent..." + placeholder="Game name" /> + value={tagline} + onChange={(e) => setTagline(e.target.value)} + onKeyPress={(e) => e.key === "Enter" && handleSearch()} + className="w-24 px-4 py-3 bg-gray-800 bg-opacity-70 border border-gray-700 rounded text-white focus:outline-none focus:border-gray-500" + placeholder="#tag" + />
{/* Stats Display - Identical to Social */}
-
+ {fetchingPlayer2 && (
@@ -129,7 +134,7 @@ const SearchAndCompare = ({ player1Stats, onPlayer2Found }) => {

Loading...

Please wait...

-
+
)}

{!friendResult && "Search for a player"} {friendResult && `${friendResult.player_id}`} diff --git a/front-end/components/SummaryBack.jsx b/front-end/components/SummaryBack.jsx index 0e80f4a..e8f7cad 100644 --- a/front-end/components/SummaryBack.jsx +++ b/front-end/components/SummaryBack.jsx @@ -23,8 +23,8 @@ const SummaryBack = ({ data }) => { setActiveTrait(null); setActiveRef(null); }; - window.addEventListener("click", handleClickOutside); - return () => window.removeEventListener("click", handleClickOutside); + window.addEventListener("mousedown", handleClickOutside); + return () => window.removeEventListener("mousedown", handleClickOutside); }, []); const handleTraitClick = (trait, ref, e) => { diff --git a/front-end/components/SummaryFront.jsx b/front-end/components/SummaryFront.jsx index f509010..40a411d 100644 --- a/front-end/components/SummaryFront.jsx +++ b/front-end/components/SummaryFront.jsx @@ -1,13 +1,9 @@ import React, { useState, useEffect, useRef } from "react"; import { - BarChart, - Bar, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, + PieChart, + Pie, Cell, + ResponsiveContainer, } from "recharts"; import TraitPopup from "./TraitPopup"; @@ -15,27 +11,23 @@ const SummaryFront = ({ data }) => { const [activeTrait, setActiveTrait] = useState(null); const [activeRef, setActiveRef] = useState(null); - // ✅ Create ref arrays outside the render loop const strengthRefs = useRef([]); const weaknessRefs = useRef([]); + const overallPercentile = data.overall.percentile; + const percentileLabel = data.overall.interpretation; - const roleData = Object.entries(data.roles || {}).map(([role, count]) => ({ - role: role.toUpperCase(), - games: count, - })); + const pieData = [ + { name: "Percentile", value: overallPercentile }, + { name: "Remaining", value: 100 - overallPercentile }, + ]; + + const pieColors = ["#8b6f4e", "#e5e5e5"]; - const roleColors = { - TOP: "#f9c74f", - JG: "#90be6d", - MID: "#577590", - ADC: "#f94144", - SUP: "#f3722c", - }; useEffect(() => { const handleClickOutside = () => setActiveTrait(null); - window.addEventListener("click", handleClickOutside); - return () => window.removeEventListener("click", handleClickOutside); + window.addEventListener("mousedown", handleClickOutside); + return () => window.removeEventListener("mousedown", handleClickOutside); }, []); const handleTraitClick = (trait, ref, e) => { @@ -49,32 +41,66 @@ const SummaryFront = ({ data }) => { } }; + const statDisplayNames = { + "avg_dpm": "Damage per minute", + "avg_gpm": "Gold per minute", + "avg_kill_participation": "Kill participation", + "avg_kda": "KDA", + "avg_vision_score": "Vision Score", + "avg_cs_per_min": "CS per minute", + "avg_team_damage_pct": "Team Damage Participation", + "avg_outnumbered_kills": "Outnumbered Kills", + "avg_solo_kills": "Solo Kills", + "avg_kills_near_tower": "Kills near tower", + "avg_shields_on_teammates": "Shielding Teammates", + "avg_objective_damage": "Objective Damage", + "avg_dragon_takedowns": "Dragon Takedowns", + "avg_herald_takedowns": "Rift Herald Takedowns", + "avg_early_gold_adv": "Early Gold Advantage", + "avg_heals_on_teammates": "Healing Teammates", + "avg_longest_alive": "Time Alive", + "avg_cc_time": "Crowd Control Time", + "avg_time_dead": "Time Dead", + "avg_pick_kills": "Pick Kills", + "avg_deaths": "Deaths", + "death_consistency": "Death rate", + "cs_consistency": "CS rate", + "win_rate": "Win Rate" + } + return (

Gameplay Overview

- - {/* Role Distribution Chart */} -
-

Role Distribution

- - - - - - - - {roleData.map((entry, index) => ( - - ))} - - - + {/* 🌟 Overall Percentile Section */} +
+

Overall Performance Percentile

+
+
+ + + + {pieData.map((entry, index) => ( + + ))} + + + +
+
+

+ {overallPercentile}% +

+

{percentileLabel}

+
+
{/* Strengths & Weaknesses Section */} @@ -86,7 +112,7 @@ const SummaryFront = ({ data }) => {

Strengths

    - {data.strengths?.slice(0, 4).map((trait, index) => { + {data.strengths?.slice(0, 5).map((trait, index) => { // ✅ Initialize a unique ref for each item (once) if (!strengthRefs.current[index]) { strengthRefs.current[index] = React.createRef(); @@ -97,15 +123,15 @@ const SummaryFront = ({ data }) => {
  • handleTraitClick(trait, ref, e)} style={{ color: "#16a34a" }} > - {trait} + {statDisplayNames[trait.stat]} {activeTrait === trait && activeRef === ref && (

    - {data.details?.[trait] || + {`Percentile: ${trait.percentile}` || "No specific data available."}

    @@ -120,7 +146,7 @@ const SummaryFront = ({ data }) => {

    Weaknesses

      - {data.weaknesses?.slice(0, 4).map((trait, index) => { + {data.weaknesses?.slice(0, 5).map((trait, index) => { // ✅ Initialize a unique ref for each weakness item if (!weaknessRefs.current[index]) { weaknessRefs.current[index] = React.createRef(); @@ -131,15 +157,15 @@ const SummaryFront = ({ data }) => {
    • handleTraitClick(trait, ref, e)} style={{ color: "#dc2626" }} > - {trait} + {statDisplayNames[trait.stat]} {activeTrait === trait && activeRef === ref && (

      - {data.details?.[trait] || + {`Percentile: ${trait.percentile}` || "No specific data available."}

      diff --git a/front-end/context/PercentileContext.js b/front-end/context/PercentileContext.js new file mode 100644 index 0000000..4308414 --- /dev/null +++ b/front-end/context/PercentileContext.js @@ -0,0 +1,15 @@ +"use client"; +import { createContext, useContext, useState } from "react"; + +const PercentileContext = createContext(); + +export function PercentileContextProvider({ children }) { + const [percentileResult, setPercentileResult] = useState(null); + return ( + + {children} + + ); +} + +export const usePercentileContext = () => useContext(PercentileContext); diff --git a/front-end/pages/_app.js b/front-end/pages/_app.js index f4897a1..2d1c1d7 100644 --- a/front-end/pages/_app.js +++ b/front-end/pages/_app.js @@ -10,6 +10,7 @@ import "@/styles/Summary.css"; import { TimelineContextProvider } from "@/context/TimelineContext"; import { FriendContextProvider } from "@/context/FriendContext"; import { RealTimelineContextProvider } from "@/context/RealTimelineContext"; +import { PercentileContextProvider } from "@/context/PercentileContext"; import "@/styles/detentionslip.css"; import "@/styles/index.css"; @@ -17,14 +18,15 @@ export default function App({ Component, pageProps }) { return ( <>
      - - - - - - - - + + + + + + + + +
      ); diff --git a/front-end/pages/index.js b/front-end/pages/index.js index aad3484..1785d7f 100644 --- a/front-end/pages/index.js +++ b/front-end/pages/index.js @@ -1,6 +1,7 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; import { useTimelineContext } from "@/context/TimelineContext"; +import { usePercentileContext } from "@/context/PercentileContext"; export default function Home() { const [usernameValue, setUsernameValue] = useState(""); @@ -17,6 +18,7 @@ export default function Home() { setCountValue(event.target.value); }; const { setTimelineResult } = useTimelineContext(); + const { setPercentileResult } = usePercentileContext(); const router = useRouter(); const handleSearch = async () => { router.push("/loading"); @@ -31,25 +33,30 @@ export default function Home() { try { console.log("Fetching..."); - const res= await + const [timelineRes, percentileRes] = await Promise.all([ fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), - }) + }), + fetch(`https://v4ft9564pb.execute-api.us-west-2.amazonaws.com/player/percentiles?game_name=${usernameValue}&tagline=${taglineValue}`) + ]) - if (!res.ok) { - throw new Error(`API request failed: ${res.status}`); + if (!timelineRes.ok || !percentileRes.ok) { + throw new Error(`API request failed: ${timelineRes.status} / ${percentileRes.status}`); } - const timelineData = await res.json(); + const timelineData = await timelineRes.json(); + const percentileData = await percentileRes.json(); setTimelineResult(timelineData); + setPercentileResult(percentileData); router.push("/FlipBook") } catch (err) { console.error("Error fetching API data:", err); setTimelineResult({ error: "Failed to load timeline." }); + setPercentileResult({ error: "Failed to load percentiles." }); router.push("/invalid") } }; From 2c66691d8f8e4315a48535fc1941dd52e4e8cfd5 Mon Sep 17 00:00:00 2001 From: qhgill Date: Mon, 10 Nov 2025 07:39:52 -0800 Subject: [PATCH 4/6] speed up page turns --- front-end/components/FlipBook.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front-end/components/FlipBook.jsx b/front-end/components/FlipBook.jsx index 83cb170..f373ba8 100644 --- a/front-end/components/FlipBook.jsx +++ b/front-end/components/FlipBook.jsx @@ -389,7 +389,7 @@ const FlipBook = () => { setTimeout(() => { pageRefs.current[i]?.flip(); resolve(); - }, 800); + }, 500); }); } } else { @@ -398,7 +398,7 @@ const FlipBook = () => { setTimeout(() => { pageRefs.current[i]?.flip(); resolve(); - }, 800); + }, 500); }); } } From 41f6ed41b79aba94a696a1d2893873f985e3cc83 Mon Sep 17 00:00:00 2001 From: Ryan Taheri Date: Mon, 10 Nov 2025 07:56:21 -0800 Subject: [PATCH 5/6] wrote descriptions --- front-end/components/SummaryBack.jsx | 50 +++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/front-end/components/SummaryBack.jsx b/front-end/components/SummaryBack.jsx index 0e80f4a..158f609 100644 --- a/front-end/components/SummaryBack.jsx +++ b/front-end/components/SummaryBack.jsx @@ -14,7 +14,55 @@ const SummaryBack = ({ data }) => { "Late-Game": `Shines in the late stages of ${data.region} battle once resources are built.`, "Scaling": "Grows stronger over time with continuous development.", "Empire-Building": "Focuses on expansion and strategic dominance.", - }; + "Patient": "Ready to capitalize on enemy mistakes.", + "Unpredictable": "No one knows this players next move will be, not even they know.", + "Whimsical": "Tilt free and good mental.", + "Mobile": "This summoner moves around the map quickly.", + "Creative": "Utilizes unique methods to put themselves ahead.", + "Relentless": "No one can hold this player back.", + "Undying": "Death is a foreign concept to this summoner.", + "Aggressive": "Takes control of the game by force.", + "Altruistic": "Looks out for teammates in need of support.", + "Opportunistic": "Constantly looking for plays to change the tide of the game.", + "Risky" : "Willing to lose it all for a massive payout.", + "Gold-Focused" : "Gold is the number one prospect in this player's agenda.", + "Jungle-Focused": "Focuses on gettting a lead through massive jungle monsters.", + "Elemental": "Grabs elemental buffs when needed.", + "Objective-Control": "Sets up pressure to secure powerful objectives.", + "Hidden": "Successful ganker", + "Balanced": "Can attack ruthlessly with little to no holes in their defense.", + "Harmonious": "In the flow state, they cannot be shaken.", + "Skillful": "Takes fights to outplay the enemy.", + "Adaptable": "Ready to overcome any situation, no matter how tough.", + "Honorable": "Willing to fight fairly in order to prove a point.", + "Protective": "Capable of protecting the less fortunate.", + "Teamfight": "Coordinates with their team to setup favorable fights.", + "Consistent": "Looks out for any possible positive outcomes that could bring about victory.", + "Dominant": "Bullies the opposing laner into submission.", + "Conquest": "Topples towers and inhibitors in order to crush enemy morale.", + "Powerful": "Enemies crumble to the mere presence of this summoner.", + "Efficient": "Makes the most out of every resource they have.", + "Innovative": "When one idea fails, they have ten more waiting to be unleashed.", + "Calculated": "Meticulous decision making goes into to each and every step of their plans.", + "Wealthy": "Money is of no object.", + "Draining": "Fuels their own life through the pain of their enemies.", + "Persistent": "Unyielding pressure that has no end.", + "Sustain": "Has an infinite amount of self-healing making it impossible for them to die.", + "Deaths-Dance": "Takes damage to recover that damage back.", + "Supportive": "Live and die for your allies.", + "Vision": "Able to see everywhere and anywhere across the map.", + "Celestial": "Benevolent aura.", + "Tanky": "You are an insurmountable wall that protects your team from enemy attacks.", + "Survival": "Able to survive the harsh cold as well as brutal attacks.", + "CC-Heavy": "Enemies find themselves unable to move in your presence.", + "Enduring": "No matter how many blows you take, no matter how much you want to capitulate, you endure.", + "Consuming": "Devours the enemy team's hope for victory.", + "Chaotic": "Ready to take any fights, whenever and wherever on the map.", + "Damage-Focused": "Eager to dish out tons of damage without a care in the world.", + "Experimental": "Eager to try new builds to see what syngeries can be brought about.", + "Damage-Over-Time": "Willing to push themselves to the limit to achieve their goals.", + "High-Risk": "High risk? High reward." + }; From 789b2a450d8b56a141cee6584db9e64098dadfcb Mon Sep 17 00:00:00 2001 From: qhgill Date: Mon, 10 Nov 2025 08:18:10 -0800 Subject: [PATCH 6/6] error handling --- front-end/components/FlipBook.jsx | 4 ++-- front-end/components/SearchandCompare.js | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/front-end/components/FlipBook.jsx b/front-end/components/FlipBook.jsx index f373ba8..a291e58 100644 --- a/front-end/components/FlipBook.jsx +++ b/front-end/components/FlipBook.jsx @@ -196,7 +196,7 @@ const FlipBook = () => { // Create page structure once and store it in a ref const pageStructure = useMemo(() => { - if (!mostPlayed || !timelineResult || !player1Stats || !percentileResult) return []; + if (!mostPlayed || !timelineResult || !player1Stats || !percentileResult ) return []; return [ { cover: "book_cover.jpg", frontCover: true, id: 0 }, @@ -223,7 +223,7 @@ const FlipBook = () => { back: s.trim()), + profile: timelineResult.playstyle?.profile ? timelineResult.playstyle.profile.split(",").map(s => s.trim()): [], statistics: { goldpm: player1Stats.avg_gpm.toFixed(2), winRate: player1Stats.win_rate.toFixed(2), averageKDA: player1Stats.avg_kda.toFixed(2), cspm: player1Stats.avg_cs_per_min.toFixed(2)}, mostPlayed: mostPlayed, playerStats: player1Stats diff --git a/front-end/components/SearchandCompare.js b/front-end/components/SearchandCompare.js index e48c3ed..8fbdc03 100644 --- a/front-end/components/SearchandCompare.js +++ b/front-end/components/SearchandCompare.js @@ -7,6 +7,7 @@ const SearchAndCompare = ({ player1Stats, onPlayer2Found }) => { const [username, setUsername] = useState(""); const [tagline, setTagline] = useState(""); const [fetchingPlayer2, setFetchingPlayer2] = useState(false); + const [errorFetchingPlayer2, setErrorFetchingPlayer2] = useState(false); const [player2Stats, setPlayer2Stats] = useState({ "avg_kda": 0, "avg_cs_per_min": 0, @@ -63,10 +64,13 @@ const SearchAndCompare = ({ player1Stats, onPlayer2Found }) => { setPlayer2Stats(timelineData.stats); onPlayer2Found(timelineData.stats); setFetchingPlayer2(false); + setErrorFetchingPlayer2(false); setHasSearched(true); } catch (err) { console.error("Error fetching API data:", err); setFriendResult({ error: "Failed to load timeline." }); + setErrorFetchingPlayer2(true) + setFetchingPlayer2(false); } }; @@ -135,6 +139,11 @@ const SearchAndCompare = ({ player1Stats, onPlayer2Found }) => {

      Please wait...

)} + {!fetchingPlayer2 && errorFetchingPlayer2 && (
+
+

Error loading player, try again

+
+
)}

{!friendResult && "Search for a player"} {friendResult && `${friendResult.player_id}`}