Initial commit

This commit is contained in:
Maxim
2025-12-16 22:16:45 +03:00
commit bf03750e76
64 changed files with 7619 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
node_modules
dist
.git
.gitignore
*.log
.env*

36
dota-random-builds-front/.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
.eslintcache
# Cypress
/cypress/videos/
/cypress/screenshots/
# Vitest
__screenshots__/

View File

@@ -0,0 +1,37 @@
# Repository Guidelines
## Project Structure & Module Organization
- `src/main.ts` bootstraps the Vue app, mounts `App.vue`, and registers Pinia + router.
- `src/App.vue` hosts the layout shell; global theme styles live in `src/assets/main.css`.
- `src/pages` holds route-level views (e.g., `HomePage.vue`), wired in `src/router/index.ts`.
- `src/components` contains shareable UI (`Randomizer.vue`); aim to keep these presentational.
- `src/stores` stores state and side effects (e.g., `randomData.ts` for `/api/randomize` and localStorage prefs); static datasets sit in `src/data`.
- `public` serves static assets as-is; `vite.config.ts` configures aliases (`@``src`) and plugins.
## Build, Test, and Development Commands
- `npm run dev` — start Vite dev server with hot reload.
- `npm run build` — type-check via `vue-tsc`, then produce the production bundle.
- `npm run build-only` — production bundle without type-check (useful for quick iteration).
- `npm run preview` — serve the built assets locally to verify production output.
- `npm run type-check` — standalone TS/SFC type validation.
## Coding Style & Naming Conventions
- Vue 3 + TypeScript with `<script setup>`; keep logic typed and colocated with templates/styles.
- Use 2-space indentation, single quotes for strings, and omit semicolons to match existing files.
- Components/pages use PascalCase filenames; route-level views use a `Page` suffix.
- Prefer `@/` alias imports over deep relatives; group imports by library/local.
- Keep styles inside SFC blocks or `src/assets/main.css`; reuse existing CSS variables where possible.
## Testing Guidelines
- No automated tests exist yet; before pushing, run `npm run type-check` and `npm run build` to catch regressions.
- Manual QA: exercise the randomizer flow (toggle skills/aspects, adjust item count, observe loading/error states).
- When adding tests, favor Vitest + Vue Test Utils and mirror component filenames (e.g., `Randomizer.spec.ts`).
## Commit & Pull Request Guidelines
- Git history is minimal; use concise, imperative messages (prefer Conventional Commits such as `feat: add randomizer store`) that describe intent.
- PRs should include: summary of changes, impacted areas (UI/store/API), steps to reproduce/test, and screenshots for UI-facing updates.
- Link to relevant issues, keep PRs small and focused, and call out any API or config assumptions (backend `/api/randomize` endpoint, localStorage usage).
## Environment & API Notes
- The frontend expects an `/api/randomize` POST endpoint; if running locally without the backend, configure a mock or Vite proxy.
- Avoid storing secrets in the repo; prefer runtime env vars and document any required values in the PR description.

View File

@@ -0,0 +1,52 @@
# API Endpoints for Frontend Compatibility
This document defines the minimal backend surface expected by the Vue frontend. Default base path is `/api`; responses are JSON with `Content-Type: application/json`.
## POST /api/randomize
- **Purpose:** Produce a randomized Dota 2 build and optionally return the full data sets used for rendering.
- **Request body:**
- `includeSkills` (boolean) — include a skill build suggestion.
- `includeAspect` (boolean) — include an aspect suggestion.
- `itemsCount` (number) — how many items to return (UI offers 36).
- **Response 200 body:**
- `hero` — object `{ id: string; name: string; primary: string; }`.
- `items` — array of `{ id: string; name: string; }` with length `itemsCount` (also used to hydrate cached items if you return the full pool).
- `skillBuild` (optional) — `{ id: string; name: string; description?: string; }` when `includeSkills` is true.
- `aspect` (optional) — string when `includeAspect` is true.
- `heroes` (optional) — full hero list to cache client-side; same shape as `hero`.
- `skillBuilds` (optional) — full skill build list; same shape as `skillBuild`.
- `aspects` (optional) — array of strings.
- **Example request:**
```json
{
"includeSkills": true,
"includeAspect": false,
"itemsCount": 6
}
```
- **Example response:**
```json
{
"hero": { "id": "pudge", "name": "Pudge", "primary": "strength" },
"items": [
{ "id": "blink", "name": "Blink Dagger" },
{ "id": "bkb", "name": "Black King Bar" }
],
"skillBuild": { "id": "hookdom", "name": "Hook/Rot Max", "description": "Max hook, then rot; early Flesh Heap." },
"heroes": [{ "id": "axe", "name": "Axe", "primary": "strength" }],
"skillBuilds": [{ "id": "support", "name": "Support" }],
"aspects": ["Heroic Might"]
}
```
## Error Handling
- Use standard HTTP status codes (400 invalid input, 500 server error).
- Error payload shape:
```json
{ "message": "Failed to load random build" }
```
- The UI surfaces `message` directly; keep it user-friendly.
## Implementation Notes
- Ensure deterministic constraints: unique `id` per entity and avoid empty arrays when `includeSkills`/`includeAspect` are true.
- If backing data is static, you may return `heroes/items/skillBuilds/aspects` once per request; the frontend caches them after the first successful call.

View File

@@ -0,0 +1,21 @@
FROM node:22-alpine AS build
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci
# Copy source and build
COPY . .
RUN npm run build-only
# Production stage with nginx
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,42 @@
# dota-random-builds-front
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```

1
dota-random-builds-front/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,23 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:8000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}

3088
dota-random-builds-front/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
{
"name": "dota-random-builds-front",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build"
},
"dependencies": {
"axios": "^1.13.2",
"pinia": "^3.0.4",
"vue": "^3.5.25",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.3",
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.2",
"@vue/tsconfig": "^0.8.1",
"npm-run-all2": "^8.0.4",
"typescript": "~5.9.0",
"vite": "^7.2.4",
"vite-plugin-vue-devtools": "^8.0.5",
"vue-tsc": "^3.1.5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,127 @@
<script setup lang="ts">
import { RouterView, RouterLink } from 'vue-router';
</script>
<template>
<main class="container">
<nav class="nav">
<RouterLink to="/" class="nav-link">Randomizer</RouterLink>
<RouterLink to="/build-of-day" class="nav-link">Build of the Day</RouterLink>
</nav>
<RouterView />
</main>
</template>
<style>
#app{
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
/* --- Многослойное Dota/Arcane освещение --- */
background:
/* Бирюзовый левый свет */
radial-gradient(900px 700px at 16% 22%, var(--glow-1), transparent 70%),
/* Синий правый объемный свет */
radial-gradient(800px 600px at 86% 78%, var(--glow-2), transparent 72%),
/* Верхнее золото (очень тонкое, “блеск”) */
radial-gradient(1000px 800px at 50% -20%, var(--gold-glow), transparent 85%),
/* Нижнее мягкое свечение */
radial-gradient(900px 900px at 50% 120%, var(--blue-glow), transparent 75%),
/* Вертикальный общий градиент */
linear-gradient(180deg, var(--bg-1) 0%, var(--bg-2) 100%);
color: #e6f0fa;
position: relative;
overflow: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial;
}
/* --- Лёгкий аркановый блик сверху --- */
#app::before{
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background:
linear-gradient(180deg, rgba(255,255,255,0.06), transparent 45%);
mix-blend-mode: soft-light;
}
/* --- Premium noise — чуть заметный, как в Dota UI --- */
#app::after{
content: '';
position: absolute;
inset: 0;
pointer-events: none;
opacity: var(--noise-opacity);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* Контейнер контента */
.container{
position: relative;
z-index: 5;
width: 100%;
max-width: 1100px;
padding: 32px;
box-sizing: border-box;
/* Мягкое появление */
animation: fadeIn 0.6s ease forwards;
opacity: 0;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
/* mobile */
@media (max-width: 720px){
.container{ padding: 20px; }
}
/* Navigation */
.nav {
display: flex;
gap: 12px;
margin-bottom: 24px;
justify-content: center;
}
.nav-link {
padding: 10px 20px;
border-radius: 10px;
text-decoration: none;
color: #e6f0fa;
font-weight: 600;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
transition: all 0.2s ease;
}
.nav-link:hover {
background: rgba(255, 255, 255, 0.1);
box-shadow: 0 0 12px rgba(79, 195, 247, 0.2);
}
.nav-link.router-link-exact-active {
background: linear-gradient(90deg, #b63a2b, #8b241b);
color: #f8e7c6;
border-color: rgba(182, 58, 43, 0.5);
box-shadow: 0 0 12px rgba(182, 58, 43, 0.45);
}
</style>

View File

@@ -0,0 +1,31 @@
:root{
--bg-1: #050a19;
--bg-2: #0b1f3a;
/* Бирюзово-синие glows */
--glow-1: rgba(79, 209, 197, 0.16);
--glow-2: rgba(56, 189, 248, 0.12);
--glow-3: rgba(96, 165, 250, 0.10);
/* Акценты и текст */
--accent: #4fd1c5;
--muted: #9fb3cc;
--noise-opacity: 0.04;
/* Dota-стильные подсветки */
--gold-glow: rgba(200, 168, 106, 0.12);
--blue-glow: rgba(79,195,247,0.12);
}
*{
box-sizing: border-box;
}
html, body {
/* width: 100%;
height: 100%; */
margin: 0;
padding: 0;
}

View File

@@ -0,0 +1,410 @@
<template>
<div class="randomizer">
<h2>Random Dota 2 Build</h2>
<div class="controls">
<label>
Hero:
<select v-model="selectedHeroId">
<option :value="null">Random</option>
<option v-for="hero in heroes" :key="hero.id" :value="hero.id">
{{ hero.name }}
</option>
</select>
</label>
<label><input type="checkbox" v-model="includeSkills" /> Include skill build</label>
<label><input type="checkbox" v-model="includeAspect" /> Include aspect</label>
<label>
Items to pick:
<select v-model.number="itemsCount">
<option :value="3">3</option>
<option :value="4">4</option>
<option :value="5">5</option>
<option :value="6">6</option>
</select>
</label>
<button @click="requestRandomize" :disabled="!canRandomize">
{{ loading ? 'Loading...' : 'Randomize' }}
</button>
</div>
<p v-if="error" class="error">Не удалось загрузить данные: {{ error }}</p>
<div v-if="result" class="result">
<div>
<h3>Hero</h3>
<div class="hero">{{ result.hero.name }}</div>
<div class="hero-badge">{{ result.hero.primary }}</div>
</div>
<div>
<h3>Items</h3>
<ul>
<li v-for="it in result.items" :key="it.id">{{ it.name }}</li>
</ul>
<div v-if="includeSkills">
<h3>Skill Build</h3>
<div v-if="result?.skillBuild" class="skill-build">
<div v-for="level in skillLevels" :key="level" class="skill-level">
<span class="level-num">{{ level }}</span>
<span class="skill-key" :class="getSkillClass(result.skillBuild[String(level)])">
{{ formatSkill(result.skillBuild[String(level)]) }}
</span>
</div>
</div>
<div v-else class="warning">Скилл билд не был выбран, сгенерируйте челлендж заново.</div>
</div>
<div v-if="includeAspect">
<h3>Aspect</h3>
<div v-if="result.aspect">
{{ result.aspect }}
</div>
<div v-else class="warning">Аспект не был выбран, сгенерируйте челлендж заново.</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useRandomDataStore } from '@/stores/randomData'
const randomDataStore = useRandomDataStore()
const { prefs, result, loading, error, heroes } = storeToRefs(randomDataStore)
const skillLevels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 25]
function formatSkill(skill: string | undefined): string {
if (!skill) return '-'
const map: Record<string, string> = {
q: 'Q',
w: 'W',
e: 'E',
r: 'R',
left_talent: 'L',
right_talent: 'R'
}
return map[skill] ?? skill
}
function getSkillClass(skill: string | undefined): string {
if (!skill) return ''
if (skill === 'r') return 'skill-ult'
if (skill === 'left_talent' || skill === 'right_talent') return 'skill-talent'
return 'skill-basic'
}
const includeSkills = computed({
get: () => prefs.value.includeSkills,
set: (val: boolean) => randomDataStore.setPrefs({ includeSkills: val })
})
const includeAspect = computed({
get: () => prefs.value.includeAspect,
set: (val: boolean) => randomDataStore.setPrefs({ includeAspect: val })
})
const itemsCount = computed({
get: () => prefs.value.itemsCount,
set: (val: number) => randomDataStore.setPrefs({ itemsCount: val })
})
const selectedHeroId = computed({
get: () => prefs.value.heroId ?? null,
set: (val: number | null) => randomDataStore.setPrefs({ heroId: val })
})
const canRandomize = computed(() => !loading.value)
function requestRandomize() {
randomDataStore.randomize({
includeSkills: includeSkills.value,
includeAspect: includeAspect.value,
itemsCount: itemsCount.value,
heroId: selectedHeroId.value
})
}
onMounted(() => {
randomDataStore.loadPrefs()
randomDataStore.loadHeroes()
randomDataStore.randomize()
})
</script>
<style scoped>
:root {
--dota-red: #b63a2b;
--dota-red-glow: rgba(182, 58, 43, 0.45);
--dota-gold: #c8a86a;
--dota-gold-glow: rgba(200, 168, 106, 0.3);
--dota-blue: #4fc3f7;
--dota-blue-glow: rgba(79, 195, 247, 0.35);
--panel: rgba(10, 14, 20, 0.6);
}
/* MAIN BLOCK -------------------------------------------------- */
.randomizer {
padding: 26px;
border-radius: 16px;
background: linear-gradient(180deg, rgba(20, 25, 40, 0.9), rgba(8, 10, 16, 0.92)),
radial-gradient(120% 120% at 0% 0%, rgba(255, 255, 255, 0.05), transparent);
color: #e6f0fa;
border: 1px solid rgba(255, 255, 255, 0.06);
box-shadow:
0 0 18px rgba(0, 0, 0, 0.6),
0 0 32px rgba(0, 0, 0, 0.4),
inset 0 0 20px rgba(255, 255, 255, 0.02);
position: relative;
overflow: hidden;
}
/* Decorative glow line */
.randomizer::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(circle at top left,
rgba(255, 255, 255, 0.10),
transparent 50%);
pointer-events: none;
}
/* TITLE -------------------------------------------------- */
.randomizer>h2 {
text-align: center;
font-weight: 800;
font-size: 1.7rem;
letter-spacing: 1px;
margin-bottom: 18px;
text-shadow: 0 0 6px var(--dota-red-glow);
color: var(--dota-gold);
}
/* CONTROLS -------------------------------------------------- */
.controls {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 22px;
}
.controls label {
background: rgba(255, 255, 255, 0.03);
padding: 10px 12px;
border-radius: 10px;
display: flex;
align-items: center;
gap: 8px;
font-size: 0.95rem;
border: 1px solid rgba(255, 255, 255, 0.06);
transition: 0.2s ease;
}
.controls label:hover {
background: rgba(255, 255, 255, 0.06);
box-shadow: 0 0 14px rgba(255, 255, 255, 0.08);
}
.controls select {
background: rgba(0, 0, 0, 0.35);
color: inherit;
border: 1px solid rgba(255, 255, 255, 0.12);
padding: 6px 8px;
border-radius: 8px;
}
/* BUTTON -------------------------------------------------- */
.controls button {
background: linear-gradient(90deg, var(--dota-red), #8b241b);
color: #f8e7c6;
border: 1px solid rgba(255, 255, 255, 0.12);
padding: 8px 16px;
border-radius: 10px;
font-weight: 700;
cursor: pointer;
box-shadow:
0 0 12px var(--dota-red-glow),
inset 0 0 6px rgba(0, 0, 0, 0.4);
transition: transform .12s ease, box-shadow .12s ease, opacity .12s ease;
}
.controls button:hover {
transform: translateY(-2px);
box-shadow:
0 0 18px var(--dota-red-glow),
inset 0 0 10px rgba(0, 0, 0, 0.5);
}
.controls button:active {
transform: translateY(1px);
opacity: 0.8;
}
.controls button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.error {
color: #f87171;
margin: 0 0 12px;
}
.warning {
color: #fbbf24;
font-weight: 600;
margin: 4px 0;
}
/* RESULT PANEL -------------------------------------------------- */
.result {
margin-top: 12px;
display: grid;
grid-template-columns: 220px 1fr;
gap: 22px;
background: linear-gradient(180deg, rgba(15, 17, 23, 0.9), rgba(6, 8, 13, 0.9));
padding: 18px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.06);
box-shadow: inset 0 0 18px rgba(0, 0, 0, 0.45);
}
/* HERO -------------------------------------------------- */
.hero {
font-weight: 900;
font-size: 1.25rem;
margin-bottom: 8px;
color: var(--dota-blue);
text-shadow: 0 0 6px var(--dota-blue-glow);
}
.hero-badge {
display: inline-block;
padding: 8px 14px;
border-radius: 999px;
background: linear-gradient(90deg,
rgba(255, 255, 255, 0.07),
rgba(255, 255, 255, 0.02));
border: 1px solid rgba(255, 255, 255, 0.08);
color: var(--dota-gold);
font-weight: 600;
}
/* ITEMS -------------------------------------------------- */
.result ul {
list-style: none;
padding: 0;
margin: 10px 0;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.result li {
background: rgba(255, 255, 255, 0.04);
padding: 8px 12px;
border-radius: 999px;
color: #f2f7ff;
font-weight: 600;
border: 1px solid rgba(255, 255, 255, 0.06);
box-shadow:
inset 0 -2px 0 rgba(0, 0, 0, 0.3),
0 0 10px rgba(40, 150, 255, 0.15);
transition: 0.15s ease;
}
.result li:hover {
background: rgba(255, 255, 255, 0.09);
box-shadow:
inset 0 -3px 0 rgba(0, 0, 0, 0.4),
0 0 14px rgba(40, 150, 255, 0.22);
}
/* SKILL + ASPECT -------------------------------------------------- */
h3 {
margin: 8px 0;
font-size: 1.1rem;
font-weight: 700;
color: var(--dota-gold);
text-shadow: 0 0 4px var(--dota-gold-glow);
}
.muted {
color: var(--muted);
font-size: 0.9em;
}
/* SKILL BUILD -------------------------------------------------- */
.skill-build {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 10px 0;
}
.skill-level {
display: flex;
flex-direction: column;
align-items: center;
min-width: 32px;
}
.level-num {
font-size: 0.7rem;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 2px;
}
.skill-key {
background: rgba(255, 255, 255, 0.08);
padding: 6px 10px;
border-radius: 6px;
font-weight: 700;
font-size: 0.9rem;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.skill-basic {
color: #8bc34a;
}
.skill-ult {
color: #ffd54f;
background: rgba(255, 213, 79, 0.15);
border-color: rgba(255, 213, 79, 0.3);
}
.skill-talent {
color: #4fc3f7;
background: rgba(79, 195, 247, 0.15);
border-color: rgba(79, 195, 247, 0.3);
}
/* MOBILE -------------------------------------------------- */
@media (max-width: 720px) {
.result {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,7 @@
export const aspects = [
'Carry',
'Support',
'Roamer',
'Jungler',
'Pusher'
]

View File

@@ -0,0 +1,134 @@
export interface Hero {
id: string
name: string
primary: 'Strength' | 'Agility' | 'Intelligence'
}
export const heroes: Hero[] = [
{ id: 'abaddon', name: 'Abaddon', primary: 'Strength' },
{ id: 'alchemist', name: 'Alchemist', primary: 'Strength' },
{ id: 'axe', name: 'Axe', primary: 'Strength' },
{ id: 'beastmaster', name: 'Beastmaster', primary: 'Strength' },
{ id: 'brewmaster', name: 'Brewmaster', primary: 'Strength' },
{ id: 'bristleback', name: 'Bristleback', primary: 'Strength' },
{ id: 'centaur', name: 'Centaur Warrunner', primary: 'Strength' },
{ id: 'chaos_knight', name: 'Chaos Knight', primary: 'Strength' },
{ id: 'clockwerk', name: 'Clockwerk', primary: 'Strength' },
{ id: 'dawnbreaker', name: 'Dawnbreaker', primary: 'Strength' },
{ id: 'doom', name: 'Doom', primary: 'Strength' },
{ id: 'dragon_knight', name: 'Dragon Knight', primary: 'Strength' },
{ id: 'earth_spirit', name: 'Earth Spirit', primary: 'Strength' },
{ id: 'earthshaker', name: 'Earthshaker', primary: 'Strength' },
{ id: 'elder_titan', name: 'Elder Titan', primary: 'Strength' },
{ id: 'huskar', name: 'Huskar', primary: 'Strength' },
{ id: 'kunkka', name: 'Kunkka', primary: 'Strength' },
{ id: 'legion_commander', name: 'Legion Commander', primary: 'Strength' },
{ id: 'lifestealer', name: 'Lifestealer', primary: 'Strength' },
{ id: 'lycan', name: 'Lycan', primary: 'Strength' },
{ id: 'magnus', name: 'Magnus', primary: 'Strength' },
{ id: 'mars', name: 'Mars', primary: 'Strength' },
{ id: 'night_stalker', name: 'Night Stalker', primary: 'Strength' },
{ id: 'ogre_magi', name: 'Ogre Magi', primary: 'Strength' },
{ id: 'omniknight', name: 'Omniknight', primary: 'Strength' },
{ id: 'phoenix', name: 'Phoenix', primary: 'Strength' },
{ id: 'primal_beast', name: 'Primal Beast', primary: 'Strength' },
{ id: 'pudge', name: 'Pudge', primary: 'Strength' },
{ id: 'slardar', name: 'Slardar', primary: 'Strength' },
{ id: 'snapfire', name: 'Snapfire', primary: 'Strength' },
{ id: 'spirit_breaker', name: 'Spirit Breaker', primary: 'Strength' },
{ id: 'sven', name: 'Sven', primary: 'Strength' },
{ id: 'tidehunter', name: 'Tidehunter', primary: 'Strength' },
{ id: 'timbersaw', name: 'Timbersaw', primary: 'Strength' },
{ id: 'tiny', name: 'Tiny', primary: 'Strength' },
{ id: 'treant', name: 'Treant Protector', primary: 'Strength' },
{ id: 'tusk', name: 'Tusk', primary: 'Strength' },
{ id: 'underlord', name: 'Underlord', primary: 'Strength' },
{ id: 'undying', name: 'Undying', primary: 'Strength' },
{ id: 'wraith_king', name: 'Wraith King', primary: 'Strength' },
/* -------------------- Agility -------------------- */
{ id: 'anti_mage', name: 'Anti-Mage', primary: 'Agility' },
{ id: 'arc_warden', name: 'Arc Warden', primary: 'Agility' },
{ id: 'bloodseeker', name: 'Bloodseeker', primary: 'Agility' },
{ id: 'bounty_hunter', name: 'Bounty Hunter', primary: 'Agility' },
{ id: 'broodmother', name: 'Broodmother', primary: 'Agility' },
{ id: 'clinkz', name: 'Clinkz', primary: 'Agility' },
{ id: 'drow_ranger', name: 'Drow Ranger', primary: 'Agility' },
{ id: 'ember_spirit', name: 'Ember Spirit', primary: 'Agility' },
{ id: 'faceless_void', name: 'Faceless Void', primary: 'Agility' },
{ id: 'gyrocopter', name: 'Gyrocopter', primary: 'Agility' },
{ id: 'hoodwink', name: 'Hoodwink', primary: 'Agility' },
{ id: 'juggernaut', name: 'Juggernaut', primary: 'Agility' },
{ id: 'lone_druid', name: 'Lone Druid', primary: 'Agility' },
{ id: 'luna', name: 'Luna', primary: 'Agility' },
{ id: 'medusa', name: 'Medusa', primary: 'Agility' },
{ id: 'meepo', name: 'Meepo', primary: 'Agility' },
{ id: 'mirana', name: 'Mirana', primary: 'Agility' },
{ id: 'monkey_king', name: 'Monkey King', primary: 'Agility' },
{ id: 'muerta', name: 'Muerta', primary: 'Agility' },
{ id: 'naga_siren', name: 'Naga Siren', primary: 'Agility' },
{ id: 'nyx_assassin', name: 'Nyx Assassin', primary: 'Agility' },
{ id: 'pangolier', name: 'Pangolier', primary: 'Agility' },
{ id: 'phantom_assassin', name: 'Phantom Assassin', primary: 'Agility' },
{ id: 'phantom_lancer', name: 'Phantom Lancer', primary: 'Agility' },
{ id: 'razor', name: 'Razor', primary: 'Agility' },
{ id: 'riki', name: 'Riki', primary: 'Agility' },
{ id: 'shadow_fiend', name: 'Shadow Fiend', primary: 'Agility' },
{ id: 'slark', name: 'Slark', primary: 'Agility' },
{ id: 'sniper', name: 'Sniper', primary: 'Agility' },
{ id: 'spectre', name: 'Spectre', primary: 'Agility' },
{ id: 'templar_assassin', name: 'Templar Assassin', primary: 'Agility' },
{ id: 'terrorblade', name: 'Terrorblade', primary: 'Agility' },
{ id: 'troll_warlord', name: 'Troll Warlord', primary: 'Agility' },
{ id: 'ursa', name: 'Ursa', primary: 'Agility' },
{ id: 'viper', name: 'Viper', primary: 'Agility' },
{ id: 'weaver', name: 'Weaver', primary: 'Agility' },
/* -------------------- Intelligence -------------------- */
{ id: 'ancient_apparition', name: 'Ancient Apparition', primary: 'Intelligence' },
{ id: 'bane', name: 'Bane', primary: 'Intelligence' },
{ id: 'batrider', name: 'Batrider', primary: 'Intelligence' },
{ id: 'chen', name: 'Chen', primary: 'Intelligence' },
{ id: 'crystal_maiden', name: 'Crystal Maiden', primary: 'Intelligence' },
{ id: 'dark_seer', name: 'Dark Seer', primary: 'Intelligence' },
{ id: 'dark_willow', name: 'Dark Willow', primary: 'Intelligence' },
{ id: 'dazzle', name: 'Dazzle', primary: 'Intelligence' },
{ id: 'death_prophet', name: 'Death Prophet', primary: 'Intelligence' },
{ id: 'disruptor', name: 'Disruptor', primary: 'Intelligence' },
{ id: 'enchantress', name: 'Enchantress', primary: 'Intelligence' },
{ id: 'enigma', name: 'Enigma', primary: 'Intelligence' },
{ id: 'grimstroke', name: 'Grimstroke', primary: 'Intelligence' },
{ id: 'invoker', name: 'Invoker', primary: 'Intelligence' },
{ id: 'jakiro', name: 'Jakiro', primary: 'Intelligence' },
{ id: 'keeper_of_the_light', name: 'Keeper of the Light', primary: 'Intelligence' },
{ id: 'leshrac', name: 'Leshrac', primary: 'Intelligence' },
{ id: 'lich', name: 'Lich', primary: 'Intelligence' },
{ id: 'lina', name: 'Lina', primary: 'Intelligence' },
{ id: 'lion', name: 'Lion', primary: 'Intelligence' },
{ id: 'muerta', name: 'Muerta', primary: 'Intelligence' },
{ id: 'nature_prophet', name: "Nature's Prophet", primary: 'Intelligence' },
{ id: 'necrophos', name: 'Necrophos', primary: 'Intelligence' },
{ id: 'ogre_magi_int', name: 'Ogre Magi (Int)', primary: 'Intelligence' },
{ id: 'oracle', name: 'Oracle', primary: 'Intelligence' },
{ id: 'outworld_destroyer', name: 'Outworld Destroyer', primary: 'Intelligence' },
{ id: 'puck', name: 'Puck', primary: 'Intelligence' },
{ id: 'pugna', name: 'Pugna', primary: 'Intelligence' },
{ id: 'queen_of_pain', name: 'Queen of Pain', primary: 'Intelligence' },
{ id: 'rubick', name: 'Rubick', primary: 'Intelligence' },
{ id: 'shadow_demon', name: 'Shadow Demon', primary: 'Intelligence' },
{ id: 'shadow_shaman', name: 'Shadow Shaman', primary: 'Intelligence' },
{ id: 'silencer', name: 'Silencer', primary: 'Intelligence' },
{ id: 'skywrath_mage', name: 'Skywrath Mage', primary: 'Intelligence' },
{ id: 'storm_spirit', name: 'Storm Spirit', primary: 'Intelligence' },
{ id: 'techies', name: 'Techies', primary: 'Intelligence' },
{ id: 'tinker', name: 'Tinker', primary: 'Intelligence' },
{ id: 'visage', name: 'Visage', primary: 'Intelligence' },
{ id: 'void_spirit', name: 'Void Spirit', primary: 'Intelligence' },
{ id: 'warlock', name: 'Warlock', primary: 'Intelligence' },
{ id: 'windranger', name: 'Windranger', primary: 'Intelligence' },
{ id: 'winter_wyvern', name: 'Winter Wyvern', primary: 'Intelligence' },
{ id: 'witch_doctor', name: 'Witch Doctor', primary: 'Intelligence' },
{ id: 'zeus', name: 'Zeus', primary: 'Intelligence' }
];

View File

@@ -0,0 +1,887 @@
export interface Item {
id: string
name: string
}
export const items: Item[] = [
{
"id": "item_town_portal_scroll",
"name": "Town Portal Scroll"
},
{
"id": "item_clarity",
"name": "Clarity"
},
{
"id": "item_faerie_fire",
"name": "Faerie Fire"
},
{
"id": "item_smoke_of_deceit",
"name": "Smoke of Deceit"
},
{
"id": "item_observer_ward",
"name": "Observer Ward"
},
{
"id": "item_sentry_ward",
"name": "Sentry Ward"
},
{
"id": "item_enchanted_mango",
"name": "Enchanted Mango"
},
{
"id": "item_healing_salve",
"name": "Healing Salve"
},
{
"id": "item_tango",
"name": "Tango"
},
{
"id": "item_blood_grenade",
"name": "Blood Grenade"
},
{
"id": "item_dust_of_appearance",
"name": "Dust of Appearance"
},
{
"id": "item_bottle",
"name": "Bottle"
},
{
"id": "item_aghanim_s_shard",
"name": "Aghanim's Shard"
},
{
"id": "item_aghanim_s_blessing",
"name": "Aghanim's Blessing"
},
{
"id": "item_observer_and_sentry_wards",
"name": "Observer and Sentry Wards"
},
{
"id": "item_iron_branch",
"name": "Iron Branch"
},
{
"id": "item_gauntlets_of_strength",
"name": "Gauntlets of Strength"
},
{
"id": "item_slippers_of_agility",
"name": "Slippers of Agility"
},
{
"id": "item_mantle_of_intelligence",
"name": "Mantle of Intelligence"
},
{
"id": "item_circlet",
"name": "Circlet"
},
{
"id": "item_belt_of_strength",
"name": "Belt of Strength"
},
{
"id": "item_band_of_elvenskin",
"name": "Band of Elvenskin"
},
{
"id": "item_robe_of_the_magi",
"name": "Robe of the Magi"
},
{
"id": "item_crown",
"name": "Crown"
},
{
"id": "item_ogre_axe",
"name": "Ogre Axe"
},
{
"id": "item_blade_of_alacrity",
"name": "Blade of Alacrity"
},
{
"id": "item_staff_of_wizardry",
"name": "Staff of Wizardry"
},
{
"id": "item_diadem",
"name": "Diadem"
},
{
"id": "item_quelling_blade",
"name": "Quelling Blade"
},
{
"id": "item_ring_of_protection",
"name": "Ring of Protection"
},
{
"id": "item_infused_raindrops",
"name": "Infused Raindrops"
},
{
"id": "item_orb_of_venom",
"name": "Orb of Venom"
},
{
"id": "item_orb_of_blight",
"name": "Orb of Blight"
},
{
"id": "item_blades_of_attack",
"name": "Blades of Attack"
},
{
"id": "item_orb_of_frost",
"name": "Orb of Frost"
},
{
"id": "item_gloves_of_haste",
"name": "Gloves of Haste"
},
{
"id": "item_chainmail",
"name": "Chainmail"
},
{
"id": "item_helm_of_iron_will",
"name": "Helm of Iron Will"
},
{
"id": "item_broadsword",
"name": "Broadsword"
},
{
"id": "item_blitz_knuckles",
"name": "Blitz Knuckles"
},
{
"id": "item_javelin",
"name": "Javelin"
},
{
"id": "item_claymore",
"name": "Claymore"
},
{
"id": "item_mithril_hammer",
"name": "Mithril Hammer"
},
{
"id": "item_ring_of_regen",
"name": "Ring of Regen"
},
{
"id": "item_sage_s_mask",
"name": "Sage's Mask"
},
{
"id": "item_magic_stick",
"name": "Magic Stick"
},
{
"id": "item_fluffy_hat",
"name": "Fluffy Hat"
},
{
"id": "item_wind_lace",
"name": "Wind Lace"
},
{
"id": "item_cloak",
"name": "Cloak"
},
{
"id": "item_boots_of_speed",
"name": "Boots of Speed"
},
{
"id": "item_gem_of_true_sight",
"name": "Gem of True Sight"
},
{
"id": "item_morbid_mask",
"name": "Morbid Mask"
},
{
"id": "item_voodoo_mask",
"name": "Voodoo Mask"
},
{
"id": "item_shadow_amulet",
"name": "Shadow Amulet"
},
{
"id": "item_ghost_scepter",
"name": "Ghost Scepter"
},
{
"id": "item_blink_dagger",
"name": "Blink Dagger"
},
{
"id": "item_ring_of_health",
"name": "Ring of Health"
},
{
"id": "item_void_stone",
"name": "Void Stone"
},
{
"id": "item_magic_wand",
"name": "Magic Wand"
},
{
"id": "item_null_talisman",
"name": "Null Talisman"
},
{
"id": "item_wraith_band",
"name": "Wraith Band"
},
{
"id": "item_bracer",
"name": "Bracer"
},
{
"id": "item_soul_ring",
"name": "Soul Ring"
},
{
"id": "item_orb_of_corrosion",
"name": "Orb of Corrosion"
},
{
"id": "item_falcon_blade",
"name": "Falcon Blade"
},
{
"id": "item_power_treads",
"name": "Power Treads"
},
{
"id": "item_phase_boots",
"name": "Phase Boots"
},
{
"id": "item_oblivion_staff",
"name": "Oblivion Staff"
},
{
"id": "item_perseverance",
"name": "Perseverance"
},
{
"id": "item_mask_of_madness",
"name": "Mask of Madness"
},
{
"id": "item_hand_of_midas",
"name": "Hand of Midas"
},
{
"id": "item_helm_of_the_dominator",
"name": "Helm of the Dominator"
},
{
"id": "item_boots_of_travel",
"name": "Boots of Travel"
},
{
"id": "item_moon_shard",
"name": "Moon Shard"
},
{
"id": "item_boots_of_travel_2",
"name": "Boots of Travel 2"
},
{
"id": "item_buckler",
"name": "Buckler"
},
{
"id": "item_ring_of_basilius",
"name": "Ring of Basilius"
},
{
"id": "item_headdress",
"name": "Headdress"
},
{
"id": "item_urn_of_shadows",
"name": "Urn of Shadows"
},
{
"id": "item_tranquil_boots",
"name": "Tranquil Boots"
},
{
"id": "item_pavise",
"name": "Pavise"
},
{
"id": "item_arcane_boots",
"name": "Arcane Boots"
},
{
"id": "item_drum_of_endurance",
"name": "Drum of Endurance"
},
{
"id": "item_mekansm",
"name": "Mekansm"
},
{
"id": "item_holy_locket",
"name": "Holy Locket"
},
{
"id": "item_vladmir_s_offering",
"name": "Vladmir's Offering"
},
{
"id": "item_spirit_vessel",
"name": "Spirit Vessel"
},
{
"id": "item_pipe_of_insight",
"name": "Pipe of Insight"
},
{
"id": "item_guardian_greaves",
"name": "Guardian Greaves"
},
{
"id": "item_boots_of_bearing",
"name": "Boots of Bearing"
},
{
"id": "item_parasma",
"name": "Parasma"
},
{
"id": "item_veil_of_discord",
"name": "Veil of Discord"
},
{
"id": "item_glimmer_cape",
"name": "Glimmer Cape"
},
{
"id": "item_force_staff",
"name": "Force Staff"
},
{
"id": "item_aether_lens",
"name": "Aether Lens"
},
{
"id": "item_witch_blade",
"name": "Witch Blade"
},
{
"id": "item_eul_s_scepter_of_divinity",
"name": "Eul's Scepter of Divinity"
},
{
"id": "item_rod_of_atos",
"name": "Rod of Atos"
},
{
"id": "item_dagon",
"name": "Dagon"
},
{
"id": "item_orchid_malevolence",
"name": "Orchid Malevolence"
},
{
"id": "item_solar_crest",
"name": "Solar Crest"
},
{
"id": "item_aghanim_s_scepter",
"name": "Aghanim's Scepter"
},
{
"id": "item_refresher_orb",
"name": "Refresher Orb"
},
{
"id": "item_octarine_core",
"name": "Octarine Core"
},
{
"id": "item_scythe_of_vyse",
"name": "Scythe of Vyse"
},
{
"id": "item_gleipnir",
"name": "Gleipnir"
},
{
"id": "item_wind_waker",
"name": "Wind Waker"
},
{
"id": "item_crystalys",
"name": "Crystalys"
},
{
"id": "item_meteor_hammer",
"name": "Meteor Hammer"
},
{
"id": "item_armlet_of_mordiggian",
"name": "Armlet of Mordiggian"
},
{
"id": "item_skull_basher",
"name": "Skull Basher"
},
{
"id": "item_shadow_blade",
"name": "Shadow Blade"
},
{
"id": "item_desolator",
"name": "Desolator"
},
{
"id": "item_battle_fury",
"name": "Battle Fury"
},
{
"id": "item_ethereal_blade",
"name": "Ethereal Blade"
},
{
"id": "item_nullifier",
"name": "Nullifier"
},
{
"id": "item_monkey_king_bar",
"name": "Monkey King Bar"
},
{
"id": "item_butterfly",
"name": "Butterfly"
},
{
"id": "item_radiance",
"name": "Radiance"
},
{
"id": "item_daedalus",
"name": "Daedalus"
},
{
"id": "item_silver_edge",
"name": "Silver Edge"
},
{
"id": "item_divine_rapier",
"name": "Divine Rapier"
},
{
"id": "item_bloodthorn",
"name": "Bloodthorn"
},
{
"id": "item_abyssal_blade",
"name": "Abyssal Blade"
},
{
"id": "item_revenant_s_brooch",
"name": "Revenant's Brooch"
},
{
"id": "item_disperser",
"name": "Disperser"
},
{
"id": "item_khanda",
"name": "Khanda"
},
{
"id": "item_vanguard",
"name": "Vanguard"
},
{
"id": "item_blade_mail",
"name": "Blade Mail"
},
{
"id": "item_aeon_disk",
"name": "Aeon Disk"
},
{
"id": "item_soul_booster",
"name": "Soul Booster"
},
{
"id": "item_crimson_guard",
"name": "Crimson Guard"
},
{
"id": "item_lotus_orb",
"name": "Lotus Orb"
},
{
"id": "item_black_king_bar",
"name": "Black King Bar"
},
{
"id": "item_hurricane_pike",
"name": "Hurricane Pike"
},
{
"id": "item_manta_style",
"name": "Manta Style"
},
{
"id": "item_linken_s_sphere",
"name": "Linken's Sphere"
},
{
"id": "item_shiva_s_guard",
"name": "Shiva's Guard"
},
{
"id": "item_heart_of_tarrasque",
"name": "Heart of Tarrasque"
},
{
"id": "item_assault_cuirass",
"name": "Assault Cuirass"
},
{
"id": "item_bloodstone",
"name": "Bloodstone"
},
{
"id": "item_helm_of_the_overlord",
"name": "Helm of the Overlord"
},
{
"id": "item_eternal_shroud",
"name": "Eternal Shroud"
},
{
"id": "item_dragon_lance",
"name": "Dragon Lance"
},
{
"id": "item_sange",
"name": "Sange"
},
{
"id": "item_yasha",
"name": "Yasha"
},
{
"id": "item_kaya",
"name": "Kaya"
},
{
"id": "item_echo_sabre",
"name": "Echo Sabre"
},
{
"id": "item_maelstrom",
"name": "Maelstrom"
},
{
"id": "item_diffusal_blade",
"name": "Diffusal Blade"
},
{
"id": "item_mage_slayer",
"name": "Mage Slayer"
},
{
"id": "item_phylactery",
"name": "Phylactery"
},
{
"id": "item_heaven_s_halberd",
"name": "Heaven's Halberd"
},
{
"id": "item_kaya_and_sange",
"name": "Kaya and Sange"
},
{
"id": "item_sange_and_yasha",
"name": "Sange and Yasha"
},
{
"id": "item_yasha_and_kaya",
"name": "Yasha and Kaya"
},
{
"id": "item_satanic",
"name": "Satanic"
},
{
"id": "item_eye_of_skadi",
"name": "Eye of Skadi"
},
{
"id": "item_mjollnir",
"name": "Mjollnir"
},
{
"id": "item_overwhelming_blink",
"name": "Overwhelming Blink"
},
{
"id": "item_swift_blink",
"name": "Swift Blink"
},
{
"id": "item_arcane_blink",
"name": "Arcane Blink"
},
{
"id": "item_harpoon",
"name": "Harpoon"
},
{
"id": "item_ring_of_tarrasque",
"name": "Ring of Tarrasque"
},
{
"id": "item_tiara_of_selemene",
"name": "Tiara of Selemene"
},
{
"id": "item_cornucopia",
"name": "Cornucopia"
},
{
"id": "item_energy_booster",
"name": "Energy Booster"
},
{
"id": "item_vitality_booster",
"name": "Vitality Booster"
},
{
"id": "item_point_booster",
"name": "Point Booster"
},
{
"id": "item_talisman_of_evasion",
"name": "Talisman of Evasion"
},
{
"id": "item_platemail",
"name": "Platemail"
},
{
"id": "item_hyperstone",
"name": "Hyperstone"
},
{
"id": "item_ultimate_orb",
"name": "Ultimate Orb"
},
{
"id": "item_demon_edge",
"name": "Demon Edge"
},
{
"id": "item_mystic_staff",
"name": "Mystic Staff"
},
{
"id": "item_reaver",
"name": "Reaver"
},
{
"id": "item_eaglesong",
"name": "Eaglesong"
},
{
"id": "item_sacred_relic",
"name": "Sacred Relic"
}
]
// export const items: Item[] = [
// { id: 'item_magic_wand', name: 'Magic Wand' },
// { id: 'item_bracer', name: 'Bracer' },
// { id: 'item_wraith_band', name: 'Wraith Band' },
// { id: 'item_null_talisman', name: 'Null Talisman' },
// { id: 'item_soul_ring', name: 'Soul Ring' },
// { id: 'item_falcon_blade', name: 'Falcon Blade' },
// // Boots
// { id: 'item_phase_boots', name: 'Phase Boots' },
// { id: 'item_power_treads', name: 'Power Treads' },
// { id: 'item_arcane_boots', name: 'Arcane Boots' },
// { id: 'item_tranquil_boots', name: 'Tranquil Boots' },
// { id: 'item_boots_of_bearing', name: 'Boots of Bearing' },
// { id: 'item_guardian_greaves', name: 'Guardian Greaves' },
// // Mobility
// { id: 'item_blink', name: 'Blink Dagger' },
// { id: 'item_force_staff', name: 'Force Staff' },
// { id: 'item_hurricane_pike', name: 'Hurricane Pike' },
// { id: 'item_overwhelming_blink', name: 'Overwhelming Blink' },
// { id: 'item_swift_blink', name: 'Swift Blink' },
// { id: 'item_arcane_blink', name: 'Arcane Blink' },
// // Armor / regen aura
// { id: 'item_mekansm', name: 'Mekansm' },
// { id: 'item_pipe', name: 'Pipe of Insight' },
// { id: 'item_vladmir', name: 'Vladmirs Offering' },
// { id: 'item_solar_crest', name: 'Solar Crest' },
// { id: 'item_spirit_vessel', name: 'Spirit Vessel' },
// // Simple DPS items
// { id: 'item_mask_of_madness', name: 'Mask of Madness' },
// { id: 'item_armlet', name: 'Armlet of Mordiggian' },
// { id: 'item_echo_sabre', name: 'Echo Sabre' },
// { id: 'item_skull_basher', name: 'Skull Basher' },
// { id: 'item_desolator', name: 'Desolator' },
// { id: 'item_silver_edge', name: 'Silver Edge' },
// // Magic items
// { id: 'item_veil_of_discord', name: 'Veil of Discord' },
// { id: 'item_aether_lens', name: 'Aether Lens' },
// { id: 'item_kaya', name: 'Kaya' },
// { id: 'item_rod_of_atos', name: 'Rod of Atos' },
// { id: 'item_dagon', name: 'Dagon' },
// // HP items
// { id: 'item_hood_of_defiance', name: 'Hood of Defiance' },
// { id: 'item_heart', name: 'Heart of Tarrasque' },
// // Strong DPS items
// { id: 'item_monkey_king_bar', name: 'Monkey King Bar' },
// { id: 'item_butterfly', name: 'Butterfly' },
// { id: 'item_daedalus', name: 'Daedalus' },
// { id: 'item_greater_crit', name: 'Greater Crit' },
// { id: 'item_bfury', name: 'Battle Fury' },
// { id: 'item_satanic', name: 'Satanic' },
// { id: 'item_mjollnir', name: 'Mjollnir' },
// { id: 'item_radiance', name: 'Radiance' },
// { id: 'item_diffusal_blade', name: 'Diffusal Blade' },
// { id: 'item_abissal_blade', name: 'Abyssal Blade' },
// { id: 'item_maelstrom', name: 'Maelstrom' },
// { id: 'item_mage_slayer', name: 'Mage Slayer' },
// // Tank / defensive items
// { id: 'item_black_king_bar', name: 'Black King Bar' },
// { id: 'item_shivas_guard', name: 'Shivas Guard' },
// { id: 'item_blade_mail', name: 'Blade Mail' },
// { id: 'item_lotus_orb', name: 'Lotus Orb' },
// { id: 'item_vanguard', name: 'Vanguard' },
// { id: 'item_pipe', name: 'Pipe of Insight' },
// { id: 'item_eternal_shroud', name: 'Eternal Shroud' },
// { id: 'item_crimson_guard', name: 'Crimson Guard' },
// { id: 'item_heavens_halberd', name: 'Heavens Halberd' },
// // Mobility / utility
// { id: 'item_cyclone', name: 'Euls Scepter of Divinity' },
// { id: 'item_glimmer_cape', name: 'Glimmer Cape' },
// { id: 'item_ghost', name: 'Ghost Scepter' },
// { id: 'item_ethereal_blade', name: 'Ethereal Blade' },
// { id: 'item_aeon_disk', name: 'Aeon Disk' },
// { id: 'item_boots_of_bearing', name: 'Boots of Bearing' },
// { id: 'item_rod_of_atos', name: 'Rod of Atos' },
// { id: 'item_solar_crest', name: 'Solar Crest' },
// // Healing / support items
// { id: 'item_guardian_greaves', name: 'Guardian Greaves' },
// { id: 'item_holy_locket', name: 'Holy Locket' },
// { id: 'item_arcane_boots', name: 'Arcane Boots' },
// { id: 'item_spirit_vessel', name: 'Spirit Vessel' },
// { id: 'item_medallion_of_courage', name: 'Medallion of Courage' },
// // Magical burst / caster items
// { id: 'item_kaya', name: 'Kaya' },
// { id: 'item_kaya_and_sange', name: 'Kaya and Sange' },
// { id: 'item_yasha_and_kaya', name: 'Yasha and Kaya' },
// { id: 'item_sange_and_yasha', name: 'Sange and Yasha' },
// { id: 'item_octarine_core', name: 'Octarine Core' },
// { id: 'item_scythe_of_vyse', name: 'Scythe of Vyse' },
// { id: 'item_dagon', name: 'Dagon' },
// { id: 'item_rapier', name: 'Divine Rapier' },
// { id: 'item_moon_shard', name: 'Moon Shard' },
// { id: 'item_silver_edge', name: 'Silver Edge' },
// { id: 'item_bloodthorn', name: 'Bloodthorn' },
// { id: 'item_nullifier', name: 'Nullifier' },
// // Intelligence & spellpower
// { id: 'item_refresher', name: 'Refresher Orb' },
// { id: 'item_aghanims_scepter', name: 'Aghanims Scepter' },
// { id: 'item_aghanims_shard', name: 'Aghanims Shard' },
// { id: 'item_witch_blade', name: 'Witch Blade' },
// { id: 'item_gungir', name: 'Gleipnir' },
// // Tank / sustain late game
// { id: 'item_heart', name: 'Heart of Tarrasque' },
// { id: 'item_assault', name: 'Assault Cuirass' },
// { id: 'item_satanic', name: 'Satanic' },
// { id: 'item_harpoon', name: 'Harpoon' },
// // Universal top-tier
// { id: 'item_sphere', name: 'Linkens Sphere' },
// { id: 'item_skadi', name: 'Eye of Skadi' },
// { id: 'item_manta', name: 'Manta Style' },
// { id: 'item_overwhelming_blink', name: 'Overwhelming Blink' },
// { id: 'item_swift_blink', name: 'Swift Blink' },
// // Summon & utility
// { id: 'item_necronomicon', name: 'Necronomicon' },
// { id: 'item_drum', name: 'Drum of Endurance' },
// { id: 'item_helm_of_the_overlord', name: 'Helm of the Overlord' },
// // Roshan rewards
// { id: 'item_aegis', name: 'Aegis of the Immortal' },
// { id: 'item_cheese', name: 'Cheese' },
// { id: 'item_refresher_shard', name: 'Refresher Shard' },
// { id: 'item_aghanims_blessing', name: 'Aghanims Blessing' },
// // Neutral items — Tier 1
// { id: 'item_arcane_ring', name: 'Arcane Ring' },
// { id: 'item_faded_broach', name: 'Faded Broach' },
// { id: 'item_keen_optic', name: 'Keen Optic' },
// // Tier 2
// { id: 'item_grove_bow', name: 'Grove Bow' },
// { id: 'item_pupils_gift', name: 'Pupils Gift' },
// { id: 'item_philosophers_stone', name: 'Philosophers Stone' },
// // Tier 3
// { id: 'item_paladin_sword', name: 'Paladin Sword' },
// { id: 'item_quickening_charm', name: 'Quickening Charm' },
// { id: 'item_spider_legs', name: 'Spider Legs' },
// // Tier 4
// { id: 'item_spell_prism', name: 'Spell Prism' },
// { id: 'item_timeless_relic', name: 'Timeless Relic' },
// { id: 'item_mind_breaker', name: 'Mind Breaker' },
// // Tier 5
// { id: 'item_apex', name: 'Apex' },
// { id: 'item_ex_machina', name: 'Ex Machina' },
// { id: 'item_mirror_shield', name: 'Mirror Shield' },
// { id: 'item_book_of_shadows', name: 'Book of Shadows' },
// { id: 'item_seer_stone', name: 'Seer Stone' }
// ];

View File

@@ -0,0 +1,12 @@
export interface SkillBuildOption {
id: string
name: string
description?: string
}
export const skillBuilds: SkillBuildOption[] = [
{ id: 'q_first', name: 'Max Q first', description: 'Prioritize first ability' },
{ id: 'w_first', name: 'Max W first', description: 'Prioritize second ability' },
{ id: 'e_first', name: 'Max E first', description: 'Prioritize third ability' },
{ id: 'balanced', name: 'Balanced', description: 'Evenly distribute points' }
]

View File

@@ -0,0 +1,13 @@
import "@/assets/main.css"
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

View File

@@ -0,0 +1,306 @@
<template>
<div class="build-of-day">
<h2>Build of the Day</h2>
<p v-if="loading" class="loading">Loading...</p>
<p v-if="error" class="error">Failed to load: {{ error }}</p>
<div v-if="build" class="result">
<div class="date-badge">{{ formattedDate }}</div>
<div class="hero-section">
<h3>Hero</h3>
<div class="hero">{{ build.hero.name }}</div>
<div class="hero-badge">{{ build.hero.primary }}</div>
</div>
<div class="build-section">
<h3>Items</h3>
<ul>
<li v-for="it in build.items" :key="it.id">{{ it.name }}</li>
</ul>
<h3>Skill Build</h3>
<div class="skill-build">
<div v-for="level in skillLevels" :key="level" class="skill-level">
<span class="level-num">{{ level }}</span>
<span class="skill-key" :class="getSkillClass(build.skillBuild[String(level)])">
{{ formatSkill(build.skillBuild[String(level)]) }}
</span>
</div>
</div>
<div v-if="build.aspect">
<h3>Aspect</h3>
<div class="aspect">{{ build.aspect }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useRandomDataStore } from '@/stores/randomData'
const randomDataStore = useRandomDataStore()
const { buildOfDay: build, buildOfDayLoading: loading, buildOfDayError: error } = storeToRefs(randomDataStore)
const skillLevels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 25]
const formattedDate = computed(() => {
if (!build.value?.date) return ''
const d = new Date(build.value.date)
return d.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
})
})
function formatSkill(skill: string | undefined): string {
if (!skill) return '-'
const map: Record<string, string> = {
q: 'Q',
w: 'W',
e: 'E',
r: 'R',
left_talent: 'L',
right_talent: 'R'
}
return map[skill] ?? skill
}
function getSkillClass(skill: string | undefined): string {
if (!skill) return ''
if (skill === 'r') return 'skill-ult'
if (skill === 'left_talent' || skill === 'right_talent') return 'skill-talent'
return 'skill-basic'
}
onMounted(() => {
randomDataStore.fetchBuildOfDay()
})
</script>
<style scoped>
:root {
--dota-red: #b63a2b;
--dota-red-glow: rgba(182, 58, 43, 0.45);
--dota-gold: #c8a86a;
--dota-gold-glow: rgba(200, 168, 106, 0.3);
--dota-blue: #4fc3f7;
--dota-blue-glow: rgba(79, 195, 247, 0.35);
--panel: rgba(10, 14, 20, 0.6);
}
/* MAIN BLOCK -------------------------------------------------- */
.build-of-day {
padding: 26px;
border-radius: 16px;
background: linear-gradient(180deg, rgba(20, 25, 40, 0.9), rgba(8, 10, 16, 0.92)),
radial-gradient(120% 120% at 0% 0%, rgba(255, 255, 255, 0.05), transparent);
color: #e6f0fa;
border: 1px solid rgba(255, 255, 255, 0.06);
box-shadow:
0 0 18px rgba(0, 0, 0, 0.6),
0 0 32px rgba(0, 0, 0, 0.4),
inset 0 0 20px rgba(255, 255, 255, 0.02);
position: relative;
overflow: hidden;
}
/* Decorative glow line */
.build-of-day::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(circle at top left,
rgba(255, 255, 255, 0.10),
transparent 50%);
pointer-events: none;
}
/* TITLE -------------------------------------------------- */
.build-of-day > h2 {
text-align: center;
font-weight: 800;
font-size: 1.7rem;
letter-spacing: 1px;
margin-bottom: 18px;
text-shadow: 0 0 6px var(--dota-red-glow);
color: var(--dota-gold);
}
.loading {
text-align: center;
color: var(--dota-blue);
}
.error {
color: #f87171;
text-align: center;
}
/* DATE BADGE -------------------------------------------------- */
.date-badge {
text-align: center;
font-size: 1.1rem;
font-weight: 600;
color: var(--dota-gold);
margin-bottom: 18px;
padding: 10px 20px;
background: rgba(200, 168, 106, 0.1);
border: 1px solid rgba(200, 168, 106, 0.3);
border-radius: 999px;
display: inline-block;
width: 100%;
box-sizing: border-box;
}
/* RESULT PANEL -------------------------------------------------- */
.result {
margin-top: 12px;
background: linear-gradient(180deg, rgba(15, 17, 23, 0.9), rgba(6, 8, 13, 0.9));
padding: 18px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.06);
box-shadow: inset 0 0 18px rgba(0, 0, 0, 0.45);
}
.hero-section {
margin-bottom: 20px;
}
/* HERO -------------------------------------------------- */
.hero {
font-weight: 900;
font-size: 1.25rem;
margin-bottom: 8px;
color: var(--dota-blue);
text-shadow: 0 0 6px var(--dota-blue-glow);
}
.hero-badge {
display: inline-block;
padding: 8px 14px;
border-radius: 999px;
background: linear-gradient(90deg,
rgba(255, 255, 255, 0.07),
rgba(255, 255, 255, 0.02));
border: 1px solid rgba(255, 255, 255, 0.08);
color: var(--dota-gold);
font-weight: 600;
}
/* ITEMS -------------------------------------------------- */
.result ul {
list-style: none;
padding: 0;
margin: 10px 0;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.result li {
background: rgba(255, 255, 255, 0.04);
padding: 8px 12px;
border-radius: 999px;
color: #f2f7ff;
font-weight: 600;
border: 1px solid rgba(255, 255, 255, 0.06);
box-shadow:
inset 0 -2px 0 rgba(0, 0, 0, 0.3),
0 0 10px rgba(40, 150, 255, 0.15);
transition: 0.15s ease;
}
.result li:hover {
background: rgba(255, 255, 255, 0.09);
box-shadow:
inset 0 -3px 0 rgba(0, 0, 0, 0.4),
0 0 14px rgba(40, 150, 255, 0.22);
}
/* SKILL + ASPECT -------------------------------------------------- */
h3 {
margin: 8px 0;
font-size: 1.1rem;
font-weight: 700;
color: var(--dota-gold);
text-shadow: 0 0 4px var(--dota-gold-glow);
}
.aspect {
background: rgba(255, 255, 255, 0.04);
padding: 8px 14px;
border-radius: 10px;
color: #f2f7ff;
font-weight: 600;
border: 1px solid rgba(255, 255, 255, 0.06);
display: inline-block;
}
/* SKILL BUILD -------------------------------------------------- */
.skill-build {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 10px 0;
}
.skill-level {
display: flex;
flex-direction: column;
align-items: center;
min-width: 32px;
}
.level-num {
font-size: 0.7rem;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 2px;
}
.skill-key {
background: rgba(255, 255, 255, 0.08);
padding: 6px 10px;
border-radius: 6px;
font-weight: 700;
font-size: 0.9rem;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.skill-basic {
color: #8bc34a;
}
.skill-ult {
color: #ffd54f;
background: rgba(255, 213, 79, 0.15);
border-color: rgba(255, 213, 79, 0.3);
}
.skill-talent {
color: #4fc3f7;
background: rgba(79, 195, 247, 0.15);
border-color: rgba(79, 195, 247, 0.3);
}
/* MOBILE -------------------------------------------------- */
@media (max-width: 720px) {
.build-of-day {
padding: 18px;
}
}
</style>

View File

@@ -0,0 +1,24 @@
<template>
<div class="home-page">
<h1>Dota Random Builds</h1>
<Randomizer />
</div>
</template>
<script setup lang="ts">
import Randomizer from '@/components/Randomizer.vue'
</script>
<style scoped>
.home-page {
padding: 20px;
max-width: 900px;
margin: 0 auto;
min-height: 100%;
min-height: 100%;
}
h1 {
margin-bottom: 12px
}
</style>

View File

@@ -0,0 +1,21 @@
import HomePage from '@/pages/HomePage.vue'
import BuildOfTheDayPage from '@/pages/BuildOfTheDayPage.vue'
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'HomePage',
component: HomePage
},
{
path: '/build-of-day',
name: 'BuildOfTheDayPage',
component: BuildOfTheDayPage
}
]
});
export default router

View File

@@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

View File

@@ -0,0 +1,139 @@
import axios from 'axios'
import { defineStore } from 'pinia'
export interface Hero {
id: number
name: string
primary: string
}
export interface Item {
id: number
name: string
}
export type SkillBuild = Record<string, string>
export interface RandomizeResult {
hero: Hero
items: Item[]
skillBuild?: SkillBuild
aspect?: string
}
export interface BuildOfDayResult {
date: string
hero: Hero
items: Item[]
skillBuild: SkillBuild
aspect?: string
}
type RandomizePayload = {
includeSkills: boolean
includeAspect: boolean
itemsCount: number
heroId?: number | null
}
const PREFS_KEY = 'randomizer:prefs'
const BASE_URL = import.meta.env.VITE_API_URL || ''
export const useRandomDataStore = defineStore('randomData', {
state: () => ({
heroes: [] as Hero[],
result: null as RandomizeResult | null,
loading: false,
error: null as string | null,
prefs: {
includeSkills: false,
includeAspect: false,
itemsCount: 6,
heroId: null as number | null
} as RandomizePayload,
buildOfDay: null as BuildOfDayResult | null,
buildOfDayLoading: false,
buildOfDayError: null as string | null
}),
actions: {
loadPrefs() {
try {
const raw = localStorage.getItem(PREFS_KEY)
if (!raw) return
const parsed = JSON.parse(raw)
this.prefs = {
includeSkills: Boolean(parsed.includeSkills),
includeAspect: Boolean(parsed.includeAspect),
itemsCount: Number(parsed.itemsCount) || 6,
heroId: parsed.heroId ?? null
}
} catch (err) {
console.warn('Failed to load prefs', err)
}
},
async loadHeroes() {
try {
const { data } = await axios.get<Hero[]>(`${BASE_URL}/api/heroes`)
this.heroes = data
} catch (err) {
console.warn('Failed to load heroes', err)
}
},
savePrefs() {
try {
localStorage.setItem(PREFS_KEY, JSON.stringify(this.prefs))
} catch (err) {
console.warn('Failed to save prefs', err)
}
},
setPrefs(patch: Partial<RandomizePayload>) {
this.prefs = { ...this.prefs, ...patch }
this.savePrefs()
},
async randomize(overrides?: Partial<RandomizePayload>) {
if (this.loading) return
this.loading = true
this.error = null
const payload: RandomizePayload = {
...this.prefs,
...overrides
}
// sync prefs with overrides so UI reflects latest choice
this.prefs = payload
this.savePrefs()
try {
const { data } = await axios.post<RandomizeResult>(`${BASE_URL}/api/randomize`, { ...payload })
this.result = data
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to load random build'
this.error = message
} finally {
this.loading = false
}
},
async fetchBuildOfDay() {
if (this.buildOfDayLoading) return
this.buildOfDayLoading = true
this.buildOfDayError = null
try {
const { data } = await axios.get<BuildOfDayResult>(`${BASE_URL}/api/build-of-day`)
this.buildOfDay = data
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to load build of the day'
this.buildOfDayError = message
} finally {
this.buildOfDayLoading = false
}
}
}
})

View File

@@ -0,0 +1,12 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

View File

@@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node24/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

View File

@@ -0,0 +1,18 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})