Skip to content

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í do app/_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, nie red. 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.css a do tailwind.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ýlHrúbkaMobilOd 768 pxVýška riadkuRozostup písmen
H1ExtraBold (800)24 px32 px120 %−4 %
H2Bold (700)20 px24 px125 %−4 %
H3SemiBold (600)16 px20 px130 %−4 %
BodyRegular (400)14 px16 px150 %−2 %
Body MediumMedium (500)14 px16 px150 %−2 %
SmallRegular (400)12 px14 px140 %0 %
Small MediumMedium (500)12 px14 px140 %0 %
ButtonMedium (500), veľké písmená12 px14 px140 %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ógy ion-alert. Na tieto prvky nepíšte vlastné HTML; iný vzhľad v dizajne upravte cez triedy alebo CSS premenné.
  • Jednoduché bloky robte ako div s Tailwind triedami – karta, štítok alebo zoznam sú len pozadie, zaoblenie, tieň a padding. Ionic komponenty ako ion-card, ion-chip alebo ion-list na ne nepoužívajte, viac komplikujú, než pomáhajú. Výnimkou je, keď ich potrebujete pre konkrétnu funkciu – napríklad ion-item vo vnútri ion-item-sliding pre 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ýstupformátovanie dátumu, porovnanie textu bez diakritiky
_composables/Reaktívna logika (ref, computed, watch) pre jednu obrazovku alebo funkciuvyhľadávanie a filtre v zozname podujatí
_stores/ (Pinia)Stav, ktorý potrebuje viac nesúvisiacich častí appkyprihlá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é do app/.

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. finally vypne 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'
	}
}