[VUE] Jak zbudowałem aplikację o zwierzętach i nauczyłem się więcej niż na kursach?
1. Wstęp
Pewnego dnia, przeglądając różne ciekawostki o zwierzętach, naszła mnie myśl - a co, gdyby stworzyć prostą aplikację, w której można by zobaczyć zdjęcie zwierzaka i poznać kilka najważniejszych informacji na jego temat? Pomysł wydawał się intrygujący, więc od razu zabrałem się za spisywanie listy zwierząt, które przyszły mi do głowy. Na początku były to oczywiście te najbliższe – domowe pupile i zwierzęta gospodarskie. Jednak szybko poczułem, że czegoś tu brakuje.
Na szczęście, wkrótce później, w jeden z ciepłych dni odwiedziłem wrocławskie ZOO. Wtedy wpadłem na pomysł, aby skupić się wyłącznie na mieszkańcach ogrodów zoologicznych – w końcu to one są dla wielu osób fascynujące, a jednocześnie często trudno dostępne w codziennym życiu.
Od pomysłu do działania! Chwyciłem za nową, świeżą kartkę i zacząłem spisywać najważniejsze informacje, jakie chciałbym mieć o każdym zwierzaku. Potem przyszła pora na wybór technologii. Od jakiegoś czasu chodziło mi po głowie, żeby oderwać się na chwilę od backendu i spróbować czegoś nowego w frontendzie. Wybór padł na Vue.js – prosty start, znajomość JavaScriptu i spora dawka ciekawości przesądziły sprawę.
A skoro już mowa o wyzwaniach – skąd wziąć zdjęcia zwierząt? Tu pojawił się kolejny pomysł: skoro można wygenerować je za pomocą AI, to czemu nie przy okazji przećwiczyć pisania promptów? Kilka minut lektury na temat odpowiedniego doboru słów, światła i kompozycji… i gotowe! Pierwsza grafika pojawiła się na ekranie, a moje emocje sięgnęły zenitu – a przecież to dopiero początek.
Co było dalej? Jakie pojawiły się wyzwania, czego się nauczyłem i jak finalnie wygląda efekt mojej pracy? O tym przeczytasz w dalszej części artykułu. Zapraszam do lektury!
1.1. Projekt interfejsu
Pomysł na aplikację to fundament projektu – a jak wiadomo, bez solidnego fundamentu nawet najpiękniejsza budowla długo nie przetrwa. Nikt nie chce budować na ruchomych piaskach, by po chwili patrzeć, jak wszystko rozsypuje się w proch. Dlatego pierwszym krokiem było uruchomienie programu Lunacy, by dokładnie zaplanować każdy element interfejsu.
Jako miłośnik matematyki (a zwłaszcza macierzy!) od razu wiedziałem, że zwierzaki na stronie powinny być rozmieszczone w uporządkowany sposób – niemal jak w macierzy kwadratowej. Pierwsza myśl? Układ 2×2. Brzmi elegancko, ale… zdecydowanie za mało. No to może 4×4? Chwila zastanowienia, szybki szkic i… coś mi tu nie pasuje. Na komputerach jeszcze jakoś to wygląda, ale co z urządzeniami mobilnymi? A przecież „Mobile First” to nie tylko hasło, ale święta zasada!
Po serii eksperymentów i kilku minutach intensywnego wpatrywania się w ekran, kompromis został osiągnięty: 3×3! To oznacza, że na jednej stronie zobaczymy dziewięć zwierzaków. Wszystko super, dopóki nie rzuciłem okiem na moją listę… która była zdecydowanie dłuższa. Co robić? Proste – losowe wyświetlanie! W końcu pierwsza myśl (choć nie zawsze, jak pokazuje ten artykuł…) bywa najlepsza.
Teraz kolejna kwestia: jak prezentować informacje o zwierzaku? Pomysłów było kilka – od dymków pojawiających się po najechaniu myszką, przez rozwijaną listę przy nazwie zwierzaka, aż po małą ikonkę z literą (i) w prawym górnym rogu karty. To ostatnie rozwiązanie wygrało – proste, intuicyjne i nie zajmuje zbędnego miejsca. Klik i gotowe – wszystkie najważniejsze informacje pod ręką!
Prosty szkic interfejsu aplikacji Animals Living in the ZOO z zaznaczonymi komponentami widoku głównego.
1.2. Wybór innych technologii
Pierwszy wybór już mamy – aplikacja powstaje we Vue.js. Ale co dalej? Gdzie przechowywać dane o zwierzętach? Jak zadbać o stylowanie komponentów? Skorzystać z gotowych rozwiązań jak Bootstrap, czy może postawić na własną kreatywność?
Dane o zwierzętach
Chciałem, żeby zebrane informacje były łatwe do udostępnienia, a przy okazji, żebym mógł poszerzyć wiedzę, przeglądając atlasy zwierząt. Najprostsze rozwiązanie? JSON! Tworzę listę obiektów i gotowe – bez potrzeby pisania API, konfigurowania dostępu i innych backendowych ceregieli.
Stylowanie
Zasada jest prosta: jeśli nie trzeba wyważać otwartych drzwi, to lepiej tego nie robić. Ale w tym przypadku postanowiłem postawić na kreatywność i samodzielnie napisać wszystkie komponenty. Decyzja o wyborze SASS przyszła naturalnie – po pierwsze, pozwala na czytelny i dobrze zorganizowany kod, a po drugie, to świetna okazja, by odświeżyć sobie jego znajomość. Aby zachować porządek w strukturze stylów, wybrałem metodologię BEM – dzięki temu wszystko jest logiczne i łatwe do utrzymania w ryzach.
Obrazki
Na koniec – grafiki. Skąd je wziąć? Cóż, skoro i tak chciałem poćwiczyć pisanie promptów, postanowiłem skorzystać z kreatora obrazów Microsoft Bing. To połączenie nauki, zabawy i… niepewności, co tak naprawdę dostanę. Ale jak to mówią – kto nie ryzykuje, ten nie ma fajnych ilustracji w aplikacji!
1.3. Środowisko uruchomieniowe
Każdy developer wie, że im mniej bałaganu w systemie, tym lepiej. Instalowanie sterty paczek na komputerze to przepis na chaos – niech pierwszy rzuci klawiaturą ten, kto nigdy nie walczył z niekompatybilnymi wersjami bibliotek. Żeby tego uniknąć, postawiłem na Docker i stworzyłem osobny kontener dla tego projektu. Dzięki temu mogę pracować w stabilnym, odizolowanym środowisku, bez obawy, że coś przypadkiem rozwali mi inne projekty.
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. Struktura projektu i organizacja kodu
Dobrze zaprojektowana struktura projektu to oszczędność czasu i nerwów – zarówno dla mnie, jak i dla potencjalnych przyszłych współpracowników (czyli najpewniej mnie samego za kilka miesięcy, kiedy kompletnie zapomnę, jak to wszystko działa). Postanowiłem oprzeć katalogi na ich przeznaczeniu:
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
Najważniejsze informacje o katalogach i plikach
- public/ – pliki statyczne, np. index.html.
- src/ – serce aplikacji, cała logika Vue.
- assets/ – obrazy zwierząt i plik JSON z bazą danych.
- components/ – mniejsze komponenty Vue.
- router/ – index.js, czyli mapa tras aplikacji.
- views/ – główne widoki, np. HomeView.vue, AboutView.vue.
Proste, przejrzyste i gotowe na ewentualną rozbudowę.
3. Routing
Każda aplikacja potrzebuje nawigacji – i to najlepiej takiej, która nie wyprowadzi użytkownika w pole. W moim przypadku mamy trzy główne trasy:
- Home – lista zwierząt.
- About – opis aplikacji.
- catchAll(.*) – dla wszystkich zagubionych dusz, które wpiszą błędny adres.
Dzięki temu, nawet jeśli użytkownik postanowi trochę poeksperymentować z URL-em, nie zostanie brutalnie wyrzucony w pustkę internetu.
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. Widoki
Cała magia dzieje się w App.vue, który pełni rolę szablonu głównego. To tutaj deklaruję <router-view />, co pozwala na dynamiczne ładowanie poszczególnych widoków w jednej wspólnej części. W skład tego wchodzą:
- <NavBar /> – bo każda aplikacja musi mieć porządną nawigację.
- <FooterBar /> – żeby nie było, że zapominam o końcówce strony.
- Metoda created() – ustawia nazwę aplikacji dla każdego widoku, bo organizacja to podstawa.
<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>
W HomeView.vue umieszczam nagłówek w postaci komponentu CustomHeader, do którego przekazuję wartość "Animals Living in the ZOO", a w sekcji głównej pojawia się komponent MainPage.
<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. Komponenty
Widok strony głównej opiera się na MainPage, który wczytuje komponent ListAnimals. Aby uniknąć nieeleganckiego „mrugania” ekranu przy ładowaniu listy, wykorzystuję Suspense i komponent CustomLoading, który elegancko zajmuje użytkownika w międzyczasie.
<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>
Najważniejszy komponent to ListAnimals:
- Pobiera dane z pliku JSON.
- Losuje 9 zwierząt.
- Generuje ich obrazki.
- Renderuje wszystko w komponentach SingleAnimal, korzystając z pętli v-for.
<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>
Każdy SingleAnimal przyjmuje obiekt zwierzaka, renderuje obrazek i jego nazwę, a dodatkowo zawiera komponent MoreInfoAnimal, który jest kontrolowany przez parametr moreInfoAnimal. Domyślnie informacje są ukryte, a ich widoczność zmieniam po kliknięciu w przycisk. Całość działa na v-show, czyli nie znika z DOM-u – po prostu nie jest renderowana.
<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. Struktura JSON
Dane o zwierzakach przechowuję w pliku JSON, który zawiera listę obiektów. Każdy obiekt ma podstawowe informacje, jak np.:
{
"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"
}
Dzięki temu aplikacja nie wymaga backendu, a ja mogę się skupić na samym Vue.
7. Grafiki
Pisanie promptów do AI to prawdziwa sztuka – coś jak negocjacje z upartym artystą, który niby rozumie, co do niego mówisz, ale i tak zrobi po swojemu. Po kilku próbach doszedłem do wniosku, że kluczowe są:
- Cel i kontekst – zdjęcie do aplikacji, a nie galeria sztuki nowoczesnej.
- Światło i styl – naturalne oświetlenie, brak dramatycznych cieni.
- Nastrój i perspektywa – nikt nie chce oglądać ponurych, nieostrych zdjęć.
- Rozmiar zdjęcia – bo „za małe” lub „za duże” to wieczny problem.
Ostatecznie udało mi się wygenerować satysfakcjonujące grafiki przy pomocy kreatora obrazów Microsoft Bing – z odrobiną frustracji, ale i satysfakcją, gdy w końcu wyszło coś sensownego.
Utwórz zdjęcie Common hippopotamus. Rodzaj obrazu zdjęcie. Światło powinno być naturalne. Zastosuj filtr kinowy. Nastrój powinien być spokojny, perspektywa z przodu ale tak aby było widać zwierzę w całości! Rozmiar zdjęcia pionowy (9:16). To bardzo ważne. Zwierzę powinno być w swoim naturalnym środowisku.
8. Podsumowanie
Podczas pracy nad projektem:
- Nauczyłem się podstaw Vue.js.
- Odkryłem moc Suspense i innych wbudowanych komponentów.
- Przetestowałem routing i komunikację między komponentami.
- Przypomniałem sobie stylowanie w SASS + BEM.
- Poeksperymentowałem z generowaniem obrazków przez AI.
- Dowiedziałem się mnóstwa ciekawych rzeczy o zwierzętach.
- I co najważniejsze – świetnie się przy tym bawiłem!
To była dobra decyzja – nie tylko nauczyłem się nowych rzeczy, ale też stworzyłem coś, co faktycznie działa i może być użyteczne.
9 Efekt końcowy
- Link do aplikacji: Animals Living in the ZOO
- Kod na GitHub: Repozytorium GitHub
I teraz, moment na który czekaliście…

1. Wstęp
Pewnego dnia, przeglądając różne ciekawostki o zwierzętach, naszła mnie myśl - a co, gdyby stworzyć prostą aplikację, w której można by zobaczyć zdjęcie zwierzaka i poznać kilka najważniejszych informacji na jego temat? Pomysł wydawał się intrygujący, więc od razu zabrałem się za spisywanie listy zwierząt, które przyszły mi do głowy. Na początku były to oczywiście te najbliższe – domowe pupile i zwierzęta gospodarskie. Jednak szybko poczułem, że czegoś tu brakuje.
Na szczęście, wkrótce później, w jeden z ciepłych dni odwiedziłem wrocławskie ZOO. Wtedy wpadłem na pomysł, aby skupić się wyłącznie na mieszkańcach ogrodów zoologicznych – w końcu to one są dla wielu osób fascynujące, a jednocześnie często trudno dostępne w codziennym życiu.
Od pomysłu do działania! Chwyciłem za nową, świeżą kartkę i zacząłem spisywać najważniejsze informacje, jakie chciałbym mieć o każdym zwierzaku. Potem przyszła pora na wybór technologii. Od jakiegoś czasu chodziło mi po głowie, żeby oderwać się na chwilę od backendu i spróbować czegoś nowego w frontendzie. Wybór padł na Vue.js – prosty start, znajomość JavaScriptu i spora dawka ciekawości przesądziły sprawę.
A skoro już mowa o wyzwaniach – skąd wziąć zdjęcia zwierząt? Tu pojawił się kolejny pomysł: skoro można wygenerować je za pomocą AI, to czemu nie przy okazji przećwiczyć pisania promptów? Kilka minut lektury na temat odpowiedniego doboru słów, światła i kompozycji… i gotowe! Pierwsza grafika pojawiła się na ekranie, a moje emocje sięgnęły zenitu – a przecież to dopiero początek.
Co było dalej? Jakie pojawiły się wyzwania, czego się nauczyłem i jak finalnie wygląda efekt mojej pracy? O tym przeczytasz w dalszej części artykułu. Zapraszam do lektury!
1.1. Projekt interfejsu
Pomysł na aplikację to fundament projektu – a jak wiadomo, bez solidnego fundamentu nawet najpiękniejsza budowla długo nie przetrwa. Nikt nie chce budować na ruchomych piaskach, by po chwili patrzeć, jak wszystko rozsypuje się w proch. Dlatego pierwszym krokiem było uruchomienie programu Lunacy, by dokładnie zaplanować każdy element interfejsu.
Jako miłośnik matematyki (a zwłaszcza macierzy!) od razu wiedziałem, że zwierzaki na stronie powinny być rozmieszczone w uporządkowany sposób – niemal jak w macierzy kwadratowej. Pierwsza myśl? Układ 2×2. Brzmi elegancko, ale… zdecydowanie za mało. No to może 4×4? Chwila zastanowienia, szybki szkic i… coś mi tu nie pasuje. Na komputerach jeszcze jakoś to wygląda, ale co z urządzeniami mobilnymi? A przecież „Mobile First” to nie tylko hasło, ale święta zasada!
Po serii eksperymentów i kilku minutach intensywnego wpatrywania się w ekran, kompromis został osiągnięty: 3×3! To oznacza, że na jednej stronie zobaczymy dziewięć zwierzaków. Wszystko super, dopóki nie rzuciłem okiem na moją listę… która była zdecydowanie dłuższa. Co robić? Proste – losowe wyświetlanie! W końcu pierwsza myśl (choć nie zawsze, jak pokazuje ten artykuł…) bywa najlepsza.
Teraz kolejna kwestia: jak prezentować informacje o zwierzaku? Pomysłów było kilka – od dymków pojawiających się po najechaniu myszką, przez rozwijaną listę przy nazwie zwierzaka, aż po małą ikonkę z literą (i) w prawym górnym rogu karty. To ostatnie rozwiązanie wygrało – proste, intuicyjne i nie zajmuje zbędnego miejsca. Klik i gotowe – wszystkie najważniejsze informacje pod ręką!

1.2. Wybór innych technologii
Pierwszy wybór już mamy – aplikacja powstaje we Vue.js. Ale co dalej? Gdzie przechowywać dane o zwierzętach? Jak zadbać o stylowanie komponentów? Skorzystać z gotowych rozwiązań jak Bootstrap, czy może postawić na własną kreatywność?
Dane o zwierzętach
Chciałem, żeby zebrane informacje były łatwe do udostępnienia, a przy okazji, żebym mógł poszerzyć wiedzę, przeglądając atlasy zwierząt. Najprostsze rozwiązanie? JSON! Tworzę listę obiektów i gotowe – bez potrzeby pisania API, konfigurowania dostępu i innych backendowych ceregieli.
Stylowanie
Zasada jest prosta: jeśli nie trzeba wyważać otwartych drzwi, to lepiej tego nie robić. Ale w tym przypadku postanowiłem postawić na kreatywność i samodzielnie napisać wszystkie komponenty. Decyzja o wyborze SASS przyszła naturalnie – po pierwsze, pozwala na czytelny i dobrze zorganizowany kod, a po drugie, to świetna okazja, by odświeżyć sobie jego znajomość. Aby zachować porządek w strukturze stylów, wybrałem metodologię BEM – dzięki temu wszystko jest logiczne i łatwe do utrzymania w ryzach.
Obrazki
Na koniec – grafiki. Skąd je wziąć? Cóż, skoro i tak chciałem poćwiczyć pisanie promptów, postanowiłem skorzystać z kreatora obrazów Microsoft Bing. To połączenie nauki, zabawy i… niepewności, co tak naprawdę dostanę. Ale jak to mówią – kto nie ryzykuje, ten nie ma fajnych ilustracji w aplikacji!
1.3. Środowisko uruchomieniowe
Każdy developer wie, że im mniej bałaganu w systemie, tym lepiej. Instalowanie sterty paczek na komputerze to przepis na chaos – niech pierwszy rzuci klawiaturą ten, kto nigdy nie walczył z niekompatybilnymi wersjami bibliotek. Żeby tego uniknąć, postawiłem na Docker i stworzyłem osobny kontener dla tego projektu. Dzięki temu mogę pracować w stabilnym, odizolowanym środowisku, bez obawy, że coś przypadkiem rozwali mi inne projekty.
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. Struktura projektu i organizacja kodu
Dobrze zaprojektowana struktura projektu to oszczędność czasu i nerwów – zarówno dla mnie, jak i dla potencjalnych przyszłych współpracowników (czyli najpewniej mnie samego za kilka miesięcy, kiedy kompletnie zapomnę, jak to wszystko działa). Postanowiłem oprzeć katalogi na ich przeznaczeniu:
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
Najważniejsze informacje o katalogach i plikach
- public/ – pliki statyczne, np. index.html.
- src/ – serce aplikacji, cała logika Vue.
- assets/ – obrazy zwierząt i plik JSON z bazą danych.
- components/ – mniejsze komponenty Vue.
- router/ – index.js, czyli mapa tras aplikacji.
- views/ – główne widoki, np. HomeView.vue, AboutView.vue.
Proste, przejrzyste i gotowe na ewentualną rozbudowę.
3. Routing
Każda aplikacja potrzebuje nawigacji – i to najlepiej takiej, która nie wyprowadzi użytkownika w pole. W moim przypadku mamy trzy główne trasy:
- Home – lista zwierząt.
- About – opis aplikacji.
- catchAll(.*) – dla wszystkich zagubionych dusz, które wpiszą błędny adres.
Dzięki temu, nawet jeśli użytkownik postanowi trochę poeksperymentować z URL-em, nie zostanie brutalnie wyrzucony w pustkę internetu.
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. Widoki
Cała magia dzieje się w App.vue, który pełni rolę szablonu głównego. To tutaj deklaruję <router-view />, co pozwala na dynamiczne ładowanie poszczególnych widoków w jednej wspólnej części. W skład tego wchodzą:
- <NavBar /> – bo każda aplikacja musi mieć porządną nawigację.
- <FooterBar /> – żeby nie było, że zapominam o końcówce strony.
- Metoda created() – ustawia nazwę aplikacji dla każdego widoku, bo organizacja to podstawa.
<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>
W HomeView.vue umieszczam nagłówek w postaci komponentu CustomHeader, do którego przekazuję wartość "Animals Living in the ZOO", a w sekcji głównej pojawia się komponent MainPage.
<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. Komponenty
Widok strony głównej opiera się na MainPage, który wczytuje komponent ListAnimals. Aby uniknąć nieeleganckiego „mrugania” ekranu przy ładowaniu listy, wykorzystuję Suspense i komponent CustomLoading, który elegancko zajmuje użytkownika w międzyczasie.
<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>
Najważniejszy komponent to ListAnimals:
- Pobiera dane z pliku JSON.
- Losuje 9 zwierząt.
- Generuje ich obrazki.
- Renderuje wszystko w komponentach SingleAnimal, korzystając z pętli v-for.
<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>
Każdy SingleAnimal przyjmuje obiekt zwierzaka, renderuje obrazek i jego nazwę, a dodatkowo zawiera komponent MoreInfoAnimal, który jest kontrolowany przez parametr moreInfoAnimal. Domyślnie informacje są ukryte, a ich widoczność zmieniam po kliknięciu w przycisk. Całość działa na v-show, czyli nie znika z DOM-u – po prostu nie jest renderowana.
<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. Struktura JSON
Dane o zwierzakach przechowuję w pliku JSON, który zawiera listę obiektów. Każdy obiekt ma podstawowe informacje, jak np.:
{
"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"
}
Dzięki temu aplikacja nie wymaga backendu, a ja mogę się skupić na samym Vue.
7. Grafiki
Pisanie promptów do AI to prawdziwa sztuka – coś jak negocjacje z upartym artystą, który niby rozumie, co do niego mówisz, ale i tak zrobi po swojemu. Po kilku próbach doszedłem do wniosku, że kluczowe są:
- Cel i kontekst – zdjęcie do aplikacji, a nie galeria sztuki nowoczesnej.
- Światło i styl – naturalne oświetlenie, brak dramatycznych cieni.
- Nastrój i perspektywa – nikt nie chce oglądać ponurych, nieostrych zdjęć.
- Rozmiar zdjęcia – bo „za małe” lub „za duże” to wieczny problem.
Ostatecznie udało mi się wygenerować satysfakcjonujące grafiki przy pomocy kreatora obrazów Microsoft Bing – z odrobiną frustracji, ale i satysfakcją, gdy w końcu wyszło coś sensownego.
Utwórz zdjęcie Common hippopotamus. Rodzaj obrazu zdjęcie. Światło powinno być naturalne. Zastosuj filtr kinowy. Nastrój powinien być spokojny, perspektywa z przodu ale tak aby było widać zwierzę w całości! Rozmiar zdjęcia pionowy (9:16). To bardzo ważne. Zwierzę powinno być w swoim naturalnym środowisku.
8. Podsumowanie
Podczas pracy nad projektem:
- Nauczyłem się podstaw Vue.js.
- Odkryłem moc Suspense i innych wbudowanych komponentów.
- Przetestowałem routing i komunikację między komponentami.
- Przypomniałem sobie stylowanie w SASS + BEM.
- Poeksperymentowałem z generowaniem obrazków przez AI.
- Dowiedziałem się mnóstwa ciekawych rzeczy o zwierzętach.
- I co najważniejsze – świetnie się przy tym bawiłem!
To była dobra decyzja – nie tylko nauczyłem się nowych rzeczy, ale też stworzyłem coś, co faktycznie działa i może być użyteczne.
9 Efekt końcowy
- Link do aplikacji: Animals Living in the ZOO
- Kod na GitHub: Repozytorium GitHub
I teraz, moment na który czekaliście…