ClubHub – Technické zásady
Tieto zásady platia počas celého projektu. Nie sú to jediné správne riešenia, ale základné pravidlá, vďaka ktorým sa v kóde vyznáte vy aj ten, kto robí code review. Môžete písať v JavaScripte aj v TypeScripte, je to na vás. Ukážky sú v JavaScripte.
Štruktúra projektu
Celý kód appky je v src/plugins/. Spoločné veci sú v app/, každá časť appky má vlastný modul s prefixom app@.
text
src/plugins/
├── app/ # spoločné pre celú appku
│ ├── _components/ # komponenty používané vo viacerých moduloch
│ ├── _config/ # main, App.vue, router, axios (.js alebo .ts)
│ ├── _data/ # mock dáta spoločné pre celú appku (napr. users.json)
│ ├── _layout/ # layout s navigáciou
│ ├── _stores/ # globálne store-y
│ ├── _themes/ # farby a globálne štýly
│ │ ├── colors.css # hlavné premenné farieb
│ │ ├── tailwind.css # farby pre Tailwind
│ │ └── ionic.css # farby pre Ionic komponenty
│ └── _utils/ # pomocné funkcie
├── app@events/ # modul Podujatia
│ ├── events-list.vue # hlavná obrazovka modulu
│ ├── _data/ # mock dáta modulu (events.json)
│ ├── _components/ # komponenty hlavnej obrazovky a spoločné v module
│ │ ├── event-card.vue
│ │ └── event-filters.vue
│ └── event-detail/ # ďalšia obrazovka má vlastný priečinok
│ ├── event-detail.vue
│ └── _components/
└── app@saved/ # modul Uložené
└── saved.vue # modul s jednou obrazovkou ostáva plochý- Každá väčšia časť appky (položka v navigácii) má vlastný modul. Nové moduly pridávate podľa potreby.
- Hlavná obrazovka je v koreňovom priečinku modulu, každá ďalšia obrazovka má vlastný podpriečinok so svojím
_components/. - Komponent, ktorý používa len jeden modul, patrí do jeho
_components/. Komponent používaný vo viacerých moduloch patrí doapp/_components/.
Pomenovanie
- Súbory a priečinky píšte malými písmenami so spojovníkom (kebab-case):
event-card.vue,format-date.js,event-detail/. - Kód je v angličtine – názvy súborov, premenných, funkcií aj komentáre. Texty, ktoré vidí používateľ, sú po slovensky.
- Komponenty v šablóne začínajú veľkým písmenom, aby bolo hneď vidieť, že to nie je obyčajný HTML element. Výnimkou sú Ionic elementy (
ion-page,ion-button…), tie sa píšu malými písmenami.
vue
<template>
<ion-page>
<ion-content>
<Event-card :event="event" />
</ion-content>
</ion-page>
</template>Farebná paleta
Farby sú rozdelené do 3 súborov v app/_themes/. Hlavné premenné sú len v jednom z nich, Tailwind a Ionic na ne iba odkazujú. Farby z dizajnu tak prepíšete raz a v komponentoch používate len ich názvy. Hodnoty nižšie sú farby ClubHubu z design systému vo Figme (sekcia Farby), názvy sa zhodujú s premennými vo Figme.
1. colors.css – hlavné premenné
Jediné miesto, kde sú hodnoty farieb. Píšte ich ako obyčajné CSS, nie do @layer – inak ich prebije odkaz z tailwind.css.
css
:root {
--color-primary: #7a45c7;
--color-primary-contrast: #ffffff;
--color-secondary: #f19c53;
--color-secondary-contrast: #000000;
--color-text: #27035a;
--color-text-muted: #27035aa3;
--color-background: #fafafa;
--color-surface: #ffffff;
--color-border: #27035a1f;
--color-success: #2e9e6b;
--color-danger: #e5484d;
--color-danger-contrast: #ffffff;
/* gradient – náhradný obrázok kategórie, úvodná obrazovka */
--gradient-primary: linear-gradient(135deg, #612ab0, #380d76);
/* priehľadné pozadia (10 %) */
--color-primary-10: #7a45c71a;
--color-success-10: #2e9e6b1a;
--color-danger-10: #e5484d1a;
}2. tailwind.css – farby pre Tailwind
Tailwind len odkazuje na hlavné premenné. Vďaka bloku @theme inline vzniknú triedy ako bg-primary, text-danger alebo border-text-muted.
css
@import 'tailwindcss';
@theme inline {
--color-primary: var(--color-primary);
--color-primary-contrast: var(--color-primary-contrast);
--color-secondary: var(--color-secondary);
--color-secondary-contrast: var(--color-secondary-contrast);
--color-text: var(--color-text);
--color-text-muted: var(--color-text-muted);
--color-background: var(--color-background);
--color-surface: var(--color-surface);
--color-border: var(--color-border);
--color-success: var(--color-success);
--color-danger: var(--color-danger);
--color-danger-contrast: var(--color-danger-contrast);
--color-primary-10: var(--color-primary-10);
--color-success-10: var(--color-success-10);
--color-danger-10: var(--color-danger-10);
}3. ionic.css – farby pre Ionic komponenty
Ionic komponenty (ion-button, ion-tab-bar…) berú farby z vlastných premenných. Hlavná farba odkazuje na premennú z colors.css. Ostatné odtiene (-rgb, -contrast, -shade, -tint) potrebuje Ionic ako hotové hodnoty – vygenerujte ich v Ionic color generátore a vložte sem.
css
:root {
--ion-color-primary: var(--color-primary);
--ion-color-primary-rgb: 122, 69, 199;
--ion-color-primary-contrast: var(--color-primary-contrast);
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #6b3daf;
--ion-color-primary-tint: #8758cd;
--ion-color-secondary: var(--color-secondary);
--ion-color-secondary-rgb: 241, 156, 83;
--ion-color-secondary-contrast: var(--color-secondary-contrast);
--ion-color-secondary-contrast-rgb: 0, 0, 0;
--ion-color-secondary-shade: #d48949;
--ion-color-secondary-tint: #f2a664;
/* rovnako pre danger (tlačidlo Odhlásiť sa) a ďalšie farby, ktoré používajú Ionic komponenty */
}Pravidlá
- V komponentoch nikdy nepíšte hex farbu (
#7a45c7) ani Tailwind farbu mimo palety (bg-blue-600). Vždy použite názov z palety:bg-primary. - Farby pomenujte podľa účelu, nie vzhľadu:
danger, niered. Keď sa zmení dizajn, zmení sa jedna hodnota v palete a názvy v kóde ostanú. - Chýba vám farba? Najprv ju pridajte do
colors.cssa dotailwind.css, až potom ju použite v komponente. - Zmenili ste farbu, ktorú používa Ionic? Vygenerujte jej odtiene v generátore znova a prepíšte ich v
ionic.css. - Text a ikony na farebnom pozadí majú vždy farbu
-contrast(biela alebo čierna), napr. oranžové tlačidlo má čierny text:bg-secondary text-secondary-contrast.
Písmo
Celá appka používa font Alexandria z Google Fonts. Pripojte ho v index.html a nastavte ako predvolený pre Tailwind aj Ionic:
html
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Alexandria:wght@400;500;600;700;800;900&display=swap" rel="stylesheet" />css
/* tailwind.css */
@theme {
--font-sans: 'Alexandria', sans-serif;
}
/* ionic.css */
:root {
--ion-font-family: 'Alexandria', sans-serif;
}Každý textový štýl z dizajnu má veľkosť pre mobil a väčšiu od šírky 768 px (tablet a počítač, v Tailwinde md:):
| Štýl | Hrúbka | Mobil | Od 768 px | Výška riadku | Rozostup písmen |
|---|---|---|---|---|---|
| H1 | ExtraBold (800) | 24 px | 32 px | 120 % | −4 % |
| H2 | Bold (700) | 20 px | 24 px | 125 % | −4 % |
| H3 | SemiBold (600) | 16 px | 20 px | 130 % | −4 % |
| Body | Regular (400) | 14 px | 16 px | 150 % | −2 % |
| Body Medium | Medium (500) | 14 px | 16 px | 150 % | −2 % |
| Small | Regular (400) | 12 px | 14 px | 140 % | 0 % |
| Small Medium | Medium (500) | 12 px | 14 px | 140 % | 0 % |
| Button | Medium (500), veľké písmená | 12 px | 14 px | 140 % | 0 % |
Štýly nastavte raz v tailwind.css pod blokom @theme. Nadpisy h1, h2 a h3 dostanú svoj štýl samy, ostatné štýly sú triedy, ktoré dáte na akýkoľvek text (<p class="text-small">, <span class="text-body-medium">). Rovnaké triedy fungujú aj na nadpisoch, keď má text vyzerať ako iný stupeň (<h2 class="text-h3">).
css
@layer base {
body {
font-size: 14px;
line-height: 150%;
letter-spacing: -0.02em;
}
h1,
.text-h1 {
font-size: 24px;
font-weight: 800;
line-height: 120%;
letter-spacing: -0.04em;
}
h2,
.text-h2 {
font-size: 20px;
font-weight: 700;
line-height: 125%;
letter-spacing: -0.04em;
}
h3,
.text-h3 {
font-size: 16px;
font-weight: 600;
line-height: 130%;
letter-spacing: -0.04em;
}
.text-body,
.text-body-medium {
font-size: 14px;
line-height: 150%;
letter-spacing: -0.02em;
}
.text-body-medium {
font-weight: 500;
}
.text-small,
.text-small-medium,
.text-button {
font-size: 12px;
line-height: 140%;
letter-spacing: 0;
}
.text-small-medium,
.text-button {
font-weight: 500;
}
.text-button {
text-transform: uppercase;
}
/* od 768 px (tablet a počítač) väčšie písmo */
@media (width >= 48rem) {
body,
.text-body,
.text-body-medium {
font-size: 16px;
}
h1,
.text-h1 {
font-size: 32px;
}
h2,
.text-h2 {
font-size: 24px;
}
h3,
.text-h3 {
font-size: 20px;
}
.text-small,
.text-small-medium,
.text-button {
font-size: 14px;
}
}
}Štýly sú vo vrstve base, takže ich bežné Tailwind triedy (font-bold, text-primary…) v komponente vedia prepísať.
Komponenty
- Čo sa opakuje, je komponent. Keď rovnaký kus šablóny potrebujete druhýkrát (aj na tej istej obrazovke), vytiahnite ho do komponentu a rozdiely pošlite cez props. Nekopírujte kód a neupravujte kópie zvlášť.
- Dáta dnu cez props, udalosti von cez emits. Komponent nemení dáta, ktoré dostal; oznámi rodičovi, čo sa stalo, a rodič rozhodne.
- Ionic používajte na ovládacie prvky a štruktúru appky –
ion-button,ion-input,ion-searchbar,ion-toggle,ion-select,ion-tabs,ion-page, dialógyion-alert. Na tieto prvky nepíšte vlastné HTML; iný vzhľad v dizajne upravte cez triedy alebo CSS premenné. - Jednoduché bloky robte ako
divs Tailwind triedami – karta, štítok alebo zoznam sú len pozadie, zaoblenie, tieň a padding. Ionic komponenty akoion-card,ion-chipaleboion-listna ne nepoužívajte, viac komplikujú, než pomáhajú. Výnimkou je, keď ich potrebujete pre konkrétnu funkciu – napríkladion-itemvo vnútriion-item-slidingpre posúvanie položky do strany. - Jeden komponent robí jednu vec. Keď má súbor stovky riadkov, rozdeľte ho na menšie komponenty.
vue
<script setup>
const props = defineProps({
event: { type: Object, required: true },
isSaved: { type: Boolean, default: false },
})
const emit = defineEmits(['toggle-saved'])
</script>Utils, composables a store
Keď píšete logiku mimo komponentu, rozhodnite sa podľa toho, či pracuje so stavom a kto ho potrebuje.
| Kam | Čo tam patrí | Príklad v ClubHub |
|---|---|---|
_utils/ | Čisté funkcie bez Vue a bez stavu – rovnaký vstup vždy dá rovnaký výstup | formátovanie dátumu, porovnanie textu bez diakritiky |
_composables/ | Reaktívna logika (ref, computed, watch) pre jednu obrazovku alebo funkciu | vyhľadávanie a filtre v zozname podujatí |
_stores/ (Pinia) | Stav, ktorý potrebuje viac nesúvisiacich častí appky | prihlásený používateľ, uložené podujatia |
- Pravidlo pre utils: ak by funkcia fungovala aj bez Vue, patrí do
_utils/. - Store nie je odkladisko. Do store-u nedávajte dočasný stav obrazovky (otvorený filter, text vo vyhľadávaní) ani hodnoty, ktoré sa dajú vypočítať z iných.
- Composable a store pre jeden modul patria do jeho priečinka (
app@events/_composables/), spoločné doapp/.
Volania servera
So serverom komunikujete cez axios. Server vracia dáta vždy v poli data, preto ich v odpovedi axiosu nájdete v response.data.data.
Nastavenie axiosu
Adresu servera a token nastavíte na jednom mieste, v súbore app/_config/axios.js. Súbor raz naimportujte v main.js (import './axios'). Pri volaniach potom píšete už len cestu, napríklad 'events', nikdy celú adresu servera.
js
// app/_config/axios.js
import axios from 'axios'
import { useAuthStore } from '../_stores/auth.store'
axios.defaults.baseURL = 'https://adresa-servera/api/' // adresu dostanete vo Fáze 3
// ku každému volaniu pridá token prihláseného používateľa
axios.interceptors.request.use(config => {
const authStore = useAuthStore()
if (authStore.token) {
config.headers.Authorization = `Bearer ${authStore.token}`
}
return config
})Načítavanie a chyby
Každé volanie servera môže chvíľu trvať a môže zlyhať. Appka preto vždy ukazuje, že sa niečo načítava, a chybu zobrazí používateľovi, nielen v konzole.
js
import axios from 'axios'
const events = ref([])
const isLoading = ref(false)
const errorMessage = ref(null)
async function loadEvents() {
isLoading.value = true
errorMessage.value = null
try {
const response = await axios.get('events')
events.value = response.data.data
} catch (error) {
console.error(error)
errorMessage.value = 'Podujatia sa nepodarilo načítať'
} finally {
isLoading.value = false
}
}- Vždy
try / catch / finally.finallyvypne načítavanie aj vtedy, keď volanie zlyhá. - Tlačidlo počas volania zablokujte, aby používateľ neposlal tú istú požiadavku dvakrát.
Zrušenie predchádzajúceho volania
Pri vyhľadávaní a filtroch zrušte predchádzajúce volanie cez AbortController. Inak môže staršia odpoveď prísť neskôr ako novšia a appka zobrazí výsledky pre nesprávny filter.
js
import axios from 'axios'
let controller = null
async function search(query) {
controller?.abort()
controller = new AbortController()
try {
const response = await axios.get('events', {
params: { query },
signal: controller.signal,
})
events.value = response.data.data
} catch (error) {
if (axios.isCancel(error)) return
errorMessage.value = 'Vyhľadávanie sa nepodarilo'
}
}