Initial commit

This commit is contained in:
2024-11-21 19:39:22 +01:00
commit 56d1075459
32 changed files with 10966 additions and 0 deletions

24
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

12
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM node:lts-alpine as base
WORKDIR /app
FROM base as build
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM base
COPY --from=build /app/.output /app/.output
CMD ["node", ".output/server/index.mjs"]

75
frontend/README.md Normal file
View File

@@ -0,0 +1,75 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

9
frontend/app.vue Normal file
View File

@@ -0,0 +1,9 @@
<template>
<div>
<NuxtRouteAnnouncer />
<!-- <NuxtWelcome /> -->
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>

View File

@@ -0,0 +1,40 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
:root {
--color-surface: #312E2C;
--color-on-surface: #B7B8E1;
--color-surface-darker: #1f1d1c;
}
/* Font setting */
h1,h2,h3,h4,h5,h6,p,a,input[type=text] {
font-family: "Inter", sans-serif;
font-optical-sizing: auto;
font-weight: normal;
font-style: normal;
color: var(--color-on-surface);
}
/* Default margins to none */
h1,h2,h3,h4,h5,h6,p,a {
margin: 0px;
}
body {
background-color: var(--color-surface);
}
/* Different title settings */
h1 {
font-size: 40px;
font-weight: 500;
}
h2 {
font-size: 24px;
font-weight: 400;
}
h3 {
font-size: 24px;
font-weight: 300;
}

View File

@@ -0,0 +1,50 @@
<script setup>
const {data: champions} = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json")
const filteredChampions = ref(champions.value.slice(1))
const searchBar = ref(null)
watch(searchBar, (newS, oldS) => {searchBar.value.focus()})
const searchText = ref("")
watch(searchText, (newT, oldT) => {
filteredChampions.value = champions.value.slice(1).filter((champion) => champion.name.toLowerCase().includes(searchText.value.toLowerCase()))
})
</script>
<template>
<div>
<div style="width: fit-content; margin: auto;">
<input v-model="searchText" ref="searchBar" class="search-bar" type="text"/>
</div>
<div class="champion-container" style="margin-top: 20px;">
<RouterLink style="margin-left: 5px; margin-right: 5px;" v-for="champion in filteredChampions" :to="'/champion/' + champion.id">
<img :src="CDRAGON_BASE + mapPath(champion.squarePortraitPath)" :alt="champion.name"/>
</RouterLink>
</div>
</div>
</template>
<style>
.search-bar {
width: 400px;
height: 40px;
background-color: var(--color-surface-darker);
font-size: 20px;
border-radius: 12px;
border: none;
}
.search-bar:focus {
border: 2px solid var(--color-on-surface);
outline: none;
}
.champion-container {
width: 1385px;
height: 660px;
overflow: scroll;
margin: auto;
}
</style>

View File

@@ -0,0 +1,46 @@
<script setup>
const props = defineProps({
championId: {
type: String,
required: true
},
winrate: {
type: Number,
required: true
},
pickrate: {
type: Number,
required: true
},
gameCount: {
type: Number,
required: true
}
})
const championId = Number(props.championId)
const winrate = (props.winrate * 100).toFixed(2)
const pickrate = (props.pickrate * 100).toFixed(2)
const gameCount = props.gameCount
const { data: championData } = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/champions/" + championId + ".json")
const championName = championData.value.name
const championDescription = championData.value.title
</script>
<template>
<div style="display: flex;">
<img width="160" height="160" :src="CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/champion-icons/' + championId + '.png'"/>
<div style="margin-left: 15px; margin-top: 5px;">
<h1>{{ championName }}</h1>
<h3 style="margin-top: 5px">{{ championDescription }}</h3>
<div style="margin-top: 30px; display: flex;">
<h2>{{ winrate }}% win.</h2>
<h2 style="margin-left: 20px; margin-right: 20px;">{{ pickrate }}% pick.</h2>
<h2>{{ gameCount }} games</h2>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,107 @@
<script setup>
const props = defineProps({
// Runes styles: domination, precision, sorcery, inspiration, resolve
primaryStyleId: {
type: String,
required: true
},
secondaryStyleId: {
type:String,
required: true
},
selectionIds: {
type:Array,
required: false,
default: []
}
})
const primaryStyle = ref({slots:[]})
const secondaryStyle = ref({slots:[]})
let { data: perks_data } = await useFetch("https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/perks.json")
const perks = reactive(new Map())
for(let perk of perks_data.value) {
perks.set(perk.id, perk)
}
let { data: stylesData } = await useFetch("https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json")
watch(() => props.primaryStyleId, async (newP, oldP) => {refreshStyles()})
watch(() => props.secondaryStyleId, async (newP, oldP) => {refreshStyles()})
function refreshStyles() {
for(let style of stylesData.value.styles) {
if(style.id == props.primaryStyleId) {
primaryStyle.value = style
}
if(style.id == props.secondaryStyleId) {
secondaryStyle.value = style
}
}
}
refreshStyles()
</script>
<template>
<div style="display: flex;">
<div class="rune-holder">
<div class="rune-slot"><img style="margin: auto;" :src="CDRAGON_BASE + mapPath(primaryStyle.iconPath)" /></div>
<div class="rune-slot" v-for="slot in primaryStyle.slots.slice(0, 1)">
<img width="48" v-for="perk in slot.perks" :class="'rune-img rune-keystone ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')" :src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"/>
</div>
<div class="rune-slot" v-for="slot in primaryStyle.slots.slice(1, 4)">
<img width="48" v-for="perk in slot.perks" :class="'rune-img ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')" :src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"/>
</div>
</div>
<div class="rune-spacer-bar"></div>
<div class="rune-holder" style="align-content: end">
<div class="rune-slot"><img style="margin: auto;" :src="CDRAGON_BASE + mapPath(secondaryStyle.iconPath)" /></div>
<div class="rune-slot" v-for="slot in secondaryStyle.slots.slice(1, 4)">
<img width="48" v-for="perk in slot.perks" :class="'rune-img ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')" :src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"/>
</div>
<!-- <div class="rune-slot mini" v-for="slot in primaryStyle.slots.slice(4, 7)">
<img width="32" v-for="perk in slot.perks" :class="'rune-img ' + (props.selectionIds.includes(perk) ? 'rune-activated' : '')" :src="'https://raw.communitydragon.org/latest/' + mapPath(perks.get(perk).iconPath)"/>
</div> -->
</div>
</div>
</template>
<style>
.rune-holder {
/* align-content: end; */
justify-content: center;
}
.rune-slot {
width: calc(48*3px + 60px);
display: flex;
justify-content: space-between;
margin-top: 40px;
margin-bottom: 40px;
}
.mini {
margin: auto;
width: calc(32*3px + 60px);
margin-top: 10px;
margin-bottom: 10px;
}
.rune-img {
max-width: 100%;
overflow: hidden;
filter: grayscale(1);
border: 1px var(--color-on-surface) solid;
border-radius:50%;
}
.rune-keystone {
border: none;
}
.rune-activated {
filter: none;
}
.rune-spacer-bar {
margin-left: 20px;
margin-right: 20px;
border: 1px var(--color-on-surface) solid;
}
</style>

View File

@@ -0,0 +1,82 @@
<script setup>
const props = defineProps({
// Runes styles: domination, precision, sorcery, inspiration, resolve
runes: {
type: Array,
required: true
},
})
const runes = props.runes
const currentlySelectedPage = ref(runes[0])
const primaryStyles = ref([])
const secondaryStyles = ref([])
const keystoneIds = ref([])
let { data: perks_data } = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/perks.json")
const perks = reactive(new Map())
for(let perk of perks_data.value) {
perks.set(perk.id, perk)
}
let { data: stylesData } = await useFetch(CDRAGON_BASE + "plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json")
for(let style of stylesData.value.styles) {
for(let rune of runes) {
if(style.id == rune.primaryStyle) {
rune.primaryStyleValue = style
primaryStyles.value.push(style)
for(let perk of style.slots[0].perks) {
if(rune.selections.includes(perk)) {
rune.keystoneValue = perk
keystoneIds.value.push(perk)
}
}
}
if(style.id == rune.secondaryStyle) {
secondaryStyles.value.push(style)
rune.secondaryStyleValue = style
}
}
}
function runeSelect(rune) {
currentlySelectedPage.value = rune
}
</script>
<template>
<div style="width: fit-content;">
<RunePage style="margin:auto; width: fit-content;" :primaryStyleId="currentlySelectedPage.primaryStyle" :secondaryStyleId="currentlySelectedPage.secondaryStyle" :selectionIds="currentlySelectedPage.selections" />
<div style="display: flex; margin-top: 20px;">
<div v-for="rune in runes" :class="'rune-selector-entry ' + (rune == currentlySelectedPage ? 'rune-selector-entry-selected' : '')" @click="runeSelect(rune)">
<div style="display: flex; margin-top: 20px;">
<img style="margin: auto;" :src="CDRAGON_BASE + mapPath(rune.primaryStyleValue.iconPath)" />
<img width="34" :src="CDRAGON_BASE + ( mapPath(perks.get(rune.keystoneValue).iconPath))"/>
<img style="margin: auto;" :src="CDRAGON_BASE + mapPath(rune.secondaryStyleValue.iconPath)" />
</div>
<h3 style="text-align: center; margin-top: 10px;">{{ (rune.pickrate * 100).toFixed(2) }}% pick.</h3>
</div>
</div>
</div>
</template>
<style>
.rune-selector-entry {
width: 200px;
height: 120px;
margin-left: 10px;
margin-right: 10px;
border-radius: 8%;
border: 1px solid var(--color-on-surface);
}
.rune-selector-entry:hover {
cursor: pointer;
}
.rune-selector-entry-selected {
background-color: var(--color-surface-darker);
}
</style>

10
frontend/nuxt.config.ts Normal file
View File

@@ -0,0 +1,10 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
devtools: { enabled: true },
css: ['~/assets/css/main.css'],
routeRules: {
'/' : {prerender: true},
'/champion/**' : {swr: true}
}
})

9690
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
frontend/package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"mongodb": "^6.10.0",
"nuxt": "^3.14.1592",
"vue": "latest",
"vue-router": "latest"
}
}

View File

@@ -0,0 +1,15 @@
<script setup>
const route = useRoute()
const championId = route.params.id
const { data : championData } = await useFetch("/api/champion/" + championId)
</script>
<template>
<ChampionTitle style="margin-top: 64px; margin-left: 64px;" :champion-id="championId" :winrate="championData.winrate" :pickrate="championData.pickrate" :game-count="championData.gameCount" />
<!-- <RunePage style="margin-top: 64px; margin-left: 64px;" primaryStyleId="8000" secondaryStyleId="8300" :selectionIds="selections" /> -->
<RuneSelector style="margin-top: 20px; margin-left: 64px;" :runes="championData.runes" />
</template>
<style>
</style>

6
frontend/pages/index.vue Normal file
View File

@@ -0,0 +1,6 @@
<script setup>
</script>
<template>
<ChampionSelector style="margin-top: 64px;"/>
</template>

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,30 @@
import { MongoClient } from 'mongodb'
async function connectToDatabase() {
// Create a MongoClient with a MongoClientOptions object to set the Stable API version
const client = new MongoClient(`mongodb://${process.env.MONGO_USER}:${process.env.MONGO_PASS}@mongo:27017`)
await client.connect()
return client
}
async function fetchLatestPatch(client) {
const database = client.db("patches");
const patches = database.collection("patches");
const latestPatch = await patches.find().limit(1).sort({date:-1}).next()
return latestPatch.patch
}
async function championInfos(client, patch, championId) {
const database = client.db("champions");
const collection = database.collection(patch);
const query = { id:Number(championId) };
const championInfo = await collection.findOne(query);
return championInfo
}
export default defineEventHandler(async (event) => {
const championId = getRouterParam(event, "id")
const client = await connectToDatabase();
const latestPatch = await fetchLatestPatch(client);
const data = await championInfos(client, latestPatch, championId);
await client.close()
return data
})

View File

@@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

4
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

View File

@@ -0,0 +1,8 @@
const CDRAGON_BASE = "https://raw.communitydragon.org/latest/"
function mapPath(assetPath) {
if(assetPath === undefined || assetPath === null) return ""
return assetPath.toLowerCase().replace("/lol-game-data/assets/", "plugins/rcp-be-lol-game-data/global/default/")
}
export { mapPath, CDRAGON_BASE}