Frontend updates and changes
- Remove items/runes separate pages, put everything into a "build" page. - Show summoner spells. - Add a build variant selector, that for now only selects runes
This commit is contained in:
698
frontend/components/build/Viewer.vue
Normal file
698
frontend/components/build/Viewer.vue
Normal file
@@ -0,0 +1,698 @@
|
||||
<script setup lang="ts">
|
||||
import { isEmpty, deepClone } from '~/utils/helpers'
|
||||
|
||||
const props = defineProps<{
|
||||
runes: Array<{
|
||||
count: number
|
||||
primaryStyle: number
|
||||
secondaryStyle: number
|
||||
selections: Array<number>
|
||||
pickrate: number
|
||||
}>
|
||||
builds: Builds
|
||||
summonerSpells?: Array<{ id: number; count: number; pickrate: number }> // API data when available
|
||||
}>()
|
||||
|
||||
// Constants
|
||||
const ITEMS_API_URL = CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/items.json'
|
||||
const SUMMONER_SPELLS_URL =
|
||||
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/summoner-spells.json'
|
||||
|
||||
// State
|
||||
const currentlySelectedBuild = ref(0)
|
||||
|
||||
// Fetch items
|
||||
const { data: items } = useFetch<Array<Item>>(ITEMS_API_URL, {
|
||||
lazy: true,
|
||||
server: false
|
||||
})
|
||||
const itemMap = ref<Map<number, Item>>(new Map())
|
||||
|
||||
watch(
|
||||
items,
|
||||
newItems => {
|
||||
if (Array.isArray(newItems)) {
|
||||
const map = new Map<number, Item>()
|
||||
for (const item of newItems) {
|
||||
if (item?.id) {
|
||||
map.set(item.id, item)
|
||||
}
|
||||
}
|
||||
itemMap.value = map
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Fetch summoner spells
|
||||
const { data: summonerSpellsData } = useFetch<Array<SummonerSpell>>(SUMMONER_SPELLS_URL, {
|
||||
lazy: true,
|
||||
server: false
|
||||
})
|
||||
const summonerSpellMap = ref<Map<number, SummonerSpell>>(new Map())
|
||||
|
||||
watch(
|
||||
summonerSpellsData,
|
||||
newData => {
|
||||
if (Array.isArray(newData)) {
|
||||
const map = new Map<number, SummonerSpell>()
|
||||
for (const spell of newData) {
|
||||
if (spell?.id) {
|
||||
map.set(spell.id, spell)
|
||||
}
|
||||
}
|
||||
summonerSpellMap.value = map
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Mock summoner spells data if not provided by API
|
||||
const mockSummonerSpells = computed(() => {
|
||||
if (props.summonerSpells && props.summonerSpells.length > 0) {
|
||||
return props.summonerSpells
|
||||
}
|
||||
// Default mock data based on common summoner spells
|
||||
return [
|
||||
{ id: 4, count: 1000, pickrate: 0.45 }, // Flash
|
||||
{ id: 7, count: 800, pickrate: 0.35 }, // Heal
|
||||
{ id: 14, count: 600, pickrate: 0.15 }, // Ignite
|
||||
{ id: 3, count: 200, pickrate: 0.05 } // Exhaust
|
||||
]
|
||||
})
|
||||
|
||||
// Builds management
|
||||
const builds = ref<Builds>(deepClone(props.builds))
|
||||
|
||||
watch(
|
||||
() => props.builds,
|
||||
newBuilds => {
|
||||
builds.value = deepClone(newBuilds)
|
||||
trimBuilds(builds.value)
|
||||
trimLateGameItems(builds.value)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
trimBuilds(builds.value)
|
||||
trimLateGameItems(builds.value)
|
||||
})
|
||||
|
||||
function trimBuilds(builds: Builds): void {
|
||||
if (!builds?.tree?.children) return
|
||||
builds.tree.children.splice(1, builds.tree.children.length - 1)
|
||||
if (builds.tree.children[0]?.children) {
|
||||
builds.tree.children[0].children.splice(1, builds.tree.children[0].children.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
function trimLateGameItems(builds: Builds): void {
|
||||
if (!builds?.tree || isEmpty(builds.lateGame)) return
|
||||
|
||||
function trimLateGameItemsFromTree(tree: ItemTree): void {
|
||||
const foundIndex = builds.lateGame.findIndex(x => x.data === tree.data)
|
||||
if (foundIndex !== -1) {
|
||||
builds.lateGame.splice(foundIndex, 1)
|
||||
}
|
||||
for (const child of tree.children || []) {
|
||||
trimLateGameItemsFromTree(child)
|
||||
}
|
||||
}
|
||||
trimLateGameItemsFromTree(builds.tree)
|
||||
}
|
||||
|
||||
// Get first core item for build variant display
|
||||
const firstCoreItems = computed(() => {
|
||||
const result: number[] = []
|
||||
for (let i = 0; i < props.runes.length; i++) {
|
||||
const tree = builds.value?.tree
|
||||
if (tree?.children?.[0]?.data) {
|
||||
result.push(tree.children[0].data)
|
||||
} else if (tree?.data) {
|
||||
result.push(tree.data)
|
||||
} else {
|
||||
result.push(0)
|
||||
}
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
// Rune styles
|
||||
const primaryStyles: Ref<Array<PerkStyle>> = ref(Array(props.runes.length))
|
||||
const secondaryStyles: Ref<Array<PerkStyle>> = ref(Array(props.runes.length))
|
||||
const keystoneIds: Ref<Array<number>> = ref(Array(props.runes.length))
|
||||
|
||||
const { data: perks_data }: PerksResponse = await useFetch(
|
||||
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/perks.json'
|
||||
)
|
||||
const perks = reactive(new Map())
|
||||
for (const perk of perks_data.value) {
|
||||
perks.set(perk.id, perk)
|
||||
}
|
||||
|
||||
const { data: stylesData }: PerkStylesResponse = await useFetch(
|
||||
CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json'
|
||||
)
|
||||
|
||||
function refreshStylesKeystones() {
|
||||
for (const style of stylesData.value.styles) {
|
||||
for (const rune of props.runes) {
|
||||
if (style.id == rune.primaryStyle) {
|
||||
primaryStyles.value[props.runes.indexOf(rune)] = style
|
||||
for (const perk of style.slots[0].perks) {
|
||||
if (rune.selections.includes(perk)) {
|
||||
keystoneIds.value[props.runes.indexOf(rune)] = perk
|
||||
}
|
||||
}
|
||||
}
|
||||
if (style.id == rune.secondaryStyle) {
|
||||
secondaryStyles.value[props.runes.indexOf(rune)] = style
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.runes,
|
||||
() => {
|
||||
currentlySelectedBuild.value = 0
|
||||
primaryStyles.value = Array(props.runes.length)
|
||||
secondaryStyles.value = Array(props.runes.length)
|
||||
keystoneIds.value = Array(props.runes.length)
|
||||
refreshStylesKeystones()
|
||||
}
|
||||
)
|
||||
|
||||
refreshStylesKeystones()
|
||||
|
||||
function selectBuild(index: number) {
|
||||
currentlySelectedBuild.value = index
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="build-viewer">
|
||||
<!-- Global Build Variant Selector -->
|
||||
<div class="build-variant-selector">
|
||||
<div
|
||||
v-for="(_, i) in runes"
|
||||
:key="i"
|
||||
:class="['build-variant-card', { selected: i === currentlySelectedBuild }]"
|
||||
@click="selectBuild(i)"
|
||||
>
|
||||
<div class="variant-content">
|
||||
<!-- Keystone -->
|
||||
<NuxtImg
|
||||
v-if="keystoneIds[i] && perks.get(keystoneIds[i])"
|
||||
class="variant-keystone"
|
||||
:src="CDRAGON_BASE + mapPath(perks.get(keystoneIds[i]).iconPath)"
|
||||
/>
|
||||
<!-- First core item -->
|
||||
<NuxtImg
|
||||
v-if="itemMap.get(firstCoreItems[i])"
|
||||
class="variant-item"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(firstCoreItems[i])!.iconPath)"
|
||||
/>
|
||||
<div v-else class="variant-item-placeholder" />
|
||||
</div>
|
||||
<span class="variant-pickrate">{{ (runes[i].pickrate * 100).toFixed(1) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Build Content -->
|
||||
<div class="build-content">
|
||||
<!-- Left Column: Summoner Spells + Runes -->
|
||||
<div class="build-left-column">
|
||||
<!-- Summoner Spells -->
|
||||
<div class="summoner-spells-section">
|
||||
<h3 class="section-title">Summoner Spells</h3>
|
||||
<div class="summoner-spells-row">
|
||||
<div
|
||||
v-for="(spell, i) in mockSummonerSpells.slice(0, 2)"
|
||||
:key="i"
|
||||
class="summoner-spell-item"
|
||||
>
|
||||
<NuxtImg
|
||||
v-if="summonerSpellMap.get(spell.id)"
|
||||
class="summoner-spell-img"
|
||||
:src="CDRAGON_BASE + mapPath(summonerSpellMap.get(spell.id)!.iconPath)"
|
||||
/>
|
||||
<div v-else class="summoner-spell-placeholder" />
|
||||
<span class="spell-pickrate">{{ (spell.pickrate * 100).toFixed(0) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rune Page -->
|
||||
<div class="rune-section">
|
||||
<h3 class="section-title">Runes</h3>
|
||||
<div class="rune-page-wrapper">
|
||||
<RunePage
|
||||
v-if="runes[currentlySelectedBuild]"
|
||||
:primary-style-id="runes[currentlySelectedBuild].primaryStyle"
|
||||
:secondary-style-id="runes[currentlySelectedBuild].secondaryStyle"
|
||||
:selection-ids="runes[currentlySelectedBuild].selections"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Items -->
|
||||
<div class="build-right-column">
|
||||
<h3 class="section-title">Items</h3>
|
||||
|
||||
<!-- Start/Support + Boots Container -->
|
||||
<div class="item-row-group">
|
||||
<!-- Start Items -->
|
||||
<div v-if="!builds.suppItems" class="item-row">
|
||||
<span class="item-row-label">Start</span>
|
||||
<div class="item-row-content">
|
||||
<div v-for="item in builds.start" :key="item.data" class="item-cell">
|
||||
<NuxtImg
|
||||
v-if="itemMap.get(item.data)"
|
||||
class="item-img"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data)!.iconPath)"
|
||||
/>
|
||||
<div v-else class="item-placeholder" />
|
||||
<span class="item-pickrate"
|
||||
>{{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Support Items -->
|
||||
<div v-if="builds.suppItems" class="item-row">
|
||||
<span class="item-row-label">Support</span>
|
||||
<div class="item-row-content">
|
||||
<div v-for="item in builds.suppItems" :key="item.data" class="item-cell">
|
||||
<NuxtImg
|
||||
v-if="itemMap.get(item.data)"
|
||||
class="item-img"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data)!.iconPath)"
|
||||
/>
|
||||
<div v-else class="item-placeholder" />
|
||||
<span class="item-pickrate"
|
||||
>{{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Boots (regular or rush) -->
|
||||
<div class="item-row">
|
||||
<span class="item-row-label">{{
|
||||
builds.bootsFirst > 0.5 ? 'Boots Rush' : 'Boots'
|
||||
}}</span>
|
||||
<div class="item-row-content">
|
||||
<div v-for="item in builds.boots.slice(0, 2)" :key="item.data" class="item-cell">
|
||||
<NuxtImg
|
||||
v-if="itemMap.get(item.data)"
|
||||
class="item-img"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data)!.iconPath)"
|
||||
/>
|
||||
<div v-else class="item-placeholder" />
|
||||
<span class="item-pickrate"
|
||||
>{{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Core Items Tree -->
|
||||
<div class="item-row">
|
||||
<span class="item-row-label">Core</span>
|
||||
<ItemTree :tree="builds.tree" />
|
||||
</div>
|
||||
|
||||
<!-- Late Game -->
|
||||
<div class="item-row">
|
||||
<span class="item-row-label">Late Game</span>
|
||||
<div class="item-row-content">
|
||||
<div v-for="item in builds.lateGame.slice(0, 6)" :key="item.data" class="item-cell">
|
||||
<NuxtImg
|
||||
v-if="itemMap.get(item.data)"
|
||||
class="item-img"
|
||||
:src="CDRAGON_BASE + mapPath(itemMap.get(item.data)!.iconPath)"
|
||||
/>
|
||||
<div v-else class="item-placeholder" />
|
||||
<span class="item-pickrate"
|
||||
>{{ ((item.count / builds.tree.count) * 100).toFixed(0) }}%</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.build-viewer {
|
||||
width: 100%;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Build Variant Selector - Global */
|
||||
.build-variant-selector {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
padding: 0 12px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.build-variant-selector::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.build-variant-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px 14px;
|
||||
background: var(--color-surface-darker);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.build-variant-card:hover {
|
||||
border-color: var(--color-on-surface);
|
||||
}
|
||||
|
||||
.build-variant-card.selected {
|
||||
border-color: #4a9eff;
|
||||
background: linear-gradient(135deg, rgba(74, 158, 255, 0.15), rgba(74, 158, 255, 0.05));
|
||||
}
|
||||
|
||||
.variant-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.variant-keystone {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.variant-item {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(183, 184, 225, 0.3);
|
||||
}
|
||||
|
||||
.variant-item-placeholder {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: var(--color-surface);
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(183, 184, 225, 0.3);
|
||||
}
|
||||
|
||||
.variant-pickrate {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-on-surface);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Main Build Content */
|
||||
.build-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 80px;
|
||||
padding: 0 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: #4a9eff;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Rune page selector dots */
|
||||
.rune-page-selector {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.rune-page-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-surface);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.rune-page-dot:hover {
|
||||
background: var(--color-on-surface);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.rune-page-dot.active {
|
||||
background: #4a9eff;
|
||||
}
|
||||
|
||||
/* Left Column: Runes + Summoner Spells */
|
||||
.build-left-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.rune-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.rune-page-wrapper {
|
||||
transform: scale(0.85);
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
/* Summoner Spells - Compact */
|
||||
.summoner-spells-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.summoner-spells-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.summoner-spell-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.summoner-spell-img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-on-surface);
|
||||
}
|
||||
|
||||
.summoner-spell-placeholder {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: var(--color-surface);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-on-surface);
|
||||
}
|
||||
|
||||
.spell-pickrate {
|
||||
font-size: 0.7rem;
|
||||
color: var(--color-on-surface);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Right Column: Items */
|
||||
.build-right-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.item-row-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.item-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.item-row-label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-on-surface);
|
||||
opacity: 0.6;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.item-row-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.item-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-on-surface);
|
||||
}
|
||||
|
||||
.item-placeholder {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: var(--color-surface);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-on-surface);
|
||||
}
|
||||
|
||||
.item-pickrate {
|
||||
font-size: 0.65rem;
|
||||
color: var(--color-on-surface);
|
||||
opacity: 0.6;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.item-separator {
|
||||
width: 1px;
|
||||
height: 48px;
|
||||
background: var(--color-on-surface);
|
||||
opacity: 0.3;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
/* Responsive: Mobile */
|
||||
@media only screen and (max-width: 900px) {
|
||||
.build-content {
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.build-left-column {
|
||||
order: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.build-right-column {
|
||||
order: 2;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.build-variant-card {
|
||||
min-width: 70px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.variant-keystone,
|
||||
.variant-item {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.variant-item-placeholder {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.variant-pickrate {
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.rune-section,
|
||||
.summoner-spells-section {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rune-page-wrapper {
|
||||
transform: scale(1);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.item-img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.item-placeholder {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.summoner-spell-img,
|
||||
.summoner-spell-placeholder {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 400px) {
|
||||
.build-variant-selector {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.build-variant-card {
|
||||
min-width: 60px;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.variant-keystone,
|
||||
.variant-item {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.variant-item-placeholder {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user