Initial commit
This commit is contained in:
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal 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
12
frontend/Dockerfile
Normal 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
75
frontend/README.md
Normal 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
9
frontend/app.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<NuxtRouteAnnouncer />
|
||||
<!-- <NuxtWelcome /> -->
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
</template>
|
||||
40
frontend/assets/css/main.css
Normal file
40
frontend/assets/css/main.css
Normal 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;
|
||||
}
|
||||
50
frontend/components/ChampionSelector.vue
Normal file
50
frontend/components/ChampionSelector.vue
Normal 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>
|
||||
46
frontend/components/ChampionTitle.vue
Normal file
46
frontend/components/ChampionTitle.vue
Normal 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>
|
||||
107
frontend/components/RunePage.vue
Normal file
107
frontend/components/RunePage.vue
Normal 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>
|
||||
82
frontend/components/RuneSelector.vue
Normal file
82
frontend/components/RuneSelector.vue
Normal 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
10
frontend/nuxt.config.ts
Normal 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
9690
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
frontend/package.json
Normal file
18
frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
15
frontend/pages/champion/[id].vue
Normal file
15
frontend/pages/champion/[id].vue
Normal 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
6
frontend/pages/index.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ChampionSelector style="margin-top: 64px;"/>
|
||||
</template>
|
||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
1
frontend/public/robots.txt
Normal file
1
frontend/public/robots.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
30
frontend/server/api/champion/[id].js
Normal file
30
frontend/server/api/champion/[id].js
Normal 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
|
||||
})
|
||||
3
frontend/server/tsconfig.json
Normal file
3
frontend/server/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
||||
4
frontend/tsconfig.json
Normal file
4
frontend/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
||||
8
frontend/utils/cdragon.js
Normal file
8
frontend/utils/cdragon.js
Normal 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}
|
||||
Reference in New Issue
Block a user