feature: more fixes to make it amazing
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
@@ -14,27 +12,17 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
titleColor: {
|
||||
type: String,
|
||||
default: 'text-white'
|
||||
},
|
||||
batteryLevel: {
|
||||
type: Number,
|
||||
default: 80
|
||||
}
|
||||
})
|
||||
|
||||
const currentTime = ref(new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' }))
|
||||
|
||||
setInterval(() => {
|
||||
currentTime.value = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' })
|
||||
}, 1000)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="h-12 border-b border-zinc-800 flex items-center justify-between px-4 bg-zinc-900/90 backdrop-blur z-20 shrink-0 select-none">
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="font-bold text-sm uppercase tracking-tight" :class="titleColor">{{ title }}</h1>
|
||||
<h1 class="font-bold text-sm uppercase tracking-tight text-white">{{ title }}</h1>
|
||||
|
||||
<!-- Badge - can be string or array -->
|
||||
<template v-if="badge">
|
||||
@@ -68,9 +56,6 @@ setInterval(() => {
|
||||
<div class="absolute -right-0.75 top-1/2 -translate-y-1/2 w-0.5 h-1.5 bg-zinc-600 rounded-r-[1px]"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Time -->
|
||||
<span class="font-mono text-[10px] text-zinc-500">{{ currentTime }}</span>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
56
src/components/layout/PageLayout.vue
Normal file
56
src/components/layout/PageLayout.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
import Header from './Header.vue'
|
||||
import StatusBar from './StatusBar.vue'
|
||||
import BackgroundGrid from '../ui/BackgroundGrid.vue'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
badge: {
|
||||
type: [String, Array],
|
||||
default: ''
|
||||
},
|
||||
batteryLevel: {
|
||||
type: Number,
|
||||
default: 80
|
||||
},
|
||||
gridOpacity: {
|
||||
type: Number,
|
||||
default: 10
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative w-full h-full bg-zinc-950 text-white font-sans flex flex-col selection:bg-red-500/30">
|
||||
|
||||
<div class="absolute inset-0 z-0">
|
||||
<BackgroundGrid :opacity="gridOpacity" />
|
||||
<slot name="background" />
|
||||
</div>
|
||||
|
||||
<div class="relative shrink-0 z-20">
|
||||
<Header
|
||||
:title="title"
|
||||
:badge="badge"
|
||||
:subtitle="subtitle"
|
||||
:battery-level="batteryLevel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="relative flex-1 min-h-0 z-10 flex flex-col">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div class="relative shrink-0 z-20">
|
||||
<StatusBar />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,11 +3,11 @@ import { usePokedexNavigation } from '../../composables/usePokedexNavigation'
|
||||
import LeftScreen from './LeftScreen.vue'
|
||||
import RightControls from './RightControls.vue'
|
||||
|
||||
const { searchQuery, handleKeypad, handleMainButton, handleBackButton, goHome, handleNext, handlePrev } = usePokedexNavigation()
|
||||
const { searchQuery, handleKeypad, handleMainButton, handleBackButton, goHome, handleNext, handlePrev, handleSearchGo, handleSearchClear } = usePokedexNavigation()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-zinc-900 h-screen w-screen flex items-center justify-center p-8 overflow-hidden font-sans select-none">
|
||||
<div class="pokeball-bg h-screen w-screen flex items-center justify-center p-8 overflow-hidden font-sans select-none">
|
||||
|
||||
<!-- Main Red Chassis -->
|
||||
<div class="relative w-[1200px] h-[900px] bg-[#dc0a2d] rounded-3xl shadow-[0_50px_100px_-20px_rgba(0,0,0,0.5)] flex flex-row overflow-hidden border-b-8 border-r-8 border-[#89061c]">
|
||||
@@ -35,8 +35,19 @@ const { searchQuery, handleKeypad, handleMainButton, handleBackButton, goHome, h
|
||||
@go-home="goHome"
|
||||
@next="handleNext"
|
||||
@prev="handlePrev"
|
||||
@search-go="handleSearchGo"
|
||||
@search-clear="handleSearchClear"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pokeball-bg {
|
||||
background-color: #ffffff;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='80' height='80' xmlns='http://www.w3.org/2000/svg'%3E%3Cg opacity='0.8'%3E%3Ccircle cx='20' cy='20' r='12' fill='none' stroke='%23e4e4e7' stroke-width='2'/%3E%3Cpath d='M 8 20 L 32 20' stroke='%23e4e4e7' stroke-width='2'/%3E%3Ccircle cx='20' cy='20' r='4' fill='none' stroke='%23d4d4d8' stroke-width='2'/%3E%3Ccircle cx='20' cy='20' r='2' fill='%23d4d4d8'/%3E%3Ccircle cx='60' cy='60' r='8' fill='none' stroke='%23e4e4e7' stroke-width='1.5'/%3E%3Cpath d='M 52 60 L 68 60' stroke='%23e4e4e7' stroke-width='1.5'/%3E%3Ccircle cx='60' cy='60' r='3' fill='none' stroke='%23d4d4d8' stroke-width='1.5'/%3E%3Ccircle cx='60' cy='60' r='1.5' fill='%23d4d4d8'/%3E%3Ccircle cx='50' cy='15' r='6' fill='none' stroke='%23e4e4e7' stroke-width='1.2'/%3E%3Cpath d='M 44 15 L 56 15' stroke='%23e4e4e7' stroke-width='1.2'/%3E%3Ccircle cx='50' cy='15' r='2' fill='%23d4d4d8'/%3E%3Ccircle cx='15' cy='55' r='7' fill='none' stroke='%23e4e4e7' stroke-width='1.2'/%3E%3Cpath d='M 8 55 L 22 55' stroke='%23e4e4e7' stroke-width='1.2'/%3E%3Ccircle cx='15' cy='55' r='2.5' fill='%23d4d4d8'/%3E%3C/g%3E%3C/svg%3E");
|
||||
background-size: 80px 80px;
|
||||
background-repeat: repeat;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { appState } from '../../store'
|
||||
import { SIDE_MENU_ITEMS } from '../../constants/ui'
|
||||
import PillButton from '../ui/PillButton.vue'
|
||||
@@ -9,6 +10,8 @@ import PokemonInfoCard from '../ui/PokemonInfoCard.vue'
|
||||
import SearchDisplay from '../ui/SearchDisplay.vue'
|
||||
import StatusButton from '../ui/StatusButton.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
searchQuery: {
|
||||
type: String,
|
||||
@@ -22,8 +25,16 @@ const emit = defineEmits([
|
||||
'back-button',
|
||||
'go-home',
|
||||
'next',
|
||||
'prev'
|
||||
'prev',
|
||||
'search-go',
|
||||
'search-clear'
|
||||
])
|
||||
|
||||
function navigateToMenuItem(item) {
|
||||
if (item.route) {
|
||||
router.push(item.route)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -53,8 +64,23 @@ const emit = defineEmits([
|
||||
<span class="text-[9px] font-bold opacity-60">{{ appState.currentPokemon ? 'ACTIVE' : 'READY' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Home Mode -->
|
||||
<div v-if="!appState.currentPokemon" class="flex flex-col items-center justify-center opacity-60 flex-1">
|
||||
<!-- Page Mode - Show Icon and Title -->
|
||||
<div v-if="!appState.currentPokemon && appState.pageDisplay.icon" class="flex flex-col items-center justify-center gap-3 flex-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="text-[#2b2b2b] opacity-70"
|
||||
>
|
||||
<path :d="appState.pageDisplay.icon" />
|
||||
</svg>
|
||||
<div class="text-[11px] tracking-[0.2em] font-bold opacity-70">{{ appState.pageDisplay.title }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Default Mode - No Page Info -->
|
||||
<div v-else-if="!appState.currentPokemon" class="flex flex-col items-center justify-center opacity-60 flex-1">
|
||||
<div class="text-[10px] tracking-widest font-bold">WAITING INPUT</div>
|
||||
</div>
|
||||
|
||||
@@ -83,9 +109,9 @@ const emit = defineEmits([
|
||||
<!-- Compact Black Tactile Numpad -->
|
||||
<div class="grid grid-cols-3 gap-2.5 content-start">
|
||||
<NumpadKey v-for="n in 9" :key="n" :label="n" @click="emit('keypad', n)" />
|
||||
<NumpadKey label="DEL" variant="danger" @click="emit('back-button')" />
|
||||
<NumpadKey label="DEL" variant="danger" @click="searchQuery ? emit('search-clear') : null" />
|
||||
<NumpadKey label="0" @click="emit('keypad', 0)" />
|
||||
<NumpadKey label="GO" variant="success" @click="emit('main-button')" />
|
||||
<NumpadKey label="GO" variant="success" @click="searchQuery ? emit('search-go') : null" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -93,10 +119,10 @@ const emit = defineEmits([
|
||||
<!-- RIGHT: Slim Blue Buttons List -->
|
||||
<div class="w-[140px] flex flex-col gap-2 mr-4">
|
||||
<SideMenuButton
|
||||
v-for="(label, i) in SIDE_MENU_ITEMS"
|
||||
v-for="(item, i) in SIDE_MENU_ITEMS"
|
||||
:key="i"
|
||||
:label="label"
|
||||
@click="i === 0 ? emit('go-home') : null"
|
||||
:label="item.label"
|
||||
@click="navigateToMenuItem(item)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -107,18 +133,16 @@ const emit = defineEmits([
|
||||
|
||||
<!-- Back/Home Buttons -->
|
||||
<div class="flex gap-4 mb-2">
|
||||
<!-- Home / Execute -->
|
||||
<PillButton @click="emit('main-button')">
|
||||
<span class="text-[9px] font-black tracking-wider text-black/60">{{ searchQuery ? 'ENTER' : 'HOME' }}</span>
|
||||
<svg v-if="!searchQuery" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="currentColor" class="text-black/60"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="currentColor" class="text-black/60"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
<!-- Home -->
|
||||
<PillButton @click="emit('go-home')">
|
||||
<span class="text-[9px] font-black tracking-wider text-black/60">HOME</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="currentColor" class="text-black/60"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>
|
||||
</PillButton>
|
||||
|
||||
<!-- Back / Clear -->
|
||||
<!-- Back -->
|
||||
<PillButton @click="emit('back-button')">
|
||||
<span class="text-[9px] font-black tracking-wider text-black/60">{{ searchQuery ? 'CLEAR' : 'BACK' }}</span>
|
||||
<svg v-if="!searchQuery" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" class="text-black/60"><path d="M9 14 4 9l5-5"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" class="text-black/60"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
<span class="text-[9px] font-black tracking-wider text-black/60">BACK</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" class="text-black/60"><path d="M9 14 4 9l5-5"/></svg>
|
||||
</PillButton>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
leftText: {
|
||||
type: String,
|
||||
default: 'Global Database'
|
||||
},
|
||||
rightText: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
rightTextColor: {
|
||||
type: String,
|
||||
default: 'text-zinc-600'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-6 bg-zinc-900 border-t border-zinc-800 flex items-center justify-between px-4 text-[9px] text-zinc-600 uppercase tracking-widest shrink-0">
|
||||
<span>{{ leftText }}</span>
|
||||
<span :class="rightTextColor">{{ rightText }}</span>
|
||||
<span>Global Database</span>
|
||||
<span>v2.0.4 - Stable</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { appState } from '../store'
|
||||
import { MAX_SEARCH_LENGTH } from '../constants/ui'
|
||||
|
||||
const searchQuery = ref('')
|
||||
|
||||
export const homeMenuState = ref({
|
||||
selectedIndex: 0,
|
||||
itemCount: 0,
|
||||
onSelect: null,
|
||||
onMoveUp: null,
|
||||
onMoveDown: null
|
||||
})
|
||||
|
||||
export function usePokedexNavigation() {
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
function handleKeypad(num) {
|
||||
if (searchQuery.value.length < MAX_SEARCH_LENGTH) {
|
||||
@@ -19,7 +28,24 @@ export function usePokedexNavigation() {
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.go(-1)
|
||||
const path = route.path
|
||||
|
||||
if (path === '/') {
|
||||
return
|
||||
}
|
||||
|
||||
const segments = path.split('/').filter(segment => segment !== '')
|
||||
|
||||
if (segments.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (segments.length === 1) {
|
||||
router.push('/')
|
||||
} else {
|
||||
segments.pop()
|
||||
router.push('/' + segments.join('/'))
|
||||
}
|
||||
}
|
||||
|
||||
function executeSearch() {
|
||||
@@ -36,8 +62,18 @@ export function usePokedexNavigation() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearchGo() {
|
||||
executeSearch()
|
||||
}
|
||||
|
||||
function handleSearchClear() {
|
||||
clearSearch()
|
||||
}
|
||||
|
||||
function handleMainButton() {
|
||||
if (searchQuery.value) {
|
||||
if (route.path === '/' && homeMenuState.value.onSelect) {
|
||||
homeMenuState.value.onSelect()
|
||||
} else if (searchQuery.value) {
|
||||
executeSearch()
|
||||
} else {
|
||||
goHome()
|
||||
@@ -45,22 +81,24 @@ export function usePokedexNavigation() {
|
||||
}
|
||||
|
||||
function handleBackButton() {
|
||||
if (searchQuery.value) {
|
||||
clearSearch()
|
||||
} else {
|
||||
if (route.path !== '/') {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
if (appState.currentPokemon) {
|
||||
if (route.path === '/' && homeMenuState.value.onMoveDown) {
|
||||
homeMenuState.value.onMoveDown()
|
||||
} else if (appState.currentPokemon) {
|
||||
let nextId = appState.currentPokemon.id + 1
|
||||
router.push(`/pokemon/${nextId}`)
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrev() {
|
||||
if (appState.currentPokemon && appState.currentPokemon.id > 1) {
|
||||
if (route.path === '/' && homeMenuState.value.onMoveUp) {
|
||||
homeMenuState.value.onMoveUp()
|
||||
} else if (appState.currentPokemon && appState.currentPokemon.id > 1) {
|
||||
let prevId = appState.currentPokemon.id - 1
|
||||
router.push(`/pokemon/${prevId}`)
|
||||
}
|
||||
@@ -76,6 +114,8 @@ export function usePokedexNavigation() {
|
||||
handleMainButton,
|
||||
handleBackButton,
|
||||
handleNext,
|
||||
handlePrev
|
||||
handlePrev,
|
||||
handleSearchGo,
|
||||
handleSearchClear
|
||||
}
|
||||
}
|
||||
|
||||
2
src/constants/layout.js
Normal file
2
src/constants/layout.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export const SCREEN_MAX_WIDTH = 380
|
||||
export const SCREEN_MAX_HEIGHT = 680
|
||||
@@ -18,6 +18,12 @@ export const DIMENSIONS = {
|
||||
|
||||
export const MAX_SEARCH_LENGTH = 4
|
||||
|
||||
export const SIDE_MENU_ITEMS = ['POKEDEX', 'FAVORITES', 'SETTINGS', 'MAP', 'BAG']
|
||||
export const SIDE_MENU_ITEMS = [
|
||||
{ label: 'POKÉMON', route: '/pokemon' },
|
||||
{ label: 'MAPS', route: '/maps' },
|
||||
{ label: 'GAMES', route: '/games' },
|
||||
{ label: 'MOVES', route: '/moves' },
|
||||
{ label: 'SETTINGS', route: '/settings' }
|
||||
]
|
||||
|
||||
export const KEYPAD_KEYS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
|
||||
|
||||
24
src/pages/Games.vue
Normal file
24
src/pages/Games.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { appState } from '../store'
|
||||
import PageLayout from '../components/layout/PageLayout.vue'
|
||||
|
||||
onMounted(() => {
|
||||
appState.setPageDisplay(
|
||||
'M21 6H3c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-10 7H8v3H6v-3H3v-2h3V8h2v3h3v2zm4.5 2c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4-3c-.83 0-1.5-.67-1.5-1.5S18.67 9 19.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z',
|
||||
'GAMES'
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageLayout
|
||||
title="GAMES"
|
||||
badge=""
|
||||
subtitle=""
|
||||
>
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<p class="text-zinc-500 text-sm">Coming Soon</p>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
@@ -1,152 +1,249 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ColorThief from 'colorthief'
|
||||
import { generateThemeColors } from '../utils/theme'
|
||||
import { appState } from '../store'
|
||||
import Header from '../components/layout/Header.vue'
|
||||
import StatusBar from '../components/layout/StatusBar.vue'
|
||||
import BackgroundGrid from '../components/ui/BackgroundGrid.vue'
|
||||
import PageLayout from '../components/layout/PageLayout.vue'
|
||||
import { homeMenuState } from '../composables/usePokedexNavigation'
|
||||
import { useScreenActions } from '../composables/useScreenActions'
|
||||
|
||||
const router = useRouter()
|
||||
const { clearActions } = useScreenActions()
|
||||
|
||||
// Clear active pokemon state when entering Dashboard
|
||||
appState.clearCurrentPokemon()
|
||||
|
||||
const { setButtonActions } = useScreenActions()
|
||||
|
||||
// Reusing the featured list logic but integrated into the dashboard
|
||||
const featuredPokemon = ref([
|
||||
{ id: 1, name: 'Bulbasaur', type: 'Grass', customBg: '#2d2d2d', customText: '#ffffff' },
|
||||
{ id: 4, name: 'Charmander', type: 'Fire', customBg: '#2d2d2d', customText: '#ffffff' },
|
||||
{ id: 7, name: 'Squirtle', type: 'Water', customBg: '#2d2d2d', customText: '#ffffff' },
|
||||
{ id: 25, name: 'Pikachu', type: 'Electric', customBg: '#2d2d2d', customText: '#ffffff' },
|
||||
{ id: 133, name: 'Eevee', type: 'Normal', customBg: '#2d2d2d', customText: '#ffffff' },
|
||||
{ id: 150, name: 'Mewtwo', type: 'Psychic', customBg: '#2d2d2d', customText: '#ffffff' },
|
||||
const menuItems = ref([
|
||||
{
|
||||
id: 'pokemons',
|
||||
label: 'POKÉMON',
|
||||
subtitle: 'Browse Global Database',
|
||||
icon: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-5.5-2.5l7.51-3.22-7.52-3.22 3.22 7.52 3.21-7.52-6.42 6.44z',
|
||||
route: '/pokemon',
|
||||
color: 'red'
|
||||
},
|
||||
{
|
||||
id: 'maps',
|
||||
label: 'MAPS',
|
||||
subtitle: 'Explore Regions',
|
||||
icon: 'M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z',
|
||||
route: '/maps',
|
||||
color: 'blue'
|
||||
},
|
||||
{
|
||||
id: 'games',
|
||||
label: 'GAMES',
|
||||
subtitle: 'Mini Games & Challenges',
|
||||
icon: 'M21 6H3c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-10 7H8v3H6v-3H3v-2h3V8h2v3h3v2zm4.5 2c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4-3c-.83 0-1.5-.67-1.5-1.5S18.67 9 19.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z',
|
||||
route: '/games',
|
||||
color: 'green'
|
||||
},
|
||||
{
|
||||
id: 'moves',
|
||||
label: 'MOVES',
|
||||
subtitle: 'Attack & Ability Database',
|
||||
icon: 'M9 2L7.17 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2h-3.17L15 2H9zm3 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z M12 14l-1.25-2.75L8 10l2.75-1.25L12 6l1.25 2.75L16 10l-2.75 1.25z',
|
||||
route: '/moves',
|
||||
color: 'purple'
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: 'SETTINGS',
|
||||
subtitle: 'System Configuration',
|
||||
icon: 'M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z',
|
||||
route: '/settings',
|
||||
color: 'zinc'
|
||||
}
|
||||
])
|
||||
|
||||
function getImageUrl(id) {
|
||||
return `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${id}.png`
|
||||
}
|
||||
const selectedIndex = ref(0)
|
||||
|
||||
function extractColors() {
|
||||
const colorThief = new ColorThief()
|
||||
featuredPokemon.value.forEach(poke => {
|
||||
const img = new Image()
|
||||
img.crossOrigin = 'anonymous'
|
||||
img.src = getImageUrl(poke.id)
|
||||
img.onload = () => {
|
||||
const color = colorThief.getColor(img)
|
||||
const { background, text } = generateThemeColors(color)
|
||||
poke.customBg = background
|
||||
poke.customText = text
|
||||
function movePrev() {
|
||||
if (selectedIndex.value > 0) {
|
||||
selectedIndex.value--
|
||||
} else {
|
||||
selectedIndex.value = menuItems.value.length - 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function goToRandomPokemon() {
|
||||
const randomId = Math.floor(Math.random() * 1025) + 1
|
||||
router.push(`/pokemon/${randomId}`)
|
||||
function moveNext() {
|
||||
if (selectedIndex.value < menuItems.value.length - 1) {
|
||||
selectedIndex.value++
|
||||
} else {
|
||||
selectedIndex.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
function selectItem() {
|
||||
const item = menuItems.value[selectedIndex.value]
|
||||
if (item.route) {
|
||||
router.push(item.route)
|
||||
} else {
|
||||
console.log(`${item.label} - Coming Soon`)
|
||||
}
|
||||
}
|
||||
|
||||
function getIconColorClass(isSelected) {
|
||||
return isSelected ? 'text-zinc-400' : 'text-zinc-600'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
extractColors()
|
||||
setButtonActions([
|
||||
{ buttonNumber: 3, label: 'RANDOM', action: goToRandomPokemon },
|
||||
{ buttonNumber: 5, label: 'SETTINGS', action: () => console.log('Settings - TBA') }
|
||||
])
|
||||
clearActions()
|
||||
|
||||
appState.setPageDisplay(
|
||||
'M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z',
|
||||
'MAIN MENU'
|
||||
)
|
||||
|
||||
homeMenuState.value = {
|
||||
selectedIndex: selectedIndex.value,
|
||||
itemCount: menuItems.value.length,
|
||||
onSelect: selectItem,
|
||||
onMoveUp: movePrev,
|
||||
onMoveDown: moveNext
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyboard)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
homeMenuState.value = {
|
||||
selectedIndex: 0,
|
||||
itemCount: 0,
|
||||
onSelect: null,
|
||||
onMoveUp: null,
|
||||
onMoveDown: null
|
||||
}
|
||||
|
||||
window.removeEventListener('keydown', handleKeyboard)
|
||||
})
|
||||
|
||||
function handleKeyboard(e) {
|
||||
switch(e.key) {
|
||||
case 'ArrowLeft':
|
||||
case 'a':
|
||||
case 'A':
|
||||
e.preventDefault()
|
||||
movePrev()
|
||||
break
|
||||
case 'ArrowRight':
|
||||
case 'd':
|
||||
case 'D':
|
||||
e.preventDefault()
|
||||
moveNext()
|
||||
break
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
e.preventDefault()
|
||||
selectItem()
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute inset-0 bg-zinc-950 text-white font-sans flex flex-col overflow-hidden selection:bg-red-500/30">
|
||||
|
||||
<BackgroundGrid :opacity="10" />
|
||||
|
||||
<!-- Top Bar -->
|
||||
<Header
|
||||
title="POKÉDEX"
|
||||
badge="OS v2.0"
|
||||
subtitle="Global Database"
|
||||
:battery-level="80"
|
||||
/>
|
||||
|
||||
<!-- Scrollable Dashboard Content -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-5 z-10 custom-scrollbar pb-8">
|
||||
|
||||
<!-- Welcome / Status Card -->
|
||||
<div class="bg-gradient-to-br from-zinc-800 to-zinc-900 rounded-xl p-5 border border-zinc-700 shadow-lg relative overflow-hidden group hover:border-zinc-500 transition-colors">
|
||||
<div class="relative z-10">
|
||||
<h2 class="font-bold text-xl mb-1 text-white">System Ready</h2>
|
||||
<p class="text-xs text-zinc-400 max-w-[75%] leading-relaxed">
|
||||
Database synchronized. All regions accessible.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Decorative Elements -->
|
||||
<div class="absolute -right-2 -bottom-6 text-zinc-800/50 transform rotate-12 group-hover:rotate-0 transition-transform duration-500">
|
||||
<svg width="100" height="100" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-5.5-2.5l7.51-3.22-7.52-3.22 3.22 7.52 3.21-7.52-6.42 6.44z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats Grid -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="bg-zinc-900/50 border border-zinc-800 p-3 rounded-lg flex flex-col gap-1 items-center justify-center hover:bg-zinc-800 transition-colors cursor-default">
|
||||
<span class="text-2xl font-mono font-bold text-white">1025+</span>
|
||||
<span class="text-[9px] uppercase tracking-widest text-zinc-500">Total Found</span>
|
||||
</div>
|
||||
<div class="bg-zinc-900/50 border border-zinc-800 p-3 rounded-lg flex flex-col gap-1 items-center justify-center hover:bg-zinc-800 transition-colors cursor-default">
|
||||
<span class="text-2xl font-mono font-bold text-blue-400">98%</span>
|
||||
<span class="text-[9px] uppercase tracking-widest text-zinc-500">Battery</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Featured Section -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-3 px-1">
|
||||
<h3 class="text-[10px] font-bold uppercase text-zinc-500 tracking-[0.2em] flex items-center gap-2">
|
||||
<span class="w-1 h-3 bg-red-600 rounded-full"></span>
|
||||
Priority Targets
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
<router-link
|
||||
v-for="poke in featuredPokemon"
|
||||
:key="poke.id"
|
||||
:to="`/pokemon/${poke.id}`"
|
||||
class="bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 flex items-center gap-4 hover:border-zinc-500 hover:bg-zinc-800 transition-all group relative overflow-hidden"
|
||||
<PageLayout
|
||||
title="POKEDEX"
|
||||
badge=""
|
||||
subtitle=""
|
||||
>
|
||||
<!-- Row Hover Highlight -->
|
||||
<div class="absolute inset-0 bg-white/5 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
<div class="flex flex-col items-center justify-center px-4 py-4 flex-1 min-h-0">
|
||||
|
||||
<div class="w-10 h-10 bg-zinc-950 rounded border border-zinc-800 flex items-center justify-center relative shadow-inner">
|
||||
<img :src="getImageUrl(poke.id)" class="w-8 h-8 object-contain z-10 group-hover:scale-125 transition-transform duration-300" />
|
||||
<div class="mb-4 text-center">
|
||||
<p class="text-xs text-zinc-400 uppercase tracking-wide font-bold">Main Menu</p>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 z-10">
|
||||
<div class="flex justify-between items-baseline">
|
||||
<span class="font-bold text-sm text-zinc-200 group-hover:text-white">{{ poke.name }}</span>
|
||||
<span class="text-[10px] font-mono text-zinc-600 group-hover:text-zinc-400">#{{ String(poke.id).padStart(3, '0') }}</span>
|
||||
<div class="relative w-full max-w-sm h-48 mb-4 flex items-center justify-center overflow-hidden">
|
||||
<div
|
||||
v-for="(item, index) in menuItems"
|
||||
:key="item.id"
|
||||
:class="[
|
||||
'absolute transition-all duration-300 ease-out',
|
||||
'border-2 rounded-lg flex flex-col items-center justify-center gap-3',
|
||||
'bg-zinc-900',
|
||||
index === selectedIndex
|
||||
? 'z-20 scale-100 opacity-100 border-zinc-600 w-40 h-44'
|
||||
: index === selectedIndex - 1 || (selectedIndex === 0 && index === menuItems.length - 1)
|
||||
? 'z-10 scale-75 opacity-40 border-zinc-800 w-40 h-44 -translate-x-36'
|
||||
: index === selectedIndex + 1 || (selectedIndex === menuItems.length - 1 && index === 0)
|
||||
? 'z-10 scale-75 opacity-40 border-zinc-800 w-40 h-44 translate-x-36'
|
||||
: 'scale-50 opacity-0 pointer-events-none w-40 h-44'
|
||||
]"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'rounded-lg flex items-center justify-center bg-zinc-950 border-2 transition-all duration-300',
|
||||
index === selectedIndex ? 'w-14 h-14 border-zinc-700' : 'w-12 h-12 border-zinc-800'
|
||||
]"
|
||||
>
|
||||
<svg
|
||||
:class="[
|
||||
'transition-all duration-300',
|
||||
index === selectedIndex ? 'w-8 h-8' : 'w-7 h-7',
|
||||
getIconColorClass(index === selectedIndex)
|
||||
]"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path :d="item.icon" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="text-center px-2">
|
||||
<div
|
||||
:class="[
|
||||
'font-bold mb-1 transition-all duration-300',
|
||||
index === selectedIndex ? 'text-base text-white' : 'text-sm text-zinc-500'
|
||||
]"
|
||||
>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'text-xs transition-all duration-300',
|
||||
index === selectedIndex ? 'text-zinc-400' : 'text-zinc-600'
|
||||
]"
|
||||
>
|
||||
{{ item.subtitle }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 mt-0.5">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-zinc-600 group-hover:bg-green-500 transition-colors"></span>
|
||||
<span class="text-[9px] text-zinc-500 uppercase tracking-wide">{{ poke.type }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pr-1 opacity-0 group-hover:opacity-100 transition-all -translate-x-4 group-hover:translate-x-0 z-10">
|
||||
<svg class="w-4 h-4 text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
||||
<div class="flex items-center gap-1 mb-4">
|
||||
<div
|
||||
v-for="(item, index) in menuItems"
|
||||
:key="item.id"
|
||||
:class="[
|
||||
'rounded-full transition-all duration-300',
|
||||
index === selectedIndex ? 'bg-zinc-400 w-3 h-1.5' : 'bg-zinc-700 w-1.5 h-1.5'
|
||||
]"
|
||||
></div>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<div class="flex items-center gap-3 text-[10px] text-zinc-500">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="flex gap-1">
|
||||
<div class="w-5 h-5 rounded border border-zinc-700 bg-zinc-900 flex items-center justify-center">
|
||||
<svg class="w-2.5 h-2.5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="w-5 h-5 rounded border border-zinc-700 bg-zinc-900 flex items-center justify-center">
|
||||
<svg class="w-2.5 h-2.5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<span class="uppercase tracking-wide">Navigate</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="px-2 py-1 rounded border border-green-700 bg-green-900 text-green-400 font-bold text-[9px]">
|
||||
OK
|
||||
</div>
|
||||
<span class="uppercase tracking-wide">Select</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<StatusBar right-text="v2.0.4 - Stable" />
|
||||
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
|
||||
24
src/pages/Maps.vue
Normal file
24
src/pages/Maps.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { appState } from '../store'
|
||||
import PageLayout from '../components/layout/PageLayout.vue'
|
||||
|
||||
onMounted(() => {
|
||||
appState.setPageDisplay(
|
||||
'M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z',
|
||||
'MAPS'
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageLayout
|
||||
title="MAPS"
|
||||
badge=""
|
||||
subtitle=""
|
||||
>
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<p class="text-zinc-500 text-sm">Coming Soon</p>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
24
src/pages/Moves.vue
Normal file
24
src/pages/Moves.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { appState } from '../store'
|
||||
import PageLayout from '../components/layout/PageLayout.vue'
|
||||
|
||||
onMounted(() => {
|
||||
appState.setPageDisplay(
|
||||
'M9 2L7.17 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2h-3.17L15 2H9zm3 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z M12 14l-1.25-2.75L8 10l2.75-1.25L12 6l1.25 2.75L16 10l-2.75 1.25z',
|
||||
'MOVES'
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageLayout
|
||||
title="MOVES"
|
||||
badge=""
|
||||
subtitle=""
|
||||
>
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<p class="text-zinc-500 text-sm">Coming Soon</p>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
@@ -1,15 +1,19 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { appState } from '../store'
|
||||
import { useScreenActions } from '../composables/useScreenActions'
|
||||
import Header from '../components/layout/Header.vue'
|
||||
import StatusBar from '../components/layout/StatusBar.vue'
|
||||
import BackgroundGrid from '../components/ui/BackgroundGrid.vue'
|
||||
import PageLayout from '../components/layout/PageLayout.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const { setButtonActions } = useScreenActions()
|
||||
|
||||
onMounted(() => {
|
||||
appState.setPageDisplay(
|
||||
'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z',
|
||||
'ERROR 404'
|
||||
)
|
||||
|
||||
setButtonActions([
|
||||
{ buttonNumber: 3, label: 'HOME', action: () => router.push('/') }
|
||||
])
|
||||
@@ -17,24 +21,16 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full bg-zinc-950 text-white font-mono flex flex-col overflow-hidden relative selection:bg-red-500/30">
|
||||
|
||||
<BackgroundGrid :opacity="20" />
|
||||
|
||||
<!-- Top Bar -->
|
||||
<Header
|
||||
<PageLayout
|
||||
title="ERROR"
|
||||
badge="404"
|
||||
subtitle="Entry Not Found"
|
||||
title-color="text-red-400"
|
||||
subtitle=""
|
||||
:battery-level="60"
|
||||
/>
|
||||
|
||||
<!-- Main Error Content -->
|
||||
<div class="flex-1 min-h-0 flex items-center justify-center relative z-10 p-8">
|
||||
:grid-opacity="20"
|
||||
>
|
||||
<div class="flex items-center justify-center h-full p-8">
|
||||
<div class="text-center space-y-6">
|
||||
|
||||
<!-- Simple 404 Display -->
|
||||
<div>
|
||||
<div class="text-8xl font-black text-red-500 tracking-tighter leading-none mb-4">
|
||||
404
|
||||
@@ -44,16 +40,11 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Simple Message -->
|
||||
<p class="text-[10px] text-zinc-500 max-w-xs leading-relaxed">
|
||||
The requested Pokémon does not exist in the database.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<StatusBar right-text="Error - 404" right-text-color="text-red-500" />
|
||||
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
@@ -4,9 +4,7 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import Description from '../components/pokemon/Description.vue'
|
||||
import Information from '../components/pokemon/Information.vue'
|
||||
import Pokemon from '../components/pokemon/Pokemon.vue'
|
||||
import Header from '../components/layout/Header.vue'
|
||||
import StatusBar from '../components/layout/StatusBar.vue'
|
||||
import BackgroundGrid from '../components/ui/BackgroundGrid.vue'
|
||||
import PageLayout from '../components/layout/PageLayout.vue'
|
||||
import ColorThief from 'colorthief'
|
||||
import { generateThemeColors } from '../utils/theme'
|
||||
import { appState } from '../store'
|
||||
@@ -38,12 +36,10 @@ function getPokemonColors(image) {
|
||||
}
|
||||
|
||||
function getPokemonData() {
|
||||
// Reset state before fetch to avoid stale data flash
|
||||
pokemon.value = null
|
||||
|
||||
const currentId = parseInt(route.params.id)
|
||||
|
||||
// Fetch current Pokemon
|
||||
fetch(`https://pokeapi.co/api/v2/pokemon/${currentId}`)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
@@ -69,7 +65,6 @@ function getPokemonData() {
|
||||
if (json) pokemonSpecies.value = json
|
||||
});
|
||||
|
||||
// Fetch previous Pokemon (if exists)
|
||||
if (currentId > 1) {
|
||||
fetch(`https://pokeapi.co/api/v2/pokemon/${currentId - 1}`)
|
||||
.then((response) => response.ok ? response.json() : null)
|
||||
@@ -80,7 +75,6 @@ function getPokemonData() {
|
||||
appState.setPreviousPokemon(null)
|
||||
}
|
||||
|
||||
// Fetch next Pokemon
|
||||
fetch(`https://pokeapi.co/api/v2/pokemon/${currentId + 1}`)
|
||||
.then((response) => response.ok ? response.json() : null)
|
||||
.then((json) => {
|
||||
@@ -107,14 +101,15 @@ const pokemonColors = computed(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// Initial fetch
|
||||
getPokemonData()
|
||||
|
||||
// Watch for route changes to refetch data
|
||||
watch(() => route.params.id, () => {
|
||||
getPokemonData()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
appState.clearPageDisplay()
|
||||
|
||||
setButtonActions([
|
||||
{ buttonNumber: 1, label: 'LIST', action: () => router.push('/') },
|
||||
{ buttonNumber: 2, label: 'STATS', action: () => console.log('Stats view - TBA') },
|
||||
@@ -126,46 +121,36 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="pokemon && pokemonSpecies && pokemonColors" class="w-full h-full bg-zinc-950 text-white font-mono flex flex-col overflow-hidden relative selection:bg-red-500/30">
|
||||
|
||||
<BackgroundGrid :opacity="20" />
|
||||
|
||||
<!-- Dynamic accent glow based on Pokemon color -->
|
||||
<div class="absolute inset-0 opacity-20 pointer-events-none transition-colors duration-1000"
|
||||
:style="{ background: `radial-gradient(circle at 70% 50%, ${pokemonColors.backgroundColor}, transparent 70%)` }">
|
||||
</div>
|
||||
|
||||
<!-- Top Bar (Consistent with Home) -->
|
||||
<Header
|
||||
<PageLayout
|
||||
v-if="pokemon && pokemonSpecies && pokemonColors"
|
||||
:title="pokemon.name"
|
||||
:badge="pokemon.types.map(t => t.type.name)"
|
||||
:subtitle="`#${String(pokemon.id).padStart(4, '0')}`"
|
||||
:battery-level="60"
|
||||
/>
|
||||
:grid-opacity="20"
|
||||
>
|
||||
<template #background>
|
||||
<div class="absolute inset-0 opacity-20 pointer-events-none transition-colors duration-1000"
|
||||
:style="{ background: `radial-gradient(circle at 70% 50%, ${pokemonColors.backgroundColor}, transparent 70%)` }">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 min-h-0 flex flex-col relative z-10 p-3 gap-2 overflow-hidden">
|
||||
<div class="flex flex-col p-3 gap-2 h-full overflow-hidden">
|
||||
|
||||
<!-- Top Section: Name + Stats + Image -->
|
||||
<div class="flex gap-3 shrink-0">
|
||||
|
||||
<!-- Left: Species + Stats -->
|
||||
<div class="flex-1 flex flex-col gap-2 min-h-0">
|
||||
<!-- Species Info -->
|
||||
<div class="flex items-baseline gap-2 pb-2 border-b border-zinc-800">
|
||||
<span class="text-[9px] text-zinc-500 uppercase leading-none">Species:</span>
|
||||
<span class="text-[10px] font-bold text-zinc-300 leading-none">{{ pokemonSpecies.genera.find(g => g.language.name === 'en')?.genus || 'Unknown' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Stats/Information -->
|
||||
<div class="overflow-y-auto custom-scrollbar min-h-0">
|
||||
<Information :pokemonData="pokemon" :color="pokemonColors.backgroundColor" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Image Viewport -->
|
||||
<div class="w-40 h-40 relative border border-zinc-700 bg-zinc-900/50 rounded overflow-hidden flex items-center justify-center shrink-0">
|
||||
<!-- Corner brackets -->
|
||||
<div class="absolute top-0 left-0 w-3 h-3 border-t-2 border-l-2 border-zinc-500"></div>
|
||||
<div class="absolute top-0 right-0 w-3 h-3 border-t-2 border-r-2 border-zinc-500"></div>
|
||||
<div class="absolute bottom-0 left-0 w-3 h-3 border-b-2 border-l-2 border-zinc-500"></div>
|
||||
@@ -175,31 +160,24 @@ onMounted(() => {
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Bottom Section: Description (Full Width, Scrollable) -->
|
||||
<div class="flex-1 min-h-0 overflow-y-auto custom-scrollbar pt-2">
|
||||
<Description :pokemonData="pokemonSpecies" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</PageLayout>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<StatusBar right-text="v2.0.4 - Stable" />
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-else class="w-full h-full bg-zinc-950 text-white font-mono flex flex-col items-center justify-center relative overflow-hidden">
|
||||
|
||||
<BackgroundGrid :opacity="10" />
|
||||
|
||||
<div class="flex flex-col items-center gap-4 z-10">
|
||||
<!-- Animated Loader -->
|
||||
<PageLayout
|
||||
v-else
|
||||
title="LOADING"
|
||||
subtitle="Please wait..."
|
||||
:battery-level="60"
|
||||
>
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="relative w-16 h-16">
|
||||
<!-- Outer Ring -->
|
||||
<div class="absolute inset-0 border-2 border-zinc-800 rounded-full"></div>
|
||||
<!-- Spinning Segment -->
|
||||
<div class="absolute inset-0 border-t-2 border-red-500 rounded-full animate-spin"></div>
|
||||
<!-- Inner Pulse -->
|
||||
<div class="absolute inset-4 bg-zinc-900 rounded-full flex items-center justify-center shadow-[inset_0_0_10px_rgba(0,0,0,0.5)]">
|
||||
<div class="w-2 h-2 bg-red-500 rounded-full animate-ping"></div>
|
||||
</div>
|
||||
@@ -210,8 +188,8 @@ onMounted(() => {
|
||||
<div class="text-[10px] text-zinc-700 animate-pulse">Establishing secure connection...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
24
src/pages/Pokemon.vue
Normal file
24
src/pages/Pokemon.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { appState } from '../store'
|
||||
import PageLayout from '../components/layout/PageLayout.vue'
|
||||
|
||||
onMounted(() => {
|
||||
appState.setPageDisplay(
|
||||
'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-5.5-2.5l7.51-3.22-7.52-3.22 3.22 7.52 3.21-7.52-6.42 6.44z',
|
||||
'POKÉMON'
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageLayout
|
||||
title="POKÉMON"
|
||||
badge=""
|
||||
subtitle=""
|
||||
>
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<p class="text-zinc-500 text-sm">Coming Soon</p>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
24
src/pages/Settings.vue
Normal file
24
src/pages/Settings.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { appState } from '../store'
|
||||
import PageLayout from '../components/layout/PageLayout.vue'
|
||||
|
||||
onMounted(() => {
|
||||
appState.setPageDisplay(
|
||||
'M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z',
|
||||
'SETTINGS'
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageLayout
|
||||
title="SETTINGS"
|
||||
badge=""
|
||||
subtitle=""
|
||||
>
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<p class="text-zinc-500 text-sm">Coming Soon</p>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
@@ -1,11 +1,21 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import Home from "./pages/Home.vue";
|
||||
import Pokedex from "./pages/Pokedex.vue";
|
||||
import Pokemon from "./pages/Pokemon.vue";
|
||||
import Settings from "./pages/Settings.vue";
|
||||
import Maps from "./pages/Maps.vue";
|
||||
import Games from "./pages/Games.vue";
|
||||
import Moves from "./pages/Moves.vue";
|
||||
import NotFound from "./pages/NotFound.vue";
|
||||
|
||||
const routes = [
|
||||
{ path: "/", component: Home, name: "home" },
|
||||
{ path: "/pokemon/:id", component: Pokedex, name: "pokemon" },
|
||||
{ path: "/pokemon", component: Pokemon, name: "pokemon-list" },
|
||||
{ path: "/settings", component: Settings, name: "settings" },
|
||||
{ path: "/maps", component: Maps, name: "maps" },
|
||||
{ path: "/games", component: Games, name: "games" },
|
||||
{ path: "/moves", component: Moves, name: "moves" },
|
||||
{ path: "/home", component: Home, name: "home-alias" },
|
||||
{ path: "/:catchAll(.*)", component: NotFound, name: "404" },
|
||||
];
|
||||
|
||||
12
src/store.js
12
src/store.js
@@ -4,6 +4,10 @@ export const appState = reactive({
|
||||
currentPokemon: null,
|
||||
previousPokemon: null,
|
||||
nextPokemon: null,
|
||||
pageDisplay: {
|
||||
icon: null,
|
||||
title: null
|
||||
},
|
||||
setCurrentPokemon(pokemon) {
|
||||
this.currentPokemon = pokemon
|
||||
},
|
||||
@@ -17,5 +21,13 @@ export const appState = reactive({
|
||||
this.currentPokemon = null
|
||||
this.previousPokemon = null
|
||||
this.nextPokemon = null
|
||||
},
|
||||
setPageDisplay(icon, title) {
|
||||
this.pageDisplay.icon = icon
|
||||
this.pageDisplay.title = title
|
||||
},
|
||||
clearPageDisplay() {
|
||||
this.pageDisplay.icon = null
|
||||
this.pageDisplay.title = null
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user