[VUE] How I Built an Animal App and Learned More Than From Courses
1. Introduction
One day, while browsing various fun facts about animals, a thought crossed my mind – what if I created a simple app where you could see a picture of an animal and learn a few key facts about it? The idea seemed intriguing, so I immediately started listing animals that came to mind. At first, of course, they were the most familiar ones – pets and farm animals. But soon, I felt like something was missing.
Fortunately, not long after, on a warm day, I visited the Wrocław Zoo. That’s when it hit me – why not focus exclusively on zoo inhabitants? After all, they are fascinating to many people and often not easily accessible in everyday life.
From idea to action! I grabbed a fresh sheet of paper and started jotting down the key information I wanted to include for each animal. Then came the time to choose the right technology. For a while, I had been thinking about stepping away from backend development for a moment and trying something new in frontend. I decided to go with Vue.js – an easy entry point, familiarity with JavaScript, and a solid dose of curiosity sealed the deal.
And speaking of challenges – where would I get the animal images from? That’s when another idea struck me: if AI can generate images, why not use this as an opportunity to practice writing prompts? A few minutes of research on how to craft the right wording, lighting, and composition… and there it was! The first image appeared on my screen, and my excitement skyrocketed – and this was just the beginning.
What happened next? What challenges did I face, what did I learn, and what does the final result look like? You’ll find all the answers in the following sections of this article. Enjoy the read!
1.1. Interface Design
The idea for an application is the foundation of the entire project – and as we all know, even the most beautiful structure won’t last long without a solid foundation. No one wants to build on shifting sands, only to watch everything crumble to dust moments later. That’s why the first step was launching Lunacy to carefully plan out every element of the interface.
As a math enthusiast (especially when it comes to matrices!), I immediately knew that the animals on the page should be arranged in a structured manner – almost like in a square matrix. My first thought? A 2×2 grid. Sounds elegant, but… way too small. Okay, how about 4×4? A quick sketch, a moment of reflection, and… something just didn’t sit right. It looked decent on desktops, but what about mobile devices? And let’s be honest, "Mobile First" is not just a buzzword – it’s a golden rule!
After a series of experiments and a few minutes of intense screen-staring, I found the perfect compromise: 3×3! That meant nine animals per page. Everything seemed great… until I glanced at my list of animals, which was significantly longer. What to do? Easy – randomized display! After all, the first idea (although not always, as this article proves…) is often the best.
Now for the next challenge: how to present information about each animal? I considered several options – from pop-up tooltips appearing on hover, to a dropdown list next to the animal’s name, to a small (i) icon in the top-right corner of the card. The last option won – simple, intuitive, and doesn’t take up unnecessary space. Click, and boom! All the key details are right there at your fingertips.
A simple sketch of the Animals Living in the ZOO interface with marked components of the main view.
1.2. Choosing Additional Technologies
The first decision is made – the application will be built with Vue.js. But what’s next? Where should the animal data be stored? How should the components be styled? Should I use ready-made solutions like Bootstrap, or should I rely on my own creativity?
Animal Data
I wanted the gathered information to be easily shareable, and at the same time, I saw it as an opportunity to expand my knowledge by browsing animal atlases. The simplest solution? JSON! I created a list of objects, and that’s it – no need for APIs, access configurations, or other backend headaches.
Styling
The rule is simple: if you don’t need to reinvent the wheel, then don’t. But in this case, I decided to go the creative route and write all the styles from scratch. Choosing SASS came naturally – first, it allows for clean and well-organized code, and second, it was a great opportunity to refresh my knowledge of it. To keep things structured, I followed the BEM methodology, ensuring everything stays logical and easy to maintain.
Images
And finally – the graphics. Where to get them? Well, since I wanted to practice writing prompts anyway, I decided to use Microsoft Bing’s image creator. A perfect blend of learning, fun, and… uncertainty about what I would actually get. But hey – no risk, no cool illustrations for the app!
1.3. Runtime Environment
Every developer knows that the less clutter in the system, the better. Installing tons of packages on your computer is a recipe for disaster – let the first one throw their keyboard who has never battled with incompatible library versions. To avoid this nightmare, I went with Docker and created a dedicated container for this project. This way, I can work in a stable, isolated environment without worrying that one wrong move will break my other projects.
FROM node:18-alpine
# Create app directory
WORKDIR /app
# Install Python3 and compilers for node-gyp
RUN apk add --no-cache python3 make g++
# Install app dependencies
COPY package.json package-lock.json ./
# Install dependencies
RUN npm install
# Bundle app source
COPY . .
# Build the app
EXPOSE 8080
# Start the app
CMD ["npm", "run", "serve"]
version: '3.8'
services:
vue-app:
build: .
ports:
- "8080:8080"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
2. Project Structure and Code Organization
A well-designed project structure saves both time and sanity – for me now and for any potential future collaborators (which, let’s be honest, will probably be future me, struggling to remember how everything works in a few months). I decided to organize the directories based on their purpose:
animals-living-in-the-zoo/
|-- public/
|-- src/
| |-- assets/
| | |-- animals/
| | |-- animals.json
| |-- components/
| |-- router/
| |-- views/
| |-- App.vue
| |-- main.js
|-- docker-compose.yml
|-- Dockerfile
|-- package.json
|-- vue.config.js
Key Directories and Files
- public/ – Static files, e.g., index.html.
- src/ – The heart of the application, containing all Vue logic.
- assets/ – Animal images and the JSON database.
- components/ – Smaller Vue components.
- router/ – index.js, defining the application’s routes.
- views/ – Main views, e.g., HomeView.vue, AboutView.vue.
Simple, clear, and ready for future expansion.
3. Routing
Every app needs navigation – and ideally, one that doesn’t mislead users into the void. In my case, there are three main routes:
- Home – Displays the list of animals.
- About – Provides information about the app.
- catchAll(.*) – For all the lost souls who type the wrong address.
This way, even if a user decides to experiment with the URL, they won’t be mercilessly thrown into the abyss of the internet.
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import PageNotFound from '../views/PageNotFound.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue')
},
{
path: '/:catchAll(.*)',
name: 'NotFoundPage',
component: PageNotFound
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
4. Views
All the magic happens in App.vue, which serves as the main template. Here, I declare <router-view />, allowing different views to load dynamically within a shared structure. This includes:
- <NavBar /> – Because every app needs solid navigation.
- <FooterBar /> – Because I’m not one to neglect the bottom of the page.
- created() method – Sets the app name for each view, because organization is key.
<template>
<NavBar />
<router-view />
<FooterBar />
</template>
<script>
import NavBar from "@/components/NavBar.vue";
import FooterBar from "@/components/FooterBar.vue";
export default {
components: {
NavBar,
FooterBar,
},
created() {
document.title = "Animals living in the ZOO";
}
};
</script>
<style>
*,
*::after,
*::before {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 62.5%;
font-family: sans-serif;
scroll-behavior: smooth;
}
.wrapper {
margin: 0 auto;
max-width: 1200px;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
</style>
Inside HomeView.vue, I add a header using the CustomHeader component, passing the value "Animals Living in the ZOO". In the main section, the MainPage component takes over.
<template>
<CustomHeader :text="namePageHeader"/>
<main>
<div class="wrapper">
<MainPage />
</div>
</main>
</template>
<script>
import MainPage from "@/components/MainPage.vue";
import CustomHeader from "@/components/CustomHeader.vue";
export default {
name: "HomeView",
components: {
MainPage,
CustomHeader,
},
data(){
return{
namePageHeader:"Animals Living in the ZOO",
}
}
};
</script>
<style lang="scss" scoped>
main{
min-height: calc(100vh - 120px);
}
</style>
5. Components
The main page view is built around the MainPage component, which loads ListAnimals. To avoid an unsightly screen flicker while loading the list, I use Suspense along with the CustomLoading component, which provides a smooth user experience in the meantime.
<template>
<Suspense>
<template #default>
<ListAnimals/>
</template>
<template #fallback>
<CustomLoading/>
</template>
</Suspense>
</template>
<script>
import ListAnimals from "@/components/ListAnimals.vue";
import CustomLoading from "@/components/CustomLoading.vue";
export default {
name: "MainPage",
components:{
ListAnimals,
CustomLoading,
},
};
</script>
The Most Important Component: ListAnimals
- Fetches data from a JSON file.
- Randomly selects 9 animals.
- Generates their images.
- Renders everything within SingleAnimal components using the v-for loop.
<template>
<div class="animals">
<SingleAnimal v-for="animal in randomAnimals" :key="animal.id" :animal="animal">{{animal.name}}</SingleAnimal>
</div>
</template>
<script>
import { ref } from 'vue';
import SingleAnimal from "@/components/SingleAnimal.vue";
import animalsData from "@/assets/animals.json";
export default {
name: "ListAnimals",
components:{
SingleAnimal,
},
async setup() {
const animals = ref([]);
const randomAnimals = ref([]);
const getRandomAnimals = () => {
const shuffled = animals.value.sort(() => 0.5 - Math.random());
randomAnimals.value = shuffled.slice(0, 9);
const updatedAnimals = randomAnimals.value.map(animal => {
return {
...animal,
image_link: "/animals/"+animal.image_link // Update this address according to requirements
};
});
randomAnimals.value = updatedAnimals;
};
// Set data from JSON file as reference
animals.value = animalsData;
// Get random animals and cut to 9 records
getRandomAnimals();
return { randomAnimals };
},
};
</script>
<style lang="scss" scoped>
.animals{
display:flex;
justify-content:center;
flex-wrap:wrap;
}
</style>
Each SingleAnimal receives an animal object, renders its image and name, and includes the MoreInfoAnimal component, which is controlled by the moreInfoAnimal parameter. By default, the additional information is hidden, and its visibility toggles when the user clicks a button. This is handled using v-show, meaning the content remains in the DOM but is simply not rendered until needed.
<template>
<div class="card">
<img :src="animal.image_link" :alt="animal.name" class="card__image" />
<p class="card__name">{{ animal.name }}</p>
<MoreInfoAnimal v-show="moreInfoVisible" :info="animal" />
<img
v-show="!moreInfoVisible"
src="@/assets/info.svg"
class="card__info-icon"
@click="moreInfoVisible = true"
/>
<img
v-show="moreInfoVisible"
src="@/assets/x-circle.svg"
class="card__info-icon"
@click="moreInfoVisible = false"
/>
</div>
</template>
<script>
import MoreInfoAnimal from "@/components/MoreInfoAnimal.vue";
export default {
name: "SingleAnimal",
components: {
MoreInfoAnimal,
},
props: {
animal: {
type: Object,
required: true,
},
},
data() {
return {
moreInfoVisible: false,
};
},
};
</script>
<style lang="scss" scoped>
.card {
position: relative;
margin: 1em 0.8em;
width: 100%;
border-radius: 1em;
box-shadow: 0 0 5px #000;
overflow: hidden;
height: 50vh;
&__image {
width: 100%;
height: 80%;
object-fit: cover;
}
&__name {
display: flex;
align-items: center;
justify-content: center;
height: 20%;
font-size: 2.2rem;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.2em;
}
&__info-icon {
position: absolute;
top: 1em;
right: 1em;
cursor: pointer;
}
}
@media (min-width: 576px) {
.card {
height: 70vh;
width: calc(50% - (2 * 0.8em));
}
}
@media (min-width: 992px) {
.card {
height: 90vh;
width: 30%;
}
}
</style>
6. JSON Structure
I store animal data in a JSON file, which contains a list of objects. Each object includes basic information, such as:
{
"id": 6,
"name": "Cheetah",
"latin_name": "Acinonyx jubatus",
"polish_name": "Gepard",
"animal_type": "mammal",
"active_time": "morning and evening hours",
"length_max": "110 - 150",
"weight_max": "21-72",
"lifespan": "12-15",
"habitat": "Such as savannahs, arid mountain ranges in the Sahara and hilly desert terrain",
"geo_range": "Iran",
"diet": "small antelope, including springbok, steenbok, duikers, impala and gazelles",
"image_link": "cheetah.jpg"
}
Thanks to this approach, the application doesn’t require a backend, allowing me to focus solely on Vue.
7. Graphics
Writing AI prompts is a true art form—kind of like negotiating with a stubborn artist who pretends to understand you but still does things their own way. After several attempts, I realized the key factors are:
- Purpose & Context – The image should be for an app, not a modern art gallery.
- Lighting & Style – Natural lighting, no dramatic shadows.
- Mood & Perspective – No one wants to see gloomy, blurry images.
- Image Size – Because "too small" or "too big" is always an issue.
In the end, I successfully generated satisfying graphics using Microsoft Bing’s image creator—with a bit of frustration but also the joy of finally getting something that looked right.
Generate an image of a Common Hippopotamus. The image type should be a photograph. The lighting should be natural. Apply a cinematic filter. The mood should be calm, and the perspective should be from the front but wide enough to show the entire animal! The image size should be vertical (9:16). This is very important. The animal should be in its natural habitat.
8. Summary
While working on this project, I:
- Learned the basics of Vue.js.
- Discovered the power of Suspense and other built-in components
- Tested routing and component communication.
- Refreshed my SASS + BEM styling skills.
- Experimented with AI-generated images.
- Learned tons of interesting facts about animals.
- And most importantly – I had a blast!
This was a great decision—not only did I learn new things, but I also built something that actually works and might be useful!
9 Final Effect
- Link to the application: Animals Living in the ZOO
- GitHub repository: GitHub repository
And now, the moment you’ve been waiting
1. Introduction
One day, while browsing various fun facts about animals, a thought crossed my mind – what if I created a simple app where you could see a picture of an animal and learn a few key facts about it? The idea seemed intriguing, so I immediately started listing animals that came to mind. At first, of course, they were the most familiar ones – pets and farm animals. But soon, I felt like something was missing.
Fortunately, not long after, on a warm day, I visited the Wrocław Zoo. That’s when it hit me – why not focus exclusively on zoo inhabitants? After all, they are fascinating to many people and often not easily accessible in everyday life.
From idea to action! I grabbed a fresh sheet of paper and started jotting down the key information I wanted to include for each animal. Then came the time to choose the right technology. For a while, I had been thinking about stepping away from backend development for a moment and trying something new in frontend. I decided to go with Vue.js – an easy entry point, familiarity with JavaScript, and a solid dose of curiosity sealed the deal.
And speaking of challenges – where would I get the animal images from? That’s when another idea struck me: if AI can generate images, why not use this as an opportunity to practice writing prompts? A few minutes of research on how to craft the right wording, lighting, and composition… and there it was! The first image appeared on my screen, and my excitement skyrocketed – and this was just the beginning.
What happened next? What challenges did I face, what did I learn, and what does the final result look like? You’ll find all the answers in the following sections of this article. Enjoy the read!
1.1. Interface Design
The idea for an application is the foundation of the entire project – and as we all know, even the most beautiful structure won’t last long without a solid foundation. No one wants to build on shifting sands, only to watch everything crumble to dust moments later. That’s why the first step was launching Lunacy to carefully plan out every element of the interface.
As a math enthusiast (especially when it comes to matrices!), I immediately knew that the animals on the page should be arranged in a structured manner – almost like in a square matrix. My first thought? A 2×2 grid. Sounds elegant, but… way too small. Okay, how about 4×4? A quick sketch, a moment of reflection, and… something just didn’t sit right. It looked decent on desktops, but what about mobile devices? And let’s be honest, "Mobile First" is not just a buzzword – it’s a golden rule!
After a series of experiments and a few minutes of intense screen-staring, I found the perfect compromise: 3×3! That meant nine animals per page. Everything seemed great… until I glanced at my list of animals, which was significantly longer. What to do? Easy – randomized display! After all, the first idea (although not always, as this article proves…) is often the best.
Now for the next challenge: how to present information about each animal? I considered several options – from pop-up tooltips appearing on hover, to a dropdown list next to the animal’s name, to a small (i) icon in the top-right corner of the card. The last option won – simple, intuitive, and doesn’t take up unnecessary space. Click, and boom! All the key details are right there at your fingertips.
A simple sketch of the Animals Living in the ZOO interface with marked components of the main view.
1.2. Choosing Additional Technologies
The first decision is made – the application will be built with Vue.js. But what’s next? Where should the animal data be stored? How should the components be styled? Should I use ready-made solutions like Bootstrap, or should I rely on my own creativity?
Animal Data
I wanted the gathered information to be easily shareable, and at the same time, I saw it as an opportunity to expand my knowledge by browsing animal atlases. The simplest solution? JSON! I created a list of objects, and that’s it – no need for APIs, access configurations, or other backend headaches.
Styling
The rule is simple: if you don’t need to reinvent the wheel, then don’t. But in this case, I decided to go the creative route and write all the styles from scratch. Choosing SASS came naturally – first, it allows for clean and well-organized code, and second, it was a great opportunity to refresh my knowledge of it. To keep things structured, I followed the BEM methodology, ensuring everything stays logical and easy to maintain.
Images
And finally – the graphics. Where to get them? Well, since I wanted to practice writing prompts anyway, I decided to use Microsoft Bing’s image creator. A perfect blend of learning, fun, and… uncertainty about what I would actually get. But hey – no risk, no cool illustrations for the app!
1.3. Runtime Environment
Every developer knows that the less clutter in the system, the better. Installing tons of packages on your computer is a recipe for disaster – let the first one throw their keyboard who has never battled with incompatible library versions. To avoid this nightmare, I went with Docker and created a dedicated container for this project. This way, I can work in a stable, isolated environment without worrying that one wrong move will break my other projects.
FROM node:18-alpine
# Create app directory
WORKDIR /app
# Install Python3 and compilers for node-gyp
RUN apk add --no-cache python3 make g++
# Install app dependencies
COPY package.json package-lock.json ./
# Install dependencies
RUN npm install
# Bundle app source
COPY . .
# Build the app
EXPOSE 8080
# Start the app
CMD ["npm", "run", "serve"]
version: '3.8'
services:
vue-app:
build: .
ports:
- "8080:8080"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
2. Project Structure and Code Organization
A well-designed project structure saves both time and sanity – for me now and for any potential future collaborators (which, let’s be honest, will probably be future me, struggling to remember how everything works in a few months). I decided to organize the directories based on their purpose:
animals-living-in-the-zoo/
|-- public/
|-- src/
| |-- assets/
| | |-- animals/
| | |-- animals.json
| |-- components/
| |-- router/
| |-- views/
| |-- App.vue
| |-- main.js
|-- docker-compose.yml
|-- Dockerfile
|-- package.json
|-- vue.config.js
Key Directories and Files
- public/ – Static files, e.g., index.html.
- src/ – The heart of the application, containing all Vue logic.
- assets/ – Animal images and the JSON database.
- components/ – Smaller Vue components.
- router/ – index.js, defining the application’s routes.
- views/ – Main views, e.g., HomeView.vue, AboutView.vue.
Simple, clear, and ready for future expansion.
3. Routing
Every app needs navigation – and ideally, one that doesn’t mislead users into the void. In my case, there are three main routes:
- Home – Displays the list of animals.
- About – Provides information about the app.
- catchAll(.*) – For all the lost souls who type the wrong address.
This way, even if a user decides to experiment with the URL, they won’t be mercilessly thrown into the abyss of the internet.
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import PageNotFound from '../views/PageNotFound.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue')
},
{
path: '/:catchAll(.*)',
name: 'NotFoundPage',
component: PageNotFound
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
4. Views
All the magic happens in App.vue, which serves as the main template. Here, I declare <router-view />, allowing different views to load dynamically within a shared structure. This includes:
- <NavBar /> – Because every app needs solid navigation.
- <FooterBar /> – Because I’m not one to neglect the bottom of the page.
- created() method – Sets the app name for each view, because organization is key.
<template>
<NavBar />
<router-view />
<FooterBar />
</template>
<script>
import NavBar from "@/components/NavBar.vue";
import FooterBar from "@/components/FooterBar.vue";
export default {
components: {
NavBar,
FooterBar,
},
created() {
document.title = "Animals living in the ZOO";
}
};
</script>
<style>
*,
*::after,
*::before {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 62.5%;
font-family: sans-serif;
scroll-behavior: smooth;
}
.wrapper {
margin: 0 auto;
max-width: 1200px;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
</style>
Inside HomeView.vue, I add a header using the CustomHeader component, passing the value "Animals Living in the ZOO". In the main section, the MainPage component takes over.
<template>
<CustomHeader :text="namePageHeader"/>
<main>
<div class="wrapper">
<MainPage />
</div>
</main>
</template>
<script>
import MainPage from "@/components/MainPage.vue";
import CustomHeader from "@/components/CustomHeader.vue";
export default {
name: "HomeView",
components: {
MainPage,
CustomHeader,
},
data(){
return{
namePageHeader:"Animals Living in the ZOO",
}
}
};
</script>
<style lang="scss" scoped>
main{
min-height: calc(100vh - 120px);
}
</style>
5. Components
The main page view is built around the MainPage component, which loads ListAnimals. To avoid an unsightly screen flicker while loading the list, I use Suspense along with the CustomLoading component, which provides a smooth user experience in the meantime.
<template>
<Suspense>
<template #default>
<ListAnimals/>
</template>
<template #fallback>
<CustomLoading/>
</template>
</Suspense>
</template>
<script>
import ListAnimals from "@/components/ListAnimals.vue";
import CustomLoading from "@/components/CustomLoading.vue";
export default {
name: "MainPage",
components:{
ListAnimals,
CustomLoading,
},
};
</script>
The Most Important Component: ListAnimals
- Fetches data from a JSON file.
- Randomly selects 9 animals.
- Generates their images.
- Renders everything within SingleAnimal components using the v-for loop.
<template>
<div class="animals">
<SingleAnimal v-for="animal in randomAnimals" :key="animal.id" :animal="animal">{{animal.name}}</SingleAnimal>
</div>
</template>
<script>
import { ref } from 'vue';
import SingleAnimal from "@/components/SingleAnimal.vue";
import animalsData from "@/assets/animals.json";
export default {
name: "ListAnimals",
components:{
SingleAnimal,
},
async setup() {
const animals = ref([]);
const randomAnimals = ref([]);
const getRandomAnimals = () => {
const shuffled = animals.value.sort(() => 0.5 - Math.random());
randomAnimals.value = shuffled.slice(0, 9);
const updatedAnimals = randomAnimals.value.map(animal => {
return {
...animal,
image_link: "/animals/"+animal.image_link // Update this address according to requirements
};
});
randomAnimals.value = updatedAnimals;
};
// Set data from JSON file as reference
animals.value = animalsData;
// Get random animals and cut to 9 records
getRandomAnimals();
return { randomAnimals };
},
};
</script>
<style lang="scss" scoped>
.animals{
display:flex;
justify-content:center;
flex-wrap:wrap;
}
</style>
Each SingleAnimal receives an animal object, renders its image and name, and includes the MoreInfoAnimal component, which is controlled by the moreInfoAnimal parameter. By default, the additional information is hidden, and its visibility toggles when the user clicks a button. This is handled using v-show, meaning the content remains in the DOM but is simply not rendered until needed.
<template>
<div class="card">
<img :src="animal.image_link" :alt="animal.name" class="card__image" />
<p class="card__name">{{ animal.name }}</p>
<MoreInfoAnimal v-show="moreInfoVisible" :info="animal" />
<img
v-show="!moreInfoVisible"
src="@/assets/info.svg"
class="card__info-icon"
@click="moreInfoVisible = true"
/>
<img
v-show="moreInfoVisible"
src="@/assets/x-circle.svg"
class="card__info-icon"
@click="moreInfoVisible = false"
/>
</div>
</template>
<script>
import MoreInfoAnimal from "@/components/MoreInfoAnimal.vue";
export default {
name: "SingleAnimal",
components: {
MoreInfoAnimal,
},
props: {
animal: {
type: Object,
required: true,
},
},
data() {
return {
moreInfoVisible: false,
};
},
};
</script>
<style lang="scss" scoped>
.card {
position: relative;
margin: 1em 0.8em;
width: 100%;
border-radius: 1em;
box-shadow: 0 0 5px #000;
overflow: hidden;
height: 50vh;
&__image {
width: 100%;
height: 80%;
object-fit: cover;
}
&__name {
display: flex;
align-items: center;
justify-content: center;
height: 20%;
font-size: 2.2rem;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.2em;
}
&__info-icon {
position: absolute;
top: 1em;
right: 1em;
cursor: pointer;
}
}
@media (min-width: 576px) {
.card {
height: 70vh;
width: calc(50% - (2 * 0.8em));
}
}
@media (min-width: 992px) {
.card {
height: 90vh;
width: 30%;
}
}
</style>
6. JSON Structure
I store animal data in a JSON file, which contains a list of objects. Each object includes basic information, such as:
{
"id": 6,
"name": "Cheetah",
"latin_name": "Acinonyx jubatus",
"polish_name": "Gepard",
"animal_type": "mammal",
"active_time": "morning and evening hours",
"length_max": "110 - 150",
"weight_max": "21-72",
"lifespan": "12-15",
"habitat": "Such as savannahs, arid mountain ranges in the Sahara and hilly desert terrain",
"geo_range": "Iran",
"diet": "small antelope, including springbok, steenbok, duikers, impala and gazelles",
"image_link": "cheetah.jpg"
}
Thanks to this approach, the application doesn’t require a backend, allowing me to focus solely on Vue.
7. Graphics
Writing AI prompts is a true art form—kind of like negotiating with a stubborn artist who pretends to understand you but still does things their own way. After several attempts, I realized the key factors are:
- Purpose & Context – The image should be for an app, not a modern art gallery.
- Lighting & Style – Natural lighting, no dramatic shadows.
- Mood & Perspective – No one wants to see gloomy, blurry images.
- Image Size – Because "too small" or "too big" is always an issue.
In the end, I successfully generated satisfying graphics using Microsoft Bing’s image creator—with a bit of frustration but also the joy of finally getting something that looked right.
Generate an image of a Common Hippopotamus. The image type should be a photograph. The lighting should be natural. Apply a cinematic filter. The mood should be calm, and the perspective should be from the front but wide enough to show the entire animal! The image size should be vertical (9:16). This is very important. The animal should be in its natural habitat.
8. Summary
While working on this project, I:
- Learned the basics of Vue.js.
- Discovered the power of Suspense and other built-in components
- Tested routing and component communication.
- Refreshed my SASS + BEM styling skills.
- Experimented with AI-generated images.
- Learned tons of interesting facts about animals.
- And most importantly – I had a blast!
This was a great decision—not only did I learn new things, but I also built something that actually works and might be useful!
9 Final Effect
- Link to the application: Animals Living in the ZOO
- GitHub repository: GitHub repository
And now, the moment you’ve been waiting