feature: more fixes to make it amazing

This commit is contained in:
2026-02-07 16:33:07 -06:00
parent d55aac6605
commit f4dedcd66e
18 changed files with 579 additions and 261 deletions

View File

@@ -1,6 +1,4 @@
<script setup> <script setup>
import { ref } from 'vue'
const props = defineProps({ const props = defineProps({
title: { title: {
type: String, type: String,
@@ -14,27 +12,17 @@ const props = defineProps({
type: String, type: String,
default: null default: null
}, },
titleColor: {
type: String,
default: 'text-white'
},
batteryLevel: { batteryLevel: {
type: Number, type: Number,
default: 80 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> </script>
<template> <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"> <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"> <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 --> <!-- Badge - can be string or array -->
<template v-if="badge"> <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 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>
</div> </div>
<!-- Time -->
<span class="font-mono text-[10px] text-zinc-500">{{ currentTime }}</span>
</div> </div>
</header> </header>
</template> </template>

View 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>

View File

@@ -3,11 +3,11 @@ import { usePokedexNavigation } from '../../composables/usePokedexNavigation'
import LeftScreen from './LeftScreen.vue' import LeftScreen from './LeftScreen.vue'
import RightControls from './RightControls.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> </script>
<template> <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 --> <!-- 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]"> <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" @go-home="goHome"
@next="handleNext" @next="handleNext"
@prev="handlePrev" @prev="handlePrev"
@search-go="handleSearchGo"
@search-clear="handleSearchClear"
/> />
</div> </div>
</div> </div>
</template> </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>

View File

@@ -1,4 +1,5 @@
<script setup> <script setup>
import { useRouter } from 'vue-router'
import { appState } from '../../store' import { appState } from '../../store'
import { SIDE_MENU_ITEMS } from '../../constants/ui' import { SIDE_MENU_ITEMS } from '../../constants/ui'
import PillButton from '../ui/PillButton.vue' import PillButton from '../ui/PillButton.vue'
@@ -9,6 +10,8 @@ import PokemonInfoCard from '../ui/PokemonInfoCard.vue'
import SearchDisplay from '../ui/SearchDisplay.vue' import SearchDisplay from '../ui/SearchDisplay.vue'
import StatusButton from '../ui/StatusButton.vue' import StatusButton from '../ui/StatusButton.vue'
const router = useRouter()
const props = defineProps({ const props = defineProps({
searchQuery: { searchQuery: {
type: String, type: String,
@@ -22,8 +25,16 @@ const emit = defineEmits([
'back-button', 'back-button',
'go-home', 'go-home',
'next', 'next',
'prev' 'prev',
'search-go',
'search-clear'
]) ])
function navigateToMenuItem(item) {
if (item.route) {
router.push(item.route)
}
}
</script> </script>
<template> <template>
@@ -53,8 +64,23 @@ const emit = defineEmits([
<span class="text-[9px] font-bold opacity-60">{{ appState.currentPokemon ? 'ACTIVE' : 'READY' }}</span> <span class="text-[9px] font-bold opacity-60">{{ appState.currentPokemon ? 'ACTIVE' : 'READY' }}</span>
</div> </div>
<!-- Home Mode --> <!-- Page Mode - Show Icon and Title -->
<div v-if="!appState.currentPokemon" class="flex flex-col items-center justify-center opacity-60 flex-1"> <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 class="text-[10px] tracking-widest font-bold">WAITING INPUT</div>
</div> </div>
@@ -83,9 +109,9 @@ const emit = defineEmits([
<!-- Compact Black Tactile Numpad --> <!-- Compact Black Tactile Numpad -->
<div class="grid grid-cols-3 gap-2.5 content-start"> <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 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="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>
</div> </div>
@@ -93,10 +119,10 @@ const emit = defineEmits([
<!-- RIGHT: Slim Blue Buttons List --> <!-- RIGHT: Slim Blue Buttons List -->
<div class="w-[140px] flex flex-col gap-2 mr-4"> <div class="w-[140px] flex flex-col gap-2 mr-4">
<SideMenuButton <SideMenuButton
v-for="(label, i) in SIDE_MENU_ITEMS" v-for="(item, i) in SIDE_MENU_ITEMS"
:key="i" :key="i"
:label="label" :label="item.label"
@click="i === 0 ? emit('go-home') : null" @click="navigateToMenuItem(item)"
/> />
</div> </div>
@@ -107,18 +133,16 @@ const emit = defineEmits([
<!-- Back/Home Buttons --> <!-- Back/Home Buttons -->
<div class="flex gap-4 mb-2"> <div class="flex gap-4 mb-2">
<!-- Home / Execute --> <!-- Home -->
<PillButton @click="emit('main-button')"> <PillButton @click="emit('go-home')">
<span class="text-[9px] font-black tracking-wider text-black/60">{{ searchQuery ? 'ENTER' : 'HOME' }}</span> <span class="text-[9px] font-black tracking-wider text-black/60">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 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>
</PillButton> </PillButton>
<!-- Back / Clear --> <!-- Back -->
<PillButton @click="emit('back-button')"> <PillButton @click="emit('back-button')">
<span class="text-[9px] font-black tracking-wider text-black/60">{{ searchQuery ? 'CLEAR' : 'BACK' }}</span> <span class="text-[9px] font-black tracking-wider text-black/60">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 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>
</PillButton> </PillButton>
</div> </div>

View File

@@ -1,23 +1,9 @@
<script setup> <script setup>
const props = defineProps({
leftText: {
type: String,
default: 'Global Database'
},
rightText: {
type: String,
required: true
},
rightTextColor: {
type: String,
default: 'text-zinc-600'
}
})
</script> </script>
<template> <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"> <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>Global Database</span>
<span :class="rightTextColor">{{ rightText }}</span> <span>v2.0.4 - Stable</span>
</div> </div>
</template> </template>

View File

@@ -1,12 +1,21 @@
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { appState } from '../store' import { appState } from '../store'
import { MAX_SEARCH_LENGTH } from '../constants/ui' import { MAX_SEARCH_LENGTH } from '../constants/ui'
const searchQuery = ref('') const searchQuery = ref('')
export const homeMenuState = ref({
selectedIndex: 0,
itemCount: 0,
onSelect: null,
onMoveUp: null,
onMoveDown: null
})
export function usePokedexNavigation() { export function usePokedexNavigation() {
const router = useRouter() const router = useRouter()
const route = useRoute()
function handleKeypad(num) { function handleKeypad(num) {
if (searchQuery.value.length < MAX_SEARCH_LENGTH) { if (searchQuery.value.length < MAX_SEARCH_LENGTH) {
@@ -19,7 +28,24 @@ export function usePokedexNavigation() {
} }
function goBack() { 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() { function executeSearch() {
@@ -36,8 +62,18 @@ export function usePokedexNavigation() {
} }
} }
function handleSearchGo() {
executeSearch()
}
function handleSearchClear() {
clearSearch()
}
function handleMainButton() { function handleMainButton() {
if (searchQuery.value) { if (route.path === '/' && homeMenuState.value.onSelect) {
homeMenuState.value.onSelect()
} else if (searchQuery.value) {
executeSearch() executeSearch()
} else { } else {
goHome() goHome()
@@ -45,22 +81,24 @@ export function usePokedexNavigation() {
} }
function handleBackButton() { function handleBackButton() {
if (searchQuery.value) { if (route.path !== '/') {
clearSearch()
} else {
goBack() goBack()
} }
} }
function handleNext() { function handleNext() {
if (appState.currentPokemon) { if (route.path === '/' && homeMenuState.value.onMoveDown) {
homeMenuState.value.onMoveDown()
} else if (appState.currentPokemon) {
let nextId = appState.currentPokemon.id + 1 let nextId = appState.currentPokemon.id + 1
router.push(`/pokemon/${nextId}`) router.push(`/pokemon/${nextId}`)
} }
} }
function handlePrev() { 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 let prevId = appState.currentPokemon.id - 1
router.push(`/pokemon/${prevId}`) router.push(`/pokemon/${prevId}`)
} }
@@ -76,6 +114,8 @@ export function usePokedexNavigation() {
handleMainButton, handleMainButton,
handleBackButton, handleBackButton,
handleNext, handleNext,
handlePrev handlePrev,
handleSearchGo,
handleSearchClear
} }
} }

2
src/constants/layout.js Normal file
View File

@@ -0,0 +1,2 @@
export const SCREEN_MAX_WIDTH = 380
export const SCREEN_MAX_HEIGHT = 680

View File

@@ -18,6 +18,12 @@ export const DIMENSIONS = {
export const MAX_SEARCH_LENGTH = 4 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] export const KEYPAD_KEYS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]

24
src/pages/Games.vue Normal file
View 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>

View File

@@ -1,152 +1,249 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import ColorThief from 'colorthief'
import { generateThemeColors } from '../utils/theme'
import { appState } from '../store' import { appState } from '../store'
import Header from '../components/layout/Header.vue' import PageLayout from '../components/layout/PageLayout.vue'
import StatusBar from '../components/layout/StatusBar.vue' import { homeMenuState } from '../composables/usePokedexNavigation'
import BackgroundGrid from '../components/ui/BackgroundGrid.vue'
import { useScreenActions } from '../composables/useScreenActions' import { useScreenActions } from '../composables/useScreenActions'
const router = useRouter() const router = useRouter()
const { clearActions } = useScreenActions()
// Clear active pokemon state when entering Dashboard
appState.clearCurrentPokemon() appState.clearCurrentPokemon()
const { setButtonActions } = useScreenActions() const menuItems = ref([
{
// Reusing the featured list logic but integrated into the dashboard id: 'pokemons',
const featuredPokemon = ref([ label: 'POKÉMON',
{ id: 1, name: 'Bulbasaur', type: 'Grass', customBg: '#2d2d2d', customText: '#ffffff' }, subtitle: 'Browse Global Database',
{ id: 4, name: 'Charmander', type: 'Fire', customBg: '#2d2d2d', customText: '#ffffff' }, 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',
{ id: 7, name: 'Squirtle', type: 'Water', customBg: '#2d2d2d', customText: '#ffffff' }, route: '/pokemon',
{ id: 25, name: 'Pikachu', type: 'Electric', customBg: '#2d2d2d', customText: '#ffffff' }, color: 'red'
{ id: 133, name: 'Eevee', type: 'Normal', customBg: '#2d2d2d', customText: '#ffffff' }, },
{ id: 150, name: 'Mewtwo', type: 'Psychic', customBg: '#2d2d2d', customText: '#ffffff' }, {
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) { const selectedIndex = ref(0)
return `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${id}.png`
function movePrev() {
if (selectedIndex.value > 0) {
selectedIndex.value--
} else {
selectedIndex.value = menuItems.value.length - 1
}
} }
function extractColors() { function moveNext() {
const colorThief = new ColorThief() if (selectedIndex.value < menuItems.value.length - 1) {
featuredPokemon.value.forEach(poke => { selectedIndex.value++
const img = new Image() } else {
img.crossOrigin = 'anonymous' selectedIndex.value = 0
img.src = getImageUrl(poke.id)
img.onload = () => {
const color = colorThief.getColor(img)
const { background, text } = generateThemeColors(color)
poke.customBg = background
poke.customText = text
} }
})
} }
function goToRandomPokemon() { function selectItem() {
const randomId = Math.floor(Math.random() * 1025) + 1 const item = menuItems.value[selectedIndex.value]
router.push(`/pokemon/${randomId}`) 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(() => { onMounted(() => {
extractColors() clearActions()
setButtonActions([
{ buttonNumber: 3, label: 'RANDOM', action: goToRandomPokemon }, appState.setPageDisplay(
{ buttonNumber: 5, label: 'SETTINGS', action: () => console.log('Settings - TBA') } '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> </script>
<template> <template>
<div class="absolute inset-0 bg-zinc-950 text-white font-sans flex flex-col overflow-hidden selection:bg-red-500/30"> <PageLayout
title="POKEDEX"
<BackgroundGrid :opacity="10" /> badge=""
subtitle=""
<!-- 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"
> >
<!-- Row Hover Highlight --> <div class="flex flex-col items-center justify-center px-4 py-4 flex-1 min-h-0">
<div class="absolute inset-0 bg-white/5 opacity-0 group-hover:opacity-100 transition-opacity"></div>
<div class="w-10 h-10 bg-zinc-950 rounded border border-zinc-800 flex items-center justify-center relative shadow-inner"> <div class="mb-4 text-center">
<img :src="getImageUrl(poke.id)" class="w-8 h-8 object-contain z-10 group-hover:scale-125 transition-transform duration-300" /> <p class="text-xs text-zinc-400 uppercase tracking-wide font-bold">Main Menu</p>
</div> </div>
<div class="flex-1 z-10"> <div class="relative w-full max-w-sm h-48 mb-4 flex items-center justify-center overflow-hidden">
<div class="flex justify-between items-baseline"> <div
<span class="font-bold text-sm text-zinc-200 group-hover:text-white">{{ poke.name }}</span> v-for="(item, index) in menuItems"
<span class="text-[10px] font-mono text-zinc-600 group-hover:text-zinc-400">#{{ String(poke.id).padStart(3, '0') }}</span> :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>
<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> </div>
<div class="pr-1 opacity-0 group-hover:opacity-100 transition-all -translate-x-4 group-hover:translate-x-0 z-10"> <div class="flex items-center gap-1 mb-4">
<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
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> </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> </div>
</div> </div>
</PageLayout>
<!-- Status Bar -->
<StatusBar right-text="v2.0.4 - Stable" />
</div>
</template> </template>

24
src/pages/Maps.vue Normal file
View 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
View 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>

View File

@@ -1,15 +1,19 @@
<script setup> <script setup>
import { onMounted } from 'vue' import { onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { appState } from '../store'
import { useScreenActions } from '../composables/useScreenActions' import { useScreenActions } from '../composables/useScreenActions'
import Header from '../components/layout/Header.vue' import PageLayout from '../components/layout/PageLayout.vue'
import StatusBar from '../components/layout/StatusBar.vue'
import BackgroundGrid from '../components/ui/BackgroundGrid.vue'
const router = useRouter() const router = useRouter()
const { setButtonActions } = useScreenActions() const { setButtonActions } = useScreenActions()
onMounted(() => { 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([ setButtonActions([
{ buttonNumber: 3, label: 'HOME', action: () => router.push('/') } { buttonNumber: 3, label: 'HOME', action: () => router.push('/') }
]) ])
@@ -17,24 +21,16 @@ onMounted(() => {
</script> </script>
<template> <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"> <PageLayout
<BackgroundGrid :opacity="20" />
<!-- Top Bar -->
<Header
title="ERROR" title="ERROR"
badge="404" badge="404"
subtitle="Entry Not Found" subtitle=""
title-color="text-red-400"
:battery-level="60" :battery-level="60"
/> :grid-opacity="20"
>
<!-- Main Error Content --> <div class="flex items-center justify-center h-full p-8">
<div class="flex-1 min-h-0 flex items-center justify-center relative z-10 p-8">
<div class="text-center space-y-6"> <div class="text-center space-y-6">
<!-- Simple 404 Display -->
<div> <div>
<div class="text-8xl font-black text-red-500 tracking-tighter leading-none mb-4"> <div class="text-8xl font-black text-red-500 tracking-tighter leading-none mb-4">
404 404
@@ -44,16 +40,11 @@ onMounted(() => {
</div> </div>
</div> </div>
<!-- Simple Message -->
<p class="text-[10px] text-zinc-500 max-w-xs leading-relaxed"> <p class="text-[10px] text-zinc-500 max-w-xs leading-relaxed">
The requested Pokémon does not exist in the database. The requested Pokémon does not exist in the database.
</p> </p>
</div> </div>
</div> </div>
</PageLayout>
<!-- Status Bar -->
<StatusBar right-text="Error - 404" right-text-color="text-red-500" />
</div>
</template> </template>

View File

@@ -4,9 +4,7 @@ import { useRoute, useRouter } from 'vue-router';
import Description from '../components/pokemon/Description.vue' import Description from '../components/pokemon/Description.vue'
import Information from '../components/pokemon/Information.vue' import Information from '../components/pokemon/Information.vue'
import Pokemon from '../components/pokemon/Pokemon.vue' import Pokemon from '../components/pokemon/Pokemon.vue'
import Header from '../components/layout/Header.vue' import PageLayout from '../components/layout/PageLayout.vue'
import StatusBar from '../components/layout/StatusBar.vue'
import BackgroundGrid from '../components/ui/BackgroundGrid.vue'
import ColorThief from 'colorthief' import ColorThief from 'colorthief'
import { generateThemeColors } from '../utils/theme' import { generateThemeColors } from '../utils/theme'
import { appState } from '../store' import { appState } from '../store'
@@ -38,12 +36,10 @@ function getPokemonColors(image) {
} }
function getPokemonData() { function getPokemonData() {
// Reset state before fetch to avoid stale data flash
pokemon.value = null pokemon.value = null
const currentId = parseInt(route.params.id) const currentId = parseInt(route.params.id)
// Fetch current Pokemon
fetch(`https://pokeapi.co/api/v2/pokemon/${currentId}`) fetch(`https://pokeapi.co/api/v2/pokemon/${currentId}`)
.then((response) => { .then((response) => {
if (!response.ok) { if (!response.ok) {
@@ -69,7 +65,6 @@ function getPokemonData() {
if (json) pokemonSpecies.value = json if (json) pokemonSpecies.value = json
}); });
// Fetch previous Pokemon (if exists)
if (currentId > 1) { if (currentId > 1) {
fetch(`https://pokeapi.co/api/v2/pokemon/${currentId - 1}`) fetch(`https://pokeapi.co/api/v2/pokemon/${currentId - 1}`)
.then((response) => response.ok ? response.json() : null) .then((response) => response.ok ? response.json() : null)
@@ -80,7 +75,6 @@ function getPokemonData() {
appState.setPreviousPokemon(null) appState.setPreviousPokemon(null)
} }
// Fetch next Pokemon
fetch(`https://pokeapi.co/api/v2/pokemon/${currentId + 1}`) fetch(`https://pokeapi.co/api/v2/pokemon/${currentId + 1}`)
.then((response) => response.ok ? response.json() : null) .then((response) => response.ok ? response.json() : null)
.then((json) => { .then((json) => {
@@ -107,14 +101,15 @@ const pokemonColors = computed(() => {
} }
}); });
// Initial fetch
getPokemonData() getPokemonData()
// Watch for route changes to refetch data
watch(() => route.params.id, () => { watch(() => route.params.id, () => {
getPokemonData() getPokemonData()
}) })
onMounted(() => { onMounted(() => {
appState.clearPageDisplay()
setButtonActions([ setButtonActions([
{ buttonNumber: 1, label: 'LIST', action: () => router.push('/') }, { buttonNumber: 1, label: 'LIST', action: () => router.push('/') },
{ buttonNumber: 2, label: 'STATS', action: () => console.log('Stats view - TBA') }, { buttonNumber: 2, label: 'STATS', action: () => console.log('Stats view - TBA') },
@@ -126,46 +121,36 @@ onMounted(() => {
</script> </script>
<template> <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"> <PageLayout
v-if="pokemon && pokemonSpecies && pokemonColors"
<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
:title="pokemon.name" :title="pokemon.name"
:badge="pokemon.types.map(t => t.type.name)" :badge="pokemon.types.map(t => t.type.name)"
:subtitle="`#${String(pokemon.id).padStart(4, '0')}`" :subtitle="`#${String(pokemon.id).padStart(4, '0')}`"
:battery-level="60" :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 flex-col p-3 gap-2 h-full overflow-hidden">
<div class="flex-1 min-h-0 flex flex-col relative z-10 p-3 gap-2 overflow-hidden">
<!-- Top Section: Name + Stats + Image -->
<div class="flex gap-3 shrink-0"> <div class="flex gap-3 shrink-0">
<!-- Left: Species + Stats -->
<div class="flex-1 flex flex-col gap-2 min-h-0"> <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"> <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-[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> <span class="text-[10px] font-bold text-zinc-300 leading-none">{{ pokemonSpecies.genera.find(g => g.language.name === 'en')?.genus || 'Unknown' }}</span>
</div> </div>
<!-- Stats/Information -->
<div class="overflow-y-auto custom-scrollbar min-h-0"> <div class="overflow-y-auto custom-scrollbar min-h-0">
<Information :pokemonData="pokemon" :color="pokemonColors.backgroundColor" /> <Information :pokemonData="pokemon" :color="pokemonColors.backgroundColor" />
</div> </div>
</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"> <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 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 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> <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> </div>
<!-- Bottom Section: Description (Full Width, Scrollable) -->
<div class="flex-1 min-h-0 overflow-y-auto custom-scrollbar pt-2"> <div class="flex-1 min-h-0 overflow-y-auto custom-scrollbar pt-2">
<Description :pokemonData="pokemonSpecies" /> <Description :pokemonData="pokemonSpecies" />
</div> </div>
</div> </div>
</PageLayout>
<!-- Status Bar --> <PageLayout
<StatusBar right-text="v2.0.4 - Stable" /> v-else
title="LOADING"
</div> subtitle="Please wait..."
:battery-level="60"
<!-- 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"> <div class="flex items-center justify-center h-full">
<div class="flex flex-col items-center gap-4">
<BackgroundGrid :opacity="10" />
<div class="flex flex-col items-center gap-4 z-10">
<!-- Animated Loader -->
<div class="relative w-16 h-16"> <div class="relative w-16 h-16">
<!-- Outer Ring -->
<div class="absolute inset-0 border-2 border-zinc-800 rounded-full"></div> <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> <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="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 class="w-2 h-2 bg-red-500 rounded-full animate-ping"></div>
</div> </div>
@@ -210,8 +188,8 @@ onMounted(() => {
<div class="text-[10px] text-zinc-700 animate-pulse">Establishing secure connection...</div> <div class="text-[10px] text-zinc-700 animate-pulse">Establishing secure connection...</div>
</div> </div>
</div> </div>
</div> </div>
</PageLayout>
</template> </template>
<style scoped> <style scoped>

24
src/pages/Pokemon.vue Normal file
View 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
View 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>

View File

@@ -1,11 +1,21 @@
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import Home from "./pages/Home.vue"; import Home from "./pages/Home.vue";
import Pokedex from "./pages/Pokedex.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"; import NotFound from "./pages/NotFound.vue";
const routes = [ const routes = [
{ path: "/", component: Home, name: "home" }, { path: "/", component: Home, name: "home" },
{ path: "/pokemon/:id", component: Pokedex, name: "pokemon" }, { 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: "/home", component: Home, name: "home-alias" },
{ path: "/:catchAll(.*)", component: NotFound, name: "404" }, { path: "/:catchAll(.*)", component: NotFound, name: "404" },
]; ];

View File

@@ -4,6 +4,10 @@ export const appState = reactive({
currentPokemon: null, currentPokemon: null,
previousPokemon: null, previousPokemon: null,
nextPokemon: null, nextPokemon: null,
pageDisplay: {
icon: null,
title: null
},
setCurrentPokemon(pokemon) { setCurrentPokemon(pokemon) {
this.currentPokemon = pokemon this.currentPokemon = pokemon
}, },
@@ -17,5 +21,13 @@ export const appState = reactive({
this.currentPokemon = null this.currentPokemon = null
this.previousPokemon = null this.previousPokemon = null
this.nextPokemon = null this.nextPokemon = null
},
setPageDisplay(icon, title) {
this.pageDisplay.icon = icon
this.pageDisplay.title = title
},
clearPageDisplay() {
this.pageDisplay.icon = null
this.pageDisplay.title = null
} }
}) })