diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml new file mode 100644 index 0000000..cb0c024 --- /dev/null +++ b/.gitea/workflows/deploy.yaml @@ -0,0 +1,35 @@ +name: Deploy Docker Compose + +on: + push: + branches: + - master + +jobs: + deploy: + runs-on: ubuntu-latest # Use the Ubuntu runner + + steps: + - name: Checkout repository + uses: actions/checkout@v2 # Checkout your repository code + + - name: Install Docker + run: | + sudo apt-get update + sudo apt-get install -y docker.io # Install Docker + sudo usermod -aG docker $USER # Add the current user to the docker group (optional) + docker --version # Verify Docker installation + + - name: Install Docker Compose + run: | + sudo curl -L "https://github.com/docker/compose/releases/download/$(curl -s https://api.github.com/repos/docker/compose/releases/latest | jq -r .tag_name)/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + docker-compose --version # Verify Docker Compose installation + + - name: Deploy using Docker Compose + env: # Inject secrets into environment variables + LOL_APIKEY: ${{ secrets.RIOT_APIKEY }} + run: | + docker-compose build --build-arg RIOT_APIKEY =${RIOT_APIKEY } + docker-compose -f docker-compose.yaml up -d # Start the containers in detached mode + docker-compose logs # Display logs from the containers to show output diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8e197da --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +# Use an official Node.js runtime as a parent image +FROM node:18 + +# Set the working directory +WORKDIR /app + +# Install pnpm +RUN npm install -g pnpm + +# Copy package.json and pnpm-lock.yaml +COPY package.json pnpm-lock.yaml ./ + +# Accept build arguments +ARG RIOT_APIKEY + +# Set environment variables +ENV RIOT_APIKEY=$RIOT_APIKEY + +# Install dependencies using pnpm +RUN pnpm install + +# Copy the rest of the application code +COPY . . + +# Build the application (if needed) +RUN pnpm build + +# Expose the port the app runs on +EXPOSE 4173 + +# Command to run the application +CMD ["pnpm", "dev"] \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..b55f04c --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,15 @@ +version: "3" + +services: + sveltekit-app: + container_name: trackerdeq + environment: + - RIOT_APIKEY=${RIOT_APIKEY} + build: + context: . + args: + RIOT_APIKEY: ${RIOT_APIKEY} + dockerfile: Dockerfile + ports: + - "3334:4173" + restart: always diff --git a/public/file.svg b/public/file.svg deleted file mode 100644 index 004145c..0000000 --- a/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg deleted file mode 100644 index 567f17b..0000000 --- a/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/riot.txt b/public/riot.txt new file mode 100644 index 0000000..8a9486a --- /dev/null +++ b/public/riot.txt @@ -0,0 +1 @@ +b89cde11-6ba8-4f6a-b0ae-5b8bfff359df \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index 7705396..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/window.svg b/public/window.svg deleted file mode 100644 index b2b2a44..0000000 --- a/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/api/lobby/[player]/route.ts b/src/app/api/lobby/[player]/route.ts new file mode 100644 index 0000000..17af538 --- /dev/null +++ b/src/app/api/lobby/[player]/route.ts @@ -0,0 +1,19 @@ +import { getLobbyData } from "@/app/lib/utils"; +import { NextRequest } from "next/server"; + +export async function GET ( + req: NextRequest, + context: { params: { player: string } } +) +{ + try + { + const { player } = await context.params; + const data = await getLobbyData( player ); + return Response.json( data ); + } catch ( err: any ) + { + console.error( "API error:", err ); + return new Response( "Failed to fetch lobby data", { status: 500 } ); + } +} diff --git a/src/app/api/ranked/[name]/[tag]/route.ts b/src/app/api/ranked/[name]/[tag]/route.ts new file mode 100644 index 0000000..c2ebcb4 --- /dev/null +++ b/src/app/api/ranked/[name]/[tag]/route.ts @@ -0,0 +1,19 @@ +import { getRankedData } from "@/app/lib/utils"; +import { NextRequest } from "next/server"; + +export async function GET ( + req: NextRequest, + context: { params: { name: string; tag: string } } +) +{ + try + { + const { name, tag } = await context.params; + const data = await getRankedData( name, tag ); + return Response.json( data ); + } catch ( err: any ) + { + console.error( "API error:", err ); + return new Response( "Failed to fetch ranked data", { status: 500 } ); + } +} \ No newline at end of file diff --git a/src/app/api/session/[name]/[tag]/route.ts b/src/app/api/session/[name]/[tag]/route.ts new file mode 100644 index 0000000..c37155d --- /dev/null +++ b/src/app/api/session/[name]/[tag]/route.ts @@ -0,0 +1,19 @@ +import { getSessionData } from "@/app/lib/utils"; +import { NextRequest } from "next/server"; + +export async function GET ( + req: NextRequest, + context: { params: { name: string; tag: string } } +) +{ + try + { + const { name, tag } = await context.params; + const data = await getSessionData( name, tag ); + return Response.json( data ); + } catch ( err: any ) + { + console.error( "API error:", err ); + return new Response( "Failed to fetch session data", { status: 500 } ); + } +} diff --git a/src/app/components/Lobby.tsx b/src/app/components/Lobby.tsx new file mode 100644 index 0000000..5e0c3a5 --- /dev/null +++ b/src/app/components/Lobby.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { useEffect, useState } from "react"; + +interface Player +{ + name: string; + position: string; + team: string | null; + tag: string | null; + country: string | null; + logo: string | null; + championId: number; + teamId: number; + wr: number; + championImage: string; + countryFlag: string; + positionIcon: string; +} + +interface LobbyData +{ + inGame: boolean; + gameId?: number; + players?: Player[]; +} + +export default function Lobby ( { player }: { player: string } ) +{ + const [ data, setData ] = useState( null ); + const [ lastGameId, setLastGameId ] = useState( null ); + const [ showAnimation, setShowAnimation ] = useState( false ); + + useEffect( () => + { + const fetchLobby = async () => + { + const res = await fetch( `/api/lobby/${ player }` ); + if ( !res.ok ) return; + const json = await res.json(); + setData( json ); + + if ( json.inGame && json.gameId !== lastGameId ) + { + setShowAnimation( true ); + setLastGameId( json.gameId ); + } + }; + + fetchLobby(); + const interval = setInterval( fetchLobby, 45 * 1000 ); + return () => clearInterval( interval ); + }, [ player, lastGameId ] ); + + if ( !data || !data.inGame || !data.players ) return null; + + const overlayClass = `overlay slide-animation max-h-screen grid grid-rows-2 items-center font-sans font-bold tracking-wide ${ showAnimation ? "" : "hidden" + }`; + + return ( +
setShowAnimation( false ) }> + { [ 200, 100 ].map( ( teamId ) => ( +
+ { data.players + .filter( ( p ) => p.teamId === teamId ) + .map( ( player, i ) => + { + const playerClass = + "player flex flex-row justify-between items-center my-2 min-w-56 w-full max-w-[400px] bg-opacity-50 rounded-xl shadow-md " + + ( teamId === 100 ? "bg-sky-800" : "bg-red-800" ); + + return ( +
+
+ +
+
+ { player.tag ? `${ player.tag } ${ player.name }` : player.name } +
+ +
+
+
+
+ { player.wr }% +
+
+
+ + { player.logo && ( +
+ ) } +
+ ); + } ) } +
+ ) ) } +
+ ); +} diff --git a/src/app/components/Ranked.tsx b/src/app/components/Ranked.tsx new file mode 100644 index 0000000..9dae878 --- /dev/null +++ b/src/app/components/Ranked.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { useEffect, useState } from "react"; + +interface Match +{ + matchId: string; + championImg: string; + win: boolean; + kills: number; + deaths: number; + assists: number; + isOld?: boolean; +} + +interface SummonerData +{ + tier: string; + division?: string; + lp: number; + wins: number; + losses: number; + winrate: number; + matches: Match[]; +} + +export default function Ranked ( { name, tag }: { name: string; tag: string } ) +{ + const [ data, setData ] = useState( null ); + + useEffect( () => + { + const fetchData = async () => + { + try + { + const res = await fetch( `/api/ranked/${ name }/${ tag }` ); + if ( !res.ok ) throw new Error( "API error" ); + const json = await res.json(); + setData( json ); + } catch ( err ) + { + console.error( "Failed to fetch ranked data:", err ); + } + }; + + fetchData(); + const interval = setInterval( fetchData, 2 * 60 * 1000 ); + + return () => clearInterval( interval ); + }, [ name, tag ] ); + + if ( !data ) return null; + + return ( +
+
+
+ { [ "MASTER", "GRANDMASTER", "CHALLENGER" ].includes( data.tier ) ? ( +
+ { data.tier } { data.lp } LP +
+ ) : ( +
+ { data.tier } { data.division } { data.lp } LP +
+ ) } +
+ +
+
{ data.wins }W
+
{ data.losses }L
+
+ { data.winrate }% +
+
+
+ +
+ { data.matches.map( ( match, index ) => ( +
+
+
+
+
+ { match.kills }/{ match.deaths }/{ match.assists } +
+
+ ) ) } +
+
+ ); +} diff --git a/src/app/components/Session.tsx b/src/app/components/Session.tsx new file mode 100644 index 0000000..53523f0 --- /dev/null +++ b/src/app/components/Session.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { useEffect, useState } from "react"; + +interface SessionData +{ + profileIcon: string; + rankedIcon?: string; + kills: string; + deaths: string; + assists: string; + wins: number; + losses: number; + lpGain?: number; +} + +export default function Session ( { name, tag }: { name: string; tag: string } ) +{ + const [ data, setData ] = useState( null ); + + useEffect( () => + { + const fetchData = async () => + { + try + { + const res = await fetch( `/api/session/${ name }/${ tag }` ); + if ( !res.ok ) throw new Error( "API error" ); + const json = await res.json(); + setData( json ); + } catch ( err ) + { + console.error( "Failed to fetch session data:", err ); + } + }; + + fetchData(); + const interval = setInterval( fetchData, 2 * 60 * 1000 ); + return () => clearInterval( interval ); + }, [ name, tag ] ); + + if ( !data ) return null; + + return ( +
+
+
+ Summoner Icon + { data.rankedIcon && ( + Ranked Icon + ) } +
+
+ +
+
+ + { data.kills } + + / + + { data.deaths } + + / + + { data.assists } + +
+ +
+
+
+
+ { data.wins }W +
+
+ { data.losses }L +
+
+ + { typeof data.lpGain === "number" && data.lpGain !== 0 && ( +
0 + ? "text-sky-600" + : data.lpGain < 0 + ? "text-red-600" + : "text-white" + }` } + > + { data.lpGain > 0 ? `+${ data.lpGain }` : data.lpGain }LP +
+ ) } +
+
+
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..d491c1d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,114 @@ @import "tailwindcss"; :root { - --background: #ffffff; - --foreground: #171717; -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --background: #ffffff; + --foreground: #171717; } @media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } + :root { + --background: #0a0a0a; + --foreground: #ededed; + } } body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; } + +@keyframes slideInOut { + 0% { + transform: translateX(-100%); + } + + 5% { + transform: translateX(0); + } + + 95% { + transform: translateX(0); + } + + 100% { + transform: translateX(-100%); + } +} + +.slide-animation { + animation: slideInOut 30s ease-in-out forwards; +} + +.game:nth-child(1) { + opacity: 1.00; +} + +.game:nth-child(2) { + opacity: 0.95; +} + +.game:nth-child(3) { + opacity: 0.90; +} + +.game:nth-child(4) { + opacity: 0.85; +} + +.game:nth-child(5) { + opacity: 0.80; +} + +.game:nth-child(6) { + opacity: 0.75; +} + +.lose { + filter: grayscale(0.25); +} + +.game.old>.background { + filter: grayscale(0.75); +} + +.game.old>.background>.champion { + filter: blur(1px) grayscale(0.85); +} + +.floating-box { + animation: float 5s ease-in-out infinite; +} + +@keyframes float { + + 0%, + 100% { + transform: translateY(-0.5rem); + } + + 50% { + transform: translateY(-1.5rem); + } +} + +@keyframes slide { + + 0%, + 40% { + transform: translateY(0%); + } + + 50%, + 90% { + transform: translateY(-50%); + } + + 100% { + transform: translateY(0%); + } +} + +.slide-box { + animation: slide 15s ease-in-out infinite; +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..dc31caf 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,34 +1,22 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "trackerdeq" }; -export default function RootLayout({ - children, +export default function RootLayout ( { + children, }: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - {children} - - - ); + children: React.ReactNode; +}> ) +{ + return ( + + + { children } + + + ); } diff --git a/src/app/lib/utils.ts b/src/app/lib/utils.ts new file mode 100644 index 0000000..7b783c5 --- /dev/null +++ b/src/app/lib/utils.ts @@ -0,0 +1,272 @@ +import { cookies } from "next/headers"; + +const RIOT_TOKEN = process.env.RIOT_APIKEY!; +const REGION = "EUW1"; +const REGION_GROUP = "EUROPE"; +const TWELVE_HOURS_IN_MS = 12 * 60 * 60 * 1000; + +const headers = { "X-Riot-Token": RIOT_TOKEN }; + + +export function getChampionSquare ( championId: number ) +{ + return `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/champion-icons/${ championId }.png`; +} + +export function getSummonerIcon ( profileIconId: number ) +{ + return `https://raw.communitydragon.org/latest/game/assets/ux/summonericons/profileicon${ profileIconId }.png`; +} + +export function getPositionIcon ( position: string ) +{ + switch ( position ) + { + case "10_top": + return "https://raw.communitydragon.org/latest/plugins/rcp-fe-lol-clash/global/default/assets/images/position-selector/positions/icon-position-top.png"; + case "20_jungle": + return "https://raw.communitydragon.org/latest/plugins/rcp-fe-lol-clash/global/default/assets/images/position-selector/positions/icon-position-jungle.png"; + case "30_mid": + return "https://raw.communitydragon.org/latest/plugins/rcp-fe-lol-clash/global/default/assets/images/position-selector/positions/icon-position-middle.png"; + case "40_adc": + return "https://raw.communitydragon.org/latest/plugins/rcp-fe-lol-clash/global/default/assets/images/position-selector/positions/icon-position-bottom.png"; + case "50_support": + return "https://raw.communitydragon.org/latest/plugins/rcp-fe-lol-clash/global/default/assets/images/position-selector/positions/icon-position-utility.png"; + default: + return ""; + } +} + +export function getRankedIcon ( rankedTier: string ) +{ + return `https://raw.communitydragon.org/latest/plugins/rcp-fe-lol-static-assets/global/default/ranked-emblem/wings/wings_${ rankedTier.toLowerCase() }.png`; +} + +export function getCountryFlag ( countryCode: string ) +{ + return `https://flagcdn.com/160x120/${ countryCode.toLowerCase() }.png`; +} + +export function calculateWinrate ( wins: number, losses: number ): number +{ + const total = wins + losses; + return total === 0 ? 0 : Math.round( ( wins / total ) * 100 ); +} + +export function isOlderThanHoursAgo ( timestamp: number, hours: number ): boolean +{ + const currentTimestamp = Date.now(); + const hoursInMs = hours * 60 * 60 * 1000; + return timestamp < currentTimestamp - hoursInMs; +} + + + +export async function fetchAccount ( name: string, tag: string ) +{ + const res = await fetch( + `https://${ REGION_GROUP }.api.riotgames.com/riot/account/v1/accounts/by-riot-id/${ name }/${ tag }`, + { headers } + ); + if ( !res.ok ) throw new Error( "Account fetch failed" ); + return res.json(); +} + +export async function fetchSummoner ( puuid: string ) +{ + const res = await fetch( + `https://${ REGION }.api.riotgames.com/lol/summoner/v4/summoners/by-puuid/${ puuid }`, + { headers } + ); + if ( !res.ok ) throw new Error( "Summoner fetch failed" ); + return res.json(); +} + +export async function fetchRankedStats ( summonerId: string ) +{ + const res = await fetch( + `https://${ REGION }.api.riotgames.com/lol/league/v4/entries/by-summoner/${ summonerId }`, + { headers } + ); + if ( !res.ok ) return {}; + const leagues = await res.json(); + const soloQ = leagues.find( ( e: any ) => e.queueType === "RANKED_SOLO_5x5" ); + if ( !soloQ ) return {}; + return { + tier: soloQ.tier, + division: soloQ.rank, + lp: soloQ.leaguePoints, + wins: soloQ.wins, + losses: soloQ.losses, + winrate: calculateWinrate( soloQ.wins, soloQ.losses ), + }; +} + +export async function fetchMatches ( puuid: string ) +{ + const idsRes = await fetch( + `https://${ REGION_GROUP }.api.riotgames.com/lol/match/v5/matches/by-puuid/${ puuid }/ids?queue=420&count=6`, + { headers } + ); + if ( !idsRes.ok ) throw new Error( "Match list fetch failed" ); + + const matchIds = await idsRes.json(); + + const matches = await Promise.all( + matchIds.map( async ( matchId: string ) => + { + const res = await fetch( + `https://${ REGION_GROUP }.api.riotgames.com/lol/match/v5/matches/${ matchId }`, + { headers } + ); + const match = await res.json(); + const participant = match.info.participants.find( ( p: any ) => p.puuid === puuid ); + return { + matchId, + kills: participant.kills, + deaths: participant.deaths, + assists: participant.assists, + championImg: getChampionSquare( participant.championId ), + win: participant.win, + isOld: isOlderThanHoursAgo( match.info.gameCreation, 12 ), + }; + } ) + ); + + return matches; +} + +export async function getRankedData ( name: string, tag: string ) +{ + const account = await fetchAccount( name, tag ); + const summoner = await fetchSummoner( account.puuid ); + const [ matches, rankedStats ] = await Promise.all( [ + fetchMatches( account.puuid ), + fetchRankedStats( summoner.id ), + ] ); + + return { ...rankedStats, matches }; +} + +export async function getSessionData ( name: string, tag: string ) +{ + const cookieStore = cookies(); + const sessionCookie = ( await cookieStore ).get( "lpSession" ); + + const account = await fetchAccount( name, tag ); + const summoner = await fetchSummoner( account.puuid ); + const [ matches, rankedStats ] = await Promise.all( [ + fetchMatches( account.puuid ), + fetchRankedStats( summoner.id ), + ] ); + + let totalKills = 0; + let totalDeaths = 0; + let totalAssists = 0; + let wins = 0; + let losses = 0; + + for ( const match of matches ) + { + totalKills += match.kills; + totalDeaths += match.deaths; + totalAssists += match.assists; + match.win ? wins++ : losses++; + } + + const matchCount = matches.length || 1; + const avgKills = ( totalKills / matchCount ).toFixed( 1 ); + const avgDeaths = ( totalDeaths / matchCount ).toFixed( 1 ); + const avgAssists = ( totalAssists / matchCount ).toFixed( 1 ); + + let lpGain = 0; + let currentLP = rankedStats.lp || 0; + let initialLP = currentLP; + const now = Date.now(); + + if ( sessionCookie ) + { + try + { + const sessionData = JSON.parse( sessionCookie.value ); + const sessionTime = sessionData.timestamp; + + if ( now - sessionTime < TWELVE_HOURS_IN_MS ) + { + initialLP = sessionData.initialLP; + lpGain = currentLP - initialLP; + } + } catch { + } + } + + if ( !sessionCookie || lpGain === 0 ) + { + ( await cookieStore ).set( + "lpSession", + JSON.stringify( { initialLP: currentLP, timestamp: now } ), + { + path: "/", + maxAge: TWELVE_HOURS_IN_MS / 1000, + httpOnly: true, + } + ); + } + + return { + profileIcon: getSummonerIcon( summoner.profileIconId ), + rankedIcon: getRankedIcon( rankedStats.tier ), + kills: avgKills, + deaths: avgDeaths, + assists: avgAssists, + wins, + losses, + lpGain, + }; +} + +export async function getLobbyData ( player: string ) +{ + const searchRes = await fetch( `https://api.lolpros.gg/es/profiles/${ player }` ); + if ( !searchRes.ok ) throw new Error( "Profile search failed" ); + + const profile = await searchRes.json(); + if ( !profile?.uuid ) throw new Error( "Player not found" ); + + const gameRes = await fetch( `https://api.lolpros.gg/lol/game/${ profile.uuid }` ); + if ( gameRes.status === 204 ) return { inGame: false }; + + if ( !gameRes.ok ) throw new Error( "Game data fetch failed" ); + const gameData = await gameRes.json(); + + const players = gameData.participants + .filter( ( p: any ) => p.lolpros ) + .map( ( p: any ) => + { + const name = p.lolpros?.name || "Unknown"; + const position = p.lolpros?.position || "Unknown"; + const country = p.lolpros?.country || null; + + return { + name, + position, + team: p.lolpros?.team?.name || null, + tag: p.lolpros?.team?.tag || null, + country, + logo: p.lolpros?.team?.logo?.url || null, + championId: p.championId, + teamId: p.teamId, + wr: calculateWinrate( p.ranking.wins, p.ranking.losses ), + championImage: getChampionSquare( p.championId ), + countryFlag: getCountryFlag( country ), + positionIcon: getPositionIcon( position ), + }; + } ) + .sort( ( a: any, b: any ) => a.position.localeCompare( b.position ) ); + + return { + inGame: true, + gameId: gameData.gameId, + players, + }; +} diff --git a/src/app/lobby/[player]/page.tsx b/src/app/lobby/[player]/page.tsx new file mode 100644 index 0000000..5761e79 --- /dev/null +++ b/src/app/lobby/[player]/page.tsx @@ -0,0 +1,12 @@ +import Lobby from "@/app/components/Lobby"; + +export default async function Page ( { + params, +}: { + params: { player: string }; +} ) +{ + const { player } = await params; + + return ; +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index e68abe6..e8398fd 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,103 +1,17 @@ -import Image from "next/image"; -export default function Home() { - return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
- - -
- -
- ); +export default function Page () +{ + return ( + <> +
+ ranked +
+
+ session +
+
+ lobby +
+ + ); } diff --git a/src/app/ranked/[name]/[tag]/page.tsx b/src/app/ranked/[name]/[tag]/page.tsx new file mode 100644 index 0000000..42045e8 --- /dev/null +++ b/src/app/ranked/[name]/[tag]/page.tsx @@ -0,0 +1,12 @@ +import Ranked from "@/app/components/Ranked"; + +export default async function Page ( { + params, +}: { + params: { name: string; tag: string }; +} ) +{ + const { name, tag } = await params; + + return ; +} \ No newline at end of file diff --git a/src/app/session/[name]/[tag]/page.tsx b/src/app/session/[name]/[tag]/page.tsx new file mode 100644 index 0000000..912396f --- /dev/null +++ b/src/app/session/[name]/[tag]/page.tsx @@ -0,0 +1,12 @@ +import Session from "@/app/components/Session"; + +export default async function Page ( { + params, +}: { + params: { name: string; tag: string }; +} ) +{ + const { name, tag } = await params; + + return ; +} \ No newline at end of file