---
layout: 'page'
uri: '/guides/frontend-utils'
position: 3
slug: 'guides-frontend-utils'
parent: 'guides'
navTitle: 'Frontend Utils'
title: 'Frontend Utils'
description: 'Přehled všech frontend utilit — fetch, auth, toast, modals.'
---

# Frontend Utils

Rychlý přehled co je k dispozici a odkud to importovat.

| Utilita | Import | Kdy použít |
|---|---|---|
| [`apiFetch`](#apifetch) | `@/app-ui/Fetch` | Public endpoint (`/health`, bez JWT) |
| [`authFetch`](#authfetch) | `@/app-ui/Auth` | Protected API — auto-refresh na 401 |
| [`apiUpload`](#apiupload) | `@/app-ui/Fetch` | Upload souboru s progress |
| [`apiDownload`](#apidownload) | `@/app-ui/Fetch` | Download souboru (browser dialog) |
| [`useAuth`](#useauth) | `@/app-ui/Auth` | Session state, login/logout, permissions |
| [`useToast`](#usetoast) | `@/app-ui/Toast/useToast` | Notifikace |
| [`useClickOutside`](#useclickoutside) | `@/app-ui/ClickOutside/useClickOutside` | Detekce kliku mimo element |
| [`Dropdown`](#dropdown) | `@/app-ui/Dropdown/Dropdown.vue` | Click-outside dropdown menu (slot trigger + slot menu) |
| [`Modal`](#modal), [`ConfirmModal`](#confirmmodal) | `@/app-ui/Modals/*` | Dialogy |


## apiFetch

```typescript
import { apiFetch } from '@/app-ui/Fetch';

const result = await apiFetch<HealthResponse>('GET', '/health');
const result = await apiFetch<LoginResponse>('POST', '/api/v1/auth/login', {
    body: { nickname: 'admin', password: 'secret' },
});
const result = await apiFetch<UserList, ValidationError>('GET', '/api/v1/users');

if (result.success === true) { result.data; }
if (result.success === false) { result.data; /* { message: string } default */ }
```

Automaticky přidává `Authorization: Bearer` header pokud je nastaven token. **Neretrí na 401** — pro to použij `authFetch`.


## authFetch

```typescript
import { authFetch } from '@/app-ui/Auth';

const result = await authFetch<UserProfile>('GET', '/api/v1/profile');
```

Stejné API jako `apiFetch`, navíc při 401 automaticky zavolá `refresh()` a request zopakuje s novým tokenem. Paralelní 401 sdílí jedno volání `/auth/refresh`. Skip pro `/api/v1/auth/*` (login/refresh/logout se neretrají).


## apiUpload

```typescript
import { apiUpload } from '@/app-ui/Fetch';

const result = await apiUpload<UploadResult>('/api/v1/files', formData, (stats) => {
    stats.percent;  // 0-100
    stats.loaded;   // bytes
    stats.total;    // bytes
});
```


## apiDownload

```typescript
import { apiDownload } from '@/app-ui/Fetch';

const result = await apiDownload('/api/v1/exports/report.csv', 'report.csv');
// result: { success: true, status: 200, filename: 'report-2026-04.csv' }
```

Filename z `Content-Disposition`, fallback na druhý parametr.


## useAuth

```typescript
import { useAuth } from '@/app-ui/Auth';

const {
    user,              // Readonly<Ref<AuthUser | null>>
    isAuthenticated,   // Readonly<Ref<boolean>>
    login,             // (credentials) => Promise<ApiResponse>
    logout,            // () => Promise<void>
    refresh,           // () => Promise<boolean>
    hasRole,           // (role) => boolean
    isAdmin,           // () => boolean
    hasPermission,     // (permission) => boolean
    hasAllPermissions, // (permissions[]) => boolean
    hasAnyPermission,  // (permissions[]) => boolean
} = useAuth();
```

`AuthUser` má tvar `{ id, nickname, email, role, permissions[] }`. Auto-refresh běží 30s před expirací access tokenu. Při hard refreshi stránky `assets/app.ts:bootstrap()` zavolá `refresh()` ještě před mountem routeru -- session se obnoví ze cookie tiše.


## useToast

```typescript
import { useToast } from '@/app-ui/Toast/useToast';

const { success, error, info, warning, clear } = useToast();

success('Uloženo');
error('Něco se pokazilo');
info('Informace', 5000);       // vlastní duration (ms)
warning('Pozor', null);        // null = bez auto-dismiss
clear();                       // smaže všechny toasty
```


## useClickOutside

```typescript
import { ref } from 'vue';
import { useClickOutside } from '@/app-ui/ClickOutside/useClickOutside';

const containerRef = ref<HTMLElement | null>(null);
const close = (): void => { /* … */ };

useClickOutside(containerRef, close);
```

Composable navěsí listener na `document` při mountu a sundá ho při unmountu. Když klik dopadne **mimo** `containerRef`, zavolá se `close`. Používá se v `Dropdown.vue` pro auto-close.


## Dropdown

```html
<Dropdown>
    <template #trigger>
        <button type="button">Open</button>
    </template>

    <RouterLink to="/profile" class="block px-4 py-2 hover:bg-gray-100">
        Profile
    </RouterLink>
    <button type="button" class="block w-full text-left px-4 py-2 text-red-600 hover:bg-red-50" @click="logout">
        Sign out
    </button>
</Dropdown>
```

Slot `trigger` je ovládací prvek (button, ikona). Default slot je obsah menu. Klik na položku menu auto-zavře dropdown (parent má `@click="close"`). Klik mimo dropdown taky zavře (`useClickOutside`).


## Modal

```html
<Modal :show="isOpen" title="Detail" @close="isOpen = false">
    <p>Obsah modalu</p>
</Modal>
```


## ConfirmModal

```html
<ConfirmModal
    :show="isConfirmOpen"
    title="Smazat?"
    message="Tuto akci nelze vrátit."
    @confirm="handleDelete"
    @cancel="isConfirmOpen = false"
/>
```

---

[← Permissions](/guides/permissions.md) | [Forms & Validation →](/guides/forms.md)