feature: new pokemon pages

This commit is contained in:
2026-02-07 16:54:28 -06:00
parent f4dedcd66e
commit 8d0e4e9115
7 changed files with 1066 additions and 9 deletions

View File

@@ -83,10 +83,6 @@ function getPokemonData() {
})
}
function changeTheme() {
isDark.value = !isDark.value
}
const pokemonColors = computed(() => {
if (isDark.value) {
return {
@@ -110,12 +106,14 @@ watch(() => route.params.id, () => {
onMounted(() => {
appState.clearPageDisplay()
const pokemonId = route.params.id
setButtonActions([
{ buttonNumber: 1, label: 'LIST', action: () => router.push('/') },
{ buttonNumber: 2, label: 'STATS', action: () => console.log('Stats view - TBA') },
{ buttonNumber: 3, label: 'EVOL', action: () => console.log('Evolution chain - TBA') },
{ buttonNumber: 4, label: 'MOVES', action: () => console.log('Moves list - TBA') },
{ buttonNumber: 5, label: 'THEME', action: changeTheme }
{ buttonNumber: 1, label: 'LOCA', action: () => router.push(`/pokemon/${pokemonId}/locations`) },
{ buttonNumber: 2, label: 'COLOR', action: () => router.push(`/pokemon/${pokemonId}/colors`) },
{ buttonNumber: 3, label: 'FORMS', action: () => router.push(`/pokemon/${pokemonId}/forms`) },
{ buttonNumber: 4, label: 'SHAPE', action: () => router.push(`/pokemon/${pokemonId}/shapes`) },
{ buttonNumber: 5, label: 'SPEC', action: () => router.push(`/pokemon/${pokemonId}/species`) }
])
})
</script>

200
src/pages/PokemonColors.vue Normal file
View File

@@ -0,0 +1,200 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { appState } from '../store'
import PageLayout from '../components/layout/PageLayout.vue'
import { useScreenActions } from '../composables/useScreenActions'
const { setButtonActions } = useScreenActions()
const route = useRoute()
const router = useRouter()
const pokemon = ref(null)
const pokemonSpecies = ref(null)
const colorData = ref(null)
const loading = ref(true)
const colorMap = {
'black': '#1a1a1a',
'blue': '#3b82f6',
'brown': '#92400e',
'gray': '#6b7280',
'green': '#22c55e',
'pink': '#ec4899',
'purple': '#a855f7',
'red': '#ef4444',
'white': '#f5f5f5',
'yellow': '#eab308'
}
const colorHex = computed(() => {
if (!colorData.value) return '#6b7280'
return colorMap[colorData.value.name] || '#6b7280'
})
const sortedNames = computed(() => {
if (!colorData.value?.names) return []
const priorityLanguages = ['en', 'ja-Hrkt', 'roomaji', 'ko', 'zh-Hans', 'fr', 'de', 'es', 'it']
return [...colorData.value.names].sort((a, b) => {
const aIndex = priorityLanguages.indexOf(a.language.name)
const bIndex = priorityLanguages.indexOf(b.language.name)
if (aIndex === -1 && bIndex === -1) return 0
if (aIndex === -1) return 1
if (bIndex === -1) return -1
return aIndex - bIndex
})
})
function getPokemonData() {
const currentId = parseInt(route.params.id)
loading.value = true
fetch(`https://pokeapi.co/api/v2/pokemon/${currentId}`)
.then((response) => {
if (!response.ok) {
router.push({ name: '404', params: { catchAll: 'not-found' } })
return null
}
return response.json()
})
.then((json) => {
if (json) pokemon.value = json
})
fetch(`https://pokeapi.co/api/v2/pokemon-species/${currentId}`)
.then((response) => {
if (!response.ok) return null
return response.json()
})
.then((json) => {
if (json) {
pokemonSpecies.value = json
const colorUrl = json.color.url
return fetch(colorUrl)
}
return null
})
.then((response) => {
if (!response || !response.ok) return null
return response.json()
})
.then((json) => {
if (json) colorData.value = json
loading.value = false
})
}
getPokemonData()
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-9h10v2H7z',
'COLORS'
)
setButtonActions([
{ buttonNumber: 1, label: 'BACK', action: () => router.push(`/pokemon/${route.params.id}`) }
])
})
</script>
<template>
<PageLayout
v-if="pokemon && !loading"
:title="pokemon.name"
:badge="pokemon.types.map(t => t.type.name)"
subtitle="Color Data"
statusText="Color Information"
>
<div v-if="!colorData" class="flex items-center justify-center h-full">
<div class="text-center space-y-2">
<div class="text-zinc-600 text-xs"></div>
<p class="text-zinc-500 text-xs">No color data available</p>
</div>
</div>
<div v-else class="flex flex-col h-full overflow-hidden p-3 gap-3">
<div class="border border-zinc-800 bg-zinc-950/50 rounded overflow-hidden">
<div class="bg-zinc-900 px-2 py-1.5 border-b border-zinc-800">
<h3 class="text-[10px] font-bold text-zinc-300 uppercase tracking-wide">
Primary Color
</h3>
</div>
<div class="p-3 flex items-center gap-3">
<div
class="w-20 h-20 rounded border-2 border-zinc-700 shadow-lg shrink-0"
:style="{ backgroundColor: colorHex }"
>
<div v-if="colorData.name === 'white'" class="w-full h-full border border-zinc-300"></div>
</div>
<div class="flex-1 space-y-1">
<div class="text-xl font-bold text-zinc-200 capitalize">
{{ colorData.name }}
</div>
<div class="text-[10px] text-zinc-500 font-mono">
{{ colorHex }}
</div>
<div class="text-[9px] text-zinc-600 mt-2">
Pokédex classification color
</div>
</div>
</div>
</div>
<div class="border border-zinc-800 bg-zinc-950/50 rounded overflow-hidden flex-1 min-h-0 flex flex-col">
<div class="bg-zinc-900 px-2 py-1.5 border-b border-zinc-800">
<h3 class="text-[10px] font-bold text-zinc-300 uppercase tracking-wide">
Translations
</h3>
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar p-2 space-y-1.5">
<div
v-for="nameEntry in sortedNames"
:key="nameEntry.language.name"
class="flex items-baseline justify-between py-1 px-2 hover:bg-zinc-900/50 rounded"
>
<span class="text-[9px] text-zinc-500 uppercase tracking-wider min-w-15">
{{ nameEntry.language.name }}
</span>
<span class="text-[11px] text-zinc-300 font-medium">
{{ nameEntry.name }}
</span>
</div>
</div>
</div>
</div>
</PageLayout>
<PageLayout
v-else
title="LOADING"
subtitle="Please wait..."
>
<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">
<div class="absolute inset-0 border-2 border-zinc-800 rounded-full"></div>
<div class="absolute inset-0 border-t-2 border-red-500 rounded-full animate-spin"></div>
<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>
</div>
<div class="text-center">
<div class="text-xs font-bold tracking-[0.2em] text-zinc-500 mb-1">LOADING DATA</div>
<div class="text-[10px] text-zinc-700 animate-pulse">Fetching color information...</div>
</div>
</div>
</div>
</PageLayout>
</template>

175
src/pages/PokemonForms.vue Normal file
View File

@@ -0,0 +1,175 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { appState } from '../store'
import PageLayout from '../components/layout/PageLayout.vue'
import { useScreenActions } from '../composables/useScreenActions'
const { setButtonActions } = useScreenActions()
const route = useRoute()
const router = useRouter()
const pokemon = ref(null)
const forms = ref([])
const loading = ref(true)
function formatFormName(name) {
return name
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
async function getPokemonData() {
const currentId = parseInt(route.params.id)
loading.value = true
try {
const pokemonResponse = await fetch(`https://pokeapi.co/api/v2/pokemon/${currentId}`)
if (!pokemonResponse.ok) {
router.push({ name: '404', params: { catchAll: 'not-found' } })
return
}
const pokemonData = await pokemonResponse.json()
pokemon.value = pokemonData
const formPromises = pokemonData.forms.map(form =>
fetch(form.url).then(res => res.ok ? res.json() : null)
)
const formResults = await Promise.all(formPromises)
forms.value = formResults.filter(form => form !== null)
} catch (error) {
console.error('Error fetching forms:', error)
} finally {
loading.value = false
}
}
getPokemonData()
onMounted(() => {
appState.setPageDisplay(
'M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm0 18c-4.4 0-8-3.6-8-8s3.6-8 8-8 8 3.6 8 8-3.6 8-8 8zm-4-8l1.4 1.4L11 11.8V16h2v-4.2l1.6 1.6L16 12l-4-4-4 4z',
'FORMS'
)
setButtonActions([
{ buttonNumber: 1, label: 'BACK', action: () => router.push(`/pokemon/${route.params.id}`) }
])
})
</script>
<template>
<PageLayout
v-if="pokemon && !loading"
:title="pokemon.name"
:badge="pokemon.types.map(t => t.type.name)"
subtitle="Form Variations"
statusText="Form Information"
>
<div v-if="forms.length === 0" class="flex items-center justify-center h-full">
<div class="text-center space-y-2">
<div class="text-zinc-600 text-xs"></div>
<p class="text-zinc-500 text-xs">No form data available</p>
</div>
</div>
<div v-else class="flex flex-col h-full overflow-hidden p-3 gap-2">
<div class="flex-1 overflow-y-auto custom-scrollbar space-y-3">
<div
v-for="form in forms"
:key="form.id"
class="border border-zinc-800 bg-zinc-950/50 rounded overflow-hidden"
:class="{ 'border-zinc-600': form.is_default }"
>
<div class="bg-zinc-900 px-2 py-1.5 border-b border-zinc-800 flex items-center justify-between">
<h3 class="text-[10px] font-bold text-zinc-300 uppercase tracking-wide">
{{ formatFormName(form.name) }}
</h3>
<div class="flex gap-1">
<span v-if="form.is_default" class="text-[8px] bg-zinc-700 text-zinc-300 px-1.5 py-0.5 rounded uppercase">
Default
</span>
<span v-if="form.is_mega" class="text-[8px] bg-purple-900/50 text-purple-300 px-1.5 py-0.5 rounded uppercase">
Mega
</span>
<span v-if="form.is_battle_only" class="text-[8px] bg-red-900/50 text-red-300 px-1.5 py-0.5 rounded uppercase">
Battle
</span>
</div>
</div>
<div class="p-3 flex gap-3">
<div class="grid grid-cols-2 gap-2 shrink-0">
<div v-if="form.sprites.front_default" class="w-16 h-16 bg-zinc-900/50 border border-zinc-800 rounded flex items-center justify-center">
<img :src="form.sprites.front_default" :alt="form.name" class="w-full h-full object-contain" />
</div>
<div v-if="form.sprites.front_shiny" class="w-16 h-16 bg-zinc-900/50 border border-zinc-800 rounded flex items-center justify-center relative">
<img :src="form.sprites.front_shiny" :alt="form.name + ' shiny'" class="w-full h-full object-contain" />
<div class="absolute top-0.5 right-0.5 w-1.5 h-1.5 bg-yellow-400 rounded-full"></div>
</div>
<div v-if="form.sprites.back_default" class="w-16 h-16 bg-zinc-900/50 border border-zinc-800 rounded flex items-center justify-center">
<img :src="form.sprites.back_default" :alt="form.name + ' back'" class="w-full h-full object-contain opacity-60" />
</div>
<div v-if="form.sprites.back_shiny" class="w-16 h-16 bg-zinc-900/50 border border-zinc-800 rounded flex items-center justify-center relative">
<img :src="form.sprites.back_shiny" :alt="form.name + ' back shiny'" class="w-full h-full object-contain opacity-60" />
<div class="absolute top-0.5 right-0.5 w-1.5 h-1.5 bg-yellow-400 rounded-full"></div>
</div>
</div>
<div class="flex-1 space-y-2 min-w-0">
<div v-if="form.form_name" class="space-y-0.5">
<div class="text-[9px] text-zinc-500 uppercase">Form Name</div>
<div class="text-[10px] text-zinc-300 capitalize">{{ form.form_name }}</div>
</div>
<div v-if="form.types && form.types.length > 0" class="space-y-0.5">
<div class="text-[9px] text-zinc-500 uppercase">Types</div>
<div class="flex gap-1">
<span
v-for="type in form.types"
:key="type.slot"
class="text-[8px] bg-zinc-800 text-zinc-400 px-1.5 py-0.5 rounded uppercase"
>
{{ type.type.name }}
</span>
</div>
</div>
<div class="space-y-0.5">
<div class="text-[9px] text-zinc-500 uppercase">Order</div>
<div class="text-[10px] text-zinc-400 font-mono">
Form: {{ form.form_order }} | Overall: {{ form.order }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</PageLayout>
<PageLayout
v-else
title="LOADING"
subtitle="Please wait..."
>
<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">
<div class="absolute inset-0 border-2 border-zinc-800 rounded-full"></div>
<div class="absolute inset-0 border-t-2 border-red-500 rounded-full animate-spin"></div>
<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>
</div>
<div class="text-center">
<div class="text-xs font-bold tracking-[0.2em] text-zinc-500 mb-1">LOADING DATA</div>
<div class="text-[10px] text-zinc-700 animate-pulse">Fetching form information...</div>
</div>
</div>
</div>
</PageLayout>
</template>

View File

@@ -0,0 +1,185 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { appState } from '../store'
import PageLayout from '../components/layout/PageLayout.vue'
import { useScreenActions } from '../composables/useScreenActions'
const { setButtonActions } = useScreenActions()
const route = useRoute()
const router = useRouter()
const pokemon = ref(null)
const encounters = ref(null)
const loading = ref(true)
function formatLocationName(name) {
return name
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
function formatMethodName(name) {
return name.charAt(0).toUpperCase() + name.slice(1)
}
const groupedLocations = computed(() => {
if (!encounters.value || encounters.value.length === 0) return []
const grouped = {}
encounters.value.forEach(encounter => {
const locationName = encounter.location_area.name
if (!grouped[locationName]) {
grouped[locationName] = {
name: locationName,
versions: []
}
}
encounter.version_details.forEach(versionDetail => {
const versionName = versionDetail.version.name
const methods = versionDetail.encounter_details.map(detail => ({
method: detail.method.name,
minLevel: detail.min_level,
maxLevel: detail.max_level,
chance: detail.chance
}))
grouped[locationName].versions.push({
version: versionName,
maxChance: versionDetail.max_chance,
methods
})
})
})
return Object.values(grouped)
})
function getPokemonData() {
const currentId = parseInt(route.params.id)
loading.value = true
fetch(`https://pokeapi.co/api/v2/pokemon/${currentId}`)
.then((response) => {
if (!response.ok) {
router.push({ name: '404', params: { catchAll: 'not-found' } })
return null
}
return response.json()
})
.then((json) => {
if (json) pokemon.value = json
})
fetch(`https://pokeapi.co/api/v2/pokemon/${currentId}/encounters`)
.then((response) => {
if (!response.ok) return null
return response.json()
})
.then((json) => {
encounters.value = json
loading.value = false
})
}
getPokemonData()
onMounted(() => {
appState.setPageDisplay(
'M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm0 18c-4.4 0-8-3.6-8-8s3.6-8 8-8 8 3.6 8 8-3.6 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z',
'LOCATIONS'
)
setButtonActions([
{ buttonNumber: 1, label: 'BACK', action: () => router.push(`/pokemon/${route.params.id}`) }
])
})
</script>
<template>
<PageLayout
v-if="pokemon && !loading"
:title="pokemon.name"
:badge="pokemon.types.map(t => t.type.name)"
subtitle="Location Areas"
statusText="Location Information"
>
<div v-if="groupedLocations.length === 0" class="flex items-center justify-center h-full">
<div class="text-center space-y-2">
<div class="text-zinc-600 text-xs"></div>
<p class="text-zinc-500 text-xs">No location data available</p>
<p class="text-zinc-700 text-[10px]">This Pokémon cannot be found in the wild</p>
</div>
</div>
<div v-else class="flex flex-col h-full overflow-hidden p-3 gap-2">
<div class="flex-1 overflow-y-auto custom-scrollbar space-y-3">
<div
v-for="location in groupedLocations"
:key="location.name"
class="border border-zinc-800 bg-zinc-950/50 rounded overflow-hidden"
>
<div class="bg-zinc-900 px-2 py-1.5 border-b border-zinc-800">
<h3 class="text-[10px] font-bold text-zinc-300 uppercase tracking-wide">
{{ formatLocationName(location.name) }}
</h3>
</div>
<div class="p-2 space-y-2">
<div
v-for="(version, idx) in location.versions"
:key="idx"
class="space-y-1"
>
<div class="flex items-center justify-between">
<span class="text-[9px] text-zinc-500 uppercase">{{ version.version }}</span>
<span class="text-[9px] text-zinc-600">Max: {{ version.maxChance }}%</span>
</div>
<div class="space-y-1">
<div
v-for="(method, mIdx) in version.methods"
:key="mIdx"
class="flex items-center justify-between text-[9px] pl-2 py-0.5"
>
<div class="flex items-center gap-2">
<span class="text-zinc-400">{{ formatMethodName(method.method) }}</span>
<span class="text-zinc-600">Lv {{ method.minLevel }}-{{ method.maxLevel }}</span>
</div>
<span class="text-zinc-500 font-mono">{{ method.chance }}%</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</PageLayout>
<PageLayout
v-else
title="LOADING"
subtitle="Please wait..."
>
<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">
<div class="absolute inset-0 border-2 border-zinc-800 rounded-full"></div>
<div class="absolute inset-0 border-t-2 border-red-500 rounded-full animate-spin"></div>
<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>
</div>
<div class="text-center">
<div class="text-xs font-bold tracking-[0.2em] text-zinc-500 mb-1">LOADING DATA</div>
<div class="text-[10px] text-zinc-700 animate-pulse">Fetching location information...</div>
</div>
</div>
</div>
</PageLayout>
</template>

243
src/pages/PokemonShapes.vue Normal file
View File

@@ -0,0 +1,243 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { appState } from '../store'
import PageLayout from '../components/layout/PageLayout.vue'
import { useScreenActions } from '../composables/useScreenActions'
const { setButtonActions } = useScreenActions()
const route = useRoute()
const router = useRouter()
const pokemon = ref(null)
const pokemonSpecies = ref(null)
const shapeData = ref(null)
const loading = ref(true)
const shapeIcons = {
'ball': '●',
'squiggle': '~',
'fish': '><>',
'arms': '⚔',
'blob': '◉',
'upright': '▲',
'legs': '⚊',
'quadruped': '◆',
'wings': '✦',
'tentacles': '※',
'heads': '◭',
'humanoid': '☗',
'bug-wings': '✧',
'armor': '⬢'
}
const shapeIcon = computed(() => {
if (!shapeData.value) return '◆'
return shapeIcons[shapeData.value.name] || '◆'
})
const sortedNames = computed(() => {
if (!shapeData.value?.names) return []
const priorityLanguages = ['en', 'ja-Hrkt', 'roomaji', 'ko', 'zh-Hans', 'fr', 'de', 'es', 'it']
return [...shapeData.value.names].sort((a, b) => {
const aIndex = priorityLanguages.indexOf(a.language.name)
const bIndex = priorityLanguages.indexOf(b.language.name)
if (aIndex === -1 && bIndex === -1) return 0
if (aIndex === -1) return 1
if (bIndex === -1) return -1
return aIndex - bIndex
})
})
const sortedAwesomeNames = computed(() => {
if (!shapeData.value?.awesome_names) return []
const priorityLanguages = ['en', 'ja-Hrkt', 'roomaji', 'ko', 'zh-Hans', 'fr', 'de', 'es', 'it']
return [...shapeData.value.awesome_names].sort((a, b) => {
const aIndex = priorityLanguages.indexOf(a.language.name)
const bIndex = priorityLanguages.indexOf(b.language.name)
if (aIndex === -1 && bIndex === -1) return 0
if (aIndex === -1) return 1
if (bIndex === -1) return -1
return aIndex - bIndex
})
})
function getPokemonData() {
const currentId = parseInt(route.params.id)
loading.value = true
fetch(`https://pokeapi.co/api/v2/pokemon/${currentId}`)
.then((response) => {
if (!response.ok) {
router.push({ name: '404', params: { catchAll: 'not-found' } })
return null
}
return response.json()
})
.then((json) => {
if (json) pokemon.value = json
})
fetch(`https://pokeapi.co/api/v2/pokemon-species/${currentId}`)
.then((response) => {
if (!response.ok) return null
return response.json()
})
.then((json) => {
if (json) {
pokemonSpecies.value = json
if (json.shape) {
const shapeUrl = json.shape.url
return fetch(shapeUrl)
}
}
return null
})
.then((response) => {
if (!response || !response.ok) return null
return response.json()
})
.then((json) => {
if (json) shapeData.value = json
loading.value = false
})
}
getPokemonData()
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 18l-8-8 8-8 8 8-8 8z',
'SHAPES'
)
setButtonActions([
{ buttonNumber: 1, label: 'BACK', action: () => router.push(`/pokemon/${route.params.id}`) }
])
})
</script>
<template>
<PageLayout
v-if="pokemon && !loading"
:title="pokemon.name"
:badge="pokemon.types.map(t => t.type.name)"
subtitle="Body Shape"
statusText="Shape Information"
>
<div v-if="!shapeData" class="flex items-center justify-center h-full">
<div class="text-center space-y-2">
<div class="text-zinc-600 text-xs"></div>
<p class="text-zinc-500 text-xs">No shape data available</p>
</div>
</div>
<div v-else class="flex flex-col h-full overflow-hidden p-3 gap-3">
<div class="border border-zinc-800 bg-zinc-950/50 rounded overflow-hidden">
<div class="bg-zinc-900 px-2 py-1.5 border-b border-zinc-800">
<h3 class="text-[10px] font-bold text-zinc-300 uppercase tracking-wide">
Body Shape
</h3>
</div>
<div class="p-3 flex items-center gap-3">
<div class="w-20 h-20 rounded border-2 border-zinc-700 bg-zinc-900 flex items-center justify-center shrink-0">
<div class="text-5xl text-zinc-400">{{ shapeIcon }}</div>
</div>
<div class="flex-1 space-y-1">
<div class="text-xl font-bold text-zinc-200 capitalize">
{{ shapeData.name }}
</div>
<div v-if="sortedAwesomeNames.length > 0" class="text-[11px] text-zinc-400 italic">
{{ sortedAwesomeNames[0].awesome_name }}
</div>
<div class="text-[9px] text-zinc-600 mt-2">
Pokédex body shape classification
</div>
</div>
</div>
</div>
<div v-if="sortedAwesomeNames.length > 0" class="border border-zinc-800 bg-zinc-950/50 rounded overflow-hidden">
<div class="bg-zinc-900 px-2 py-1.5 border-b border-zinc-800">
<h3 class="text-[10px] font-bold text-zinc-300 uppercase tracking-wide">
Scientific Names
</h3>
</div>
<div class="p-2 space-y-1.5">
<div
v-for="nameEntry in sortedAwesomeNames"
:key="nameEntry.language.name"
class="flex items-baseline justify-between py-1 px-2 hover:bg-zinc-900/50 rounded"
>
<span class="text-[9px] text-zinc-500 uppercase tracking-wider min-w-15">
{{ nameEntry.language.name }}
</span>
<span class="text-[11px] text-zinc-300 font-medium italic">
{{ nameEntry.awesome_name }}
</span>
</div>
</div>
</div>
<div class="border border-zinc-800 bg-zinc-950/50 rounded overflow-hidden flex-1 min-h-0 flex flex-col">
<div class="bg-zinc-900 px-2 py-1.5 border-b border-zinc-800">
<h3 class="text-[10px] font-bold text-zinc-300 uppercase tracking-wide">
Translations
</h3>
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar p-2 space-y-1.5">
<div
v-for="nameEntry in sortedNames"
:key="nameEntry.language.name"
class="flex items-baseline justify-between py-1 px-2 hover:bg-zinc-900/50 rounded"
>
<span class="text-[9px] text-zinc-500 uppercase tracking-wider min-w-15">
{{ nameEntry.language.name }}
</span>
<span class="text-[11px] text-zinc-300 font-medium">
{{ nameEntry.name }}
</span>
</div>
</div>
</div>
</div>
</PageLayout>
<PageLayout
v-else
title="LOADING"
subtitle="Please wait..."
>
<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">
<div class="absolute inset-0 border-2 border-zinc-800 rounded-full"></div>
<div class="absolute inset-0 border-t-2 border-red-500 rounded-full animate-spin"></div>
<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>
</div>
<div class="text-center">
<div class="text-xs font-bold tracking-[0.2em] text-zinc-500 mb-1">LOADING DATA</div>
<div class="text-[10px] text-zinc-700 animate-pulse">Fetching shape information...</div>
</div>
</div>
</div>
</PageLayout>
</template>

View File

@@ -0,0 +1,246 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { appState } from '../store'
import PageLayout from '../components/layout/PageLayout.vue'
import { useScreenActions } from '../composables/useScreenActions'
const { setButtonActions } = useScreenActions()
const route = useRoute()
const router = useRouter()
const pokemon = ref(null)
const speciesData = ref(null)
const loading = ref(true)
const genderRateText = computed(() => {
if (!speciesData.value) return 'Unknown'
const rate = speciesData.value.gender_rate
if (rate === -1) return 'Genderless'
const femalePercent = (rate / 8) * 100
const malePercent = 100 - femalePercent
return `${femalePercent}% / ♂ ${malePercent}%`
})
const captureRateBars = computed(() => {
if (!speciesData.value) return 0
return Math.ceil((speciesData.value.capture_rate / 255) * 20)
})
function formatName(name) {
return name
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
function getPokemonData() {
const currentId = parseInt(route.params.id)
loading.value = true
fetch(`https://pokeapi.co/api/v2/pokemon/${currentId}`)
.then((response) => {
if (!response.ok) {
router.push({ name: '404', params: { catchAll: 'not-found' } })
return null
}
return response.json()
})
.then((json) => {
if (json) pokemon.value = json
})
fetch(`https://pokeapi.co/api/v2/pokemon-species/${currentId}`)
.then((response) => {
if (!response.ok) return null
return response.json()
})
.then((json) => {
if (json) speciesData.value = json
loading.value = false
})
}
getPokemonData()
onMounted(() => {
appState.setPageDisplay(
'M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm0 18c-4.4 0-8-3.6-8-8s3.6-8 8-8 8 3.6 8 8-3.6 8-8 8zm4-8c0-2.2-1.8-4-4-4s-4 1.8-4 4 1.8 4 4 4 4-1.8 4-4z',
'SPECIES'
)
setButtonActions([
{ buttonNumber: 1, label: 'BACK', action: () => router.push(`/pokemon/${route.params.id}`) }
])
})
</script>
<template>
<PageLayout
v-if="pokemon && !loading"
:title="pokemon.name"
:badge="pokemon.types.map(t => t.type.name)"
subtitle="Species Data"
statusText="Species Information"
>
<div v-if="!speciesData" class="flex items-center justify-center h-full">
<div class="text-center space-y-2">
<div class="text-zinc-600 text-xs"></div>
<p class="text-zinc-500 text-xs">No species data available</p>
</div>
</div>
<div v-else class="flex flex-col h-full overflow-hidden p-3 gap-2">
<div class="flex-1 overflow-y-auto custom-scrollbar space-y-2">
<div v-if="speciesData.is_baby || speciesData.is_legendary || speciesData.is_mythical" class="flex gap-1.5">
<span v-if="speciesData.is_baby" class="text-[8px] bg-pink-900/30 text-pink-300 px-2 py-1 rounded uppercase border border-pink-800">
Baby
</span>
<span v-if="speciesData.is_legendary" class="text-[8px] bg-yellow-900/30 text-yellow-300 px-2 py-1 rounded uppercase border border-yellow-800">
Legendary
</span>
<span v-if="speciesData.is_mythical" class="text-[8px] bg-purple-900/30 text-purple-300 px-2 py-1 rounded uppercase border border-purple-800">
Mythical
</span>
</div>
<div class="border border-zinc-800 bg-zinc-950/50 rounded overflow-hidden">
<div class="bg-zinc-900 px-2 py-1 border-b border-zinc-800">
<h3 class="text-[9px] font-bold text-zinc-300 uppercase tracking-wide">Base Stats</h3>
</div>
<div class="p-2 space-y-2">
<div class="space-y-1">
<div class="flex items-center justify-between">
<span class="text-[9px] text-zinc-500 uppercase">Capture Rate</span>
<span class="text-[9px] text-zinc-400 font-mono">{{ speciesData.capture_rate }}/255</span>
</div>
<div class="flex gap-0.5 h-1">
<div v-for="i in 20" :key="i"
class="flex-1 rounded-sm"
:class="i <= captureRateBars ? 'bg-green-500' : 'bg-zinc-800'">
</div>
</div>
</div>
<div class="flex items-baseline justify-between py-1">
<span class="text-[9px] text-zinc-500 uppercase">Base Happiness</span>
<span class="text-[10px] text-zinc-300 font-mono">{{ speciesData.base_happiness }}</span>
</div>
<div class="flex items-baseline justify-between py-1">
<span class="text-[9px] text-zinc-500 uppercase">Gender Ratio</span>
<span class="text-[9px] text-zinc-300 font-mono">{{ genderRateText }}</span>
</div>
<div class="flex items-baseline justify-between py-1">
<span class="text-[9px] text-zinc-500 uppercase">Hatch Counter</span>
<span class="text-[10px] text-zinc-300 font-mono">{{ speciesData.hatch_counter }}</span>
</div>
</div>
</div>
<div class="border border-zinc-800 bg-zinc-950/50 rounded overflow-hidden">
<div class="bg-zinc-900 px-2 py-1 border-b border-zinc-800">
<h3 class="text-[9px] font-bold text-zinc-300 uppercase tracking-wide">Classification</h3>
</div>
<div class="p-2 space-y-1.5">
<div class="flex items-baseline justify-between py-0.5">
<span class="text-[9px] text-zinc-500 uppercase">Generation</span>
<span class="text-[9px] text-zinc-300">{{ formatName(speciesData.generation.name) }}</span>
</div>
<div class="flex items-baseline justify-between py-0.5">
<span class="text-[9px] text-zinc-500 uppercase">Growth Rate</span>
<span class="text-[9px] text-zinc-300 capitalize">{{ speciesData.growth_rate.name }}</span>
</div>
<div v-if="speciesData.habitat" class="flex items-baseline justify-between py-0.5">
<span class="text-[9px] text-zinc-500 uppercase">Habitat</span>
<span class="text-[9px] text-zinc-300 capitalize">{{ speciesData.habitat.name }}</span>
</div>
<div v-if="speciesData.egg_groups && speciesData.egg_groups.length > 0" class="space-y-1 pt-1">
<span class="text-[9px] text-zinc-500 uppercase">Egg Groups</span>
<div class="flex gap-1 flex-wrap">
<span
v-for="group in speciesData.egg_groups"
:key="group.name"
class="text-[8px] bg-zinc-800 text-zinc-400 px-1.5 py-0.5 rounded capitalize"
>
{{ group.name }}
</span>
</div>
</div>
</div>
</div>
<div v-if="speciesData.evolves_from_species" class="border border-zinc-800 bg-zinc-950/50 rounded overflow-hidden">
<div class="bg-zinc-900 px-2 py-1 border-b border-zinc-800">
<h3 class="text-[9px] font-bold text-zinc-300 uppercase tracking-wide">Evolution</h3>
</div>
<div class="p-2">
<div class="flex items-center gap-2">
<span class="text-[9px] text-zinc-500 uppercase">Evolves From</span>
<span class="text-[10px] text-zinc-300 capitalize">{{ formatName(speciesData.evolves_from_species.name) }}</span>
</div>
</div>
</div>
<div v-if="speciesData.pokedex_numbers && speciesData.pokedex_numbers.length > 0" class="border border-zinc-800 bg-zinc-950/50 rounded overflow-hidden">
<div class="bg-zinc-900 px-2 py-1 border-b border-zinc-800">
<h3 class="text-[9px] font-bold text-zinc-300 uppercase tracking-wide">Pokedex Entries</h3>
</div>
<div class="p-2 space-y-1">
<div
v-for="entry in speciesData.pokedex_numbers"
:key="entry.pokedex.name"
class="flex items-baseline justify-between py-0.5"
>
<span class="text-[9px] text-zinc-500 capitalize">{{ formatName(entry.pokedex.name) }}</span>
<span class="text-[9px] text-zinc-400 font-mono">#{{ String(entry.entry_number).padStart(3, '0') }}</span>
</div>
</div>
</div>
<div v-if="speciesData.has_gender_differences || speciesData.forms_switchable" class="border border-zinc-800 bg-zinc-950/50 rounded overflow-hidden">
<div class="bg-zinc-900 px-2 py-1 border-b border-zinc-800">
<h3 class="text-[9px] font-bold text-zinc-300 uppercase tracking-wide">Additional Info</h3>
</div>
<div class="p-2 space-y-1">
<div v-if="speciesData.has_gender_differences" class="text-[9px] text-zinc-400">
Has gender differences
</div>
<div v-if="speciesData.forms_switchable" class="text-[9px] text-zinc-400">
Forms are switchable
</div>
</div>
</div>
</div>
</div>
</PageLayout>
<PageLayout
v-else
title="LOADING"
subtitle="Please wait..."
>
<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">
<div class="absolute inset-0 border-2 border-zinc-800 rounded-full"></div>
<div class="absolute inset-0 border-t-2 border-red-500 rounded-full animate-spin"></div>
<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>
</div>
<div class="text-center">
<div class="text-xs font-bold tracking-[0.2em] text-zinc-500 mb-1">LOADING DATA</div>
<div class="text-[10px] text-zinc-700 animate-pulse">Fetching species information...</div>
</div>
</div>
</div>
</PageLayout>
</template>

View File

@@ -2,6 +2,11 @@ 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 PokemonLocations from "./pages/PokemonLocations.vue";
import PokemonColors from "./pages/PokemonColors.vue";
import PokemonForms from "./pages/PokemonForms.vue";
import PokemonShapes from "./pages/PokemonShapes.vue";
import PokemonSpecies from "./pages/PokemonSpecies.vue";
import Settings from "./pages/Settings.vue";
import Maps from "./pages/Maps.vue";
import Games from "./pages/Games.vue";
@@ -11,6 +16,11 @@ import NotFound from "./pages/NotFound.vue";
const routes = [
{ path: "/", component: Home, name: "home" },
{ path: "/pokemon/:id", component: Pokedex, name: "pokemon" },
{ path: "/pokemon/:id/locations", component: PokemonLocations, name: "pokemon-locations" },
{ path: "/pokemon/:id/colors", component: PokemonColors, name: "pokemon-colors" },
{ path: "/pokemon/:id/forms", component: PokemonForms, name: "pokemon-forms" },
{ path: "/pokemon/:id/shapes", component: PokemonShapes, name: "pokemon-shapes" },
{ path: "/pokemon/:id/species", component: PokemonSpecies, name: "pokemon-species" },
{ path: "/pokemon", component: Pokemon, name: "pokemon-list" },
{ path: "/settings", component: Settings, name: "settings" },
{ path: "/maps", component: Maps, name: "maps" },